diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxUpdateManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxUpdateManager.java
index d77129baec1aa84122856125ab32d951b8b16425..e9c81237e48f9d4739b620914a29f67d2feb784a 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxUpdateManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxUpdateManager.java
@@ -63,9 +63,26 @@ public interface MailboxUpdateManager {
 	 */
 	String GROUP_KEY_SENT_CLIENT_SUPPORTS = "sentClientSupports";
 
+	/**
+	 * Returns the latest {@link MailboxUpdate} sent to the given contact.
+	 * <p>
+	 * If we have our own mailbox then the update will be a
+	 * {@link MailboxUpdateWithMailbox} containing the
+	 * {@link MailboxProperties} the contact should use for communicating with
+	 * our mailbox.
+	 */
 	MailboxUpdate getLocalUpdate(Transaction txn, ContactId c)
 			throws DbException;
 
+	/**
+	 * Returns the latest {@link MailboxUpdate} received from the given
+	 * contact, or null if no update has been received.
+	 * <p>
+	 * If the contact has a mailbox then the update will be a
+	 * {@link MailboxUpdateWithMailbox} containing the
+	 * {@link MailboxProperties} we should use for communicating with the
+	 * contact's mailbox.
+	 */
 	@Nullable
 	MailboxUpdate getRemoteUpdate(Transaction txn, ContactId c)
 			throws DbException;
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java
index 6abe9e6076ae853767f338acbe0e5464194000d8..54fcbfea310cd8444f01a9b75f021f8a7ed23278 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java
@@ -4,11 +4,13 @@ import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.TransactionManager;
 import org.briarproject.bramble.api.mailbox.MailboxProperties;
 import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
+import org.briarproject.bramble.api.mailbox.MailboxVersion;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.logging.Logger;
 
 import javax.annotation.concurrent.ThreadSafe;
@@ -55,11 +57,12 @@ class OwnMailboxConnectivityChecker extends ConnectivityCheckerImpl {
 	private boolean checkConnectivityAndStoreResult(
 			MailboxProperties properties) throws DbException {
 		try {
-			if (!mailboxApi.checkStatus(properties)) throw new ApiException();
+			List<MailboxVersion> serverSupports =
+					mailboxApi.getServerSupports(properties);
 			LOG.info("Own mailbox is reachable");
 			long now = clock.currentTimeMillis();
 			db.transaction(false, txn -> mailboxSettingsManager
-					.recordSuccessfulConnection(txn, now));
+					.recordSuccessfulConnection(txn, now, serverSupports));
 			// Call the observers and cache the result
 			onConnectivityCheckSucceeded(now);
 			return false; // Don't retry
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorker.java
new file mode 100644
index 0000000000000000000000000000000000000000..40fe296d6c57a7241b875cab7dcc17b9f85b1d7e
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorker.java
@@ -0,0 +1,369 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.Cancellable;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
+import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.NoSuchContactException;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.event.EventExecutor;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.mailbox.MailboxUpdate;
+import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
+import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
+import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
+import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
+import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+@ThreadSafe
+@NotNullByDefault
+class OwnMailboxContactListWorker
+		implements MailboxWorker, ConnectivityObserver, EventListener {
+
+	/**
+	 * When the worker is started it waits for a connectivity check, then
+	 * fetches the remote contact list and compares it to the local contact
+	 * list.
+	 * <p>
+	 * Any contacts that are missing from the remote list are added to the
+	 * mailbox's contact list, while any contacts that are missing from the
+	 * local list are removed from the mailbox's contact list.
+	 * <p>
+	 * Once the remote contact list has been brought up to date, the worker
+	 * waits for events indicating that contacts have been added or removed.
+	 * Each time an event is received, the worker updates the mailbox's
+	 * contact list and then goes back to waiting.
+	 */
+	private enum State {
+		CREATED,
+		CONNECTIVITY_CHECK,
+		FETCHING_CONTACT_LIST,
+		UPDATING_CONTACT_LIST,
+		WAITING_FOR_CHANGES,
+		DESTROYED
+	}
+
+	private static final Logger LOG =
+			getLogger(OwnMailboxContactListWorker.class.getName());
+
+	private final Executor ioExecutor;
+	private final DatabaseComponent db;
+	private final EventBus eventBus;
+	private final ConnectivityChecker connectivityChecker;
+	private final MailboxApiCaller mailboxApiCaller;
+	private final MailboxApi mailboxApi;
+	private final MailboxUpdateManager mailboxUpdateManager;
+	private final MailboxProperties mailboxProperties;
+	private final Object lock = new Object();
+
+	@GuardedBy("lock")
+	private State state = State.CREATED;
+
+	@GuardedBy("lock")
+	@Nullable
+	private Cancellable apiCall = null;
+
+	/**
+	 * A queue of updates waiting to be applied to the remote contact list.
+	 */
+	@GuardedBy("lock")
+	private final Queue<Update> updates = new LinkedList<>();
+
+	OwnMailboxContactListWorker(@IoExecutor Executor ioExecutor,
+			DatabaseComponent db,
+			EventBus eventBus,
+			ConnectivityChecker connectivityChecker,
+			MailboxApiCaller mailboxApiCaller,
+			MailboxApi mailboxApi,
+			MailboxUpdateManager mailboxUpdateManager,
+			MailboxProperties mailboxProperties) {
+		if (!mailboxProperties.isOwner()) throw new IllegalArgumentException();
+		this.ioExecutor = ioExecutor;
+		this.db = db;
+		this.connectivityChecker = connectivityChecker;
+		this.mailboxApiCaller = mailboxApiCaller;
+		this.mailboxApi = mailboxApi;
+		this.mailboxUpdateManager = mailboxUpdateManager;
+		this.mailboxProperties = mailboxProperties;
+		this.eventBus = eventBus;
+	}
+
+	@Override
+	public void start() {
+		LOG.info("Started");
+		synchronized (lock) {
+			if (state != State.CREATED) return;
+			state = State.CONNECTIVITY_CHECK;
+		}
+		// Avoid leaking observer in case destroy() is called concurrently
+		// before observer is added
+		connectivityChecker.checkConnectivity(mailboxProperties, this);
+		boolean destroyed;
+		synchronized (lock) {
+			destroyed = state == State.DESTROYED;
+		}
+		if (destroyed) connectivityChecker.removeObserver(this);
+	}
+
+	@Override
+	public void destroy() {
+		LOG.info("Destroyed");
+		Cancellable apiCall;
+		synchronized (lock) {
+			state = State.DESTROYED;
+			apiCall = this.apiCall;
+			this.apiCall = null;
+		}
+		if (apiCall != null) apiCall.cancel();
+		connectivityChecker.removeObserver(this);
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void onConnectivityCheckSucceeded() {
+		LOG.info("Connectivity check succeeded");
+		synchronized (lock) {
+			if (state != State.CONNECTIVITY_CHECK) return;
+			state = State.FETCHING_CONTACT_LIST;
+			apiCall = mailboxApiCaller.retryWithBackoff(
+					new SimpleApiCall(this::apiCallFetchContactList));
+		}
+	}
+
+	@IoExecutor
+	private void apiCallFetchContactList() throws IOException, ApiException {
+		synchronized (lock) {
+			if (state != State.FETCHING_CONTACT_LIST) return;
+		}
+		LOG.info("Fetching remote contact list");
+		Collection<ContactId> remote =
+				mailboxApi.getContacts(mailboxProperties);
+		ioExecutor.execute(() -> loadLocalContactList(remote));
+	}
+
+	@IoExecutor
+	private void loadLocalContactList(Collection<ContactId> remote) {
+		synchronized (lock) {
+			if (state != State.FETCHING_CONTACT_LIST) return;
+			apiCall = null;
+		}
+		LOG.info("Loading local contact list");
+		try {
+			db.transaction(true, txn -> {
+				Collection<Contact> local = db.getContacts(txn);
+				// Handle the result on the event executor to avoid races with
+				// incoming events
+				txn.attach(() -> reconcileContactLists(local, remote));
+			});
+		} catch (DbException e) {
+			logException(LOG, WARNING, e);
+		}
+	}
+
+	@EventExecutor
+	private void reconcileContactLists(Collection<Contact> local,
+			Collection<ContactId> remote) {
+		Set<ContactId> localIds = new HashSet<>();
+		for (Contact c : local) localIds.add(c.getId());
+		remote = new HashSet<>(remote);
+		synchronized (lock) {
+			if (state != State.FETCHING_CONTACT_LIST) return;
+			for (ContactId c : localIds) {
+				if (!remote.contains(c)) updates.add(new Update(true, c));
+			}
+			for (ContactId c : remote) {
+				if (!localIds.contains(c)) updates.add(new Update(false, c));
+			}
+			if (updates.isEmpty()) {
+				LOG.info("Contact list is up to date");
+				state = State.WAITING_FOR_CHANGES;
+			} else {
+				if (LOG.isLoggable(INFO)) {
+					LOG.info(updates.size() + " updates to apply");
+				}
+				state = State.UPDATING_CONTACT_LIST;
+				ioExecutor.execute(this::updateContactList);
+			}
+		}
+	}
+
+	@IoExecutor
+	private void updateContactList() {
+		Update update;
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST) return;
+			update = updates.poll();
+			if (update == null) {
+				LOG.info("No more updates to process");
+				state = State.WAITING_FOR_CHANGES;
+				apiCall = null;
+				return;
+			}
+		}
+		if (update.add) loadMailboxProperties(update.contactId);
+		else removeContact(update.contactId);
+	}
+
+	@IoExecutor
+	private void loadMailboxProperties(ContactId c) {
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST) return;
+		}
+		LOG.info("Loading mailbox properties for contact");
+		try {
+			MailboxUpdate mailboxUpdate = db.transactionWithResult(true, txn ->
+					mailboxUpdateManager.getLocalUpdate(txn, c));
+			if (mailboxUpdate instanceof MailboxUpdateWithMailbox) {
+				addContact(c, (MailboxUpdateWithMailbox) mailboxUpdate);
+			} else {
+				// Our own mailbox was concurrently unpaired. This worker will
+				// be destroyed soon, so we can stop here
+				LOG.info("Own mailbox was unpaired");
+			}
+		} catch (NoSuchContactException e) {
+			// Contact was removed concurrently. Move on to the next update.
+			// Later we may process a removal update for this contact, which
+			// was never added to the mailbox's contact list. The removal API
+			// call should fail safely with a TolerableFailureException
+			LOG.info("No such contact");
+			updateContactList();
+		} catch (DbException e) {
+			logException(LOG, WARNING, e);
+		}
+	}
+
+	@IoExecutor
+	private void addContact(ContactId c, MailboxUpdateWithMailbox withMailbox) {
+		MailboxProperties props = withMailbox.getMailboxProperties();
+		MailboxContact contact = new MailboxContact(c, props.getAuthToken(),
+				requireNonNull(props.getInboxId()),
+				requireNonNull(props.getOutboxId()));
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST) return;
+			apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
+					apiCallAddContact(contact)));
+		}
+	}
+
+	@IoExecutor
+	private void apiCallAddContact(MailboxContact contact)
+			throws IOException, ApiException, TolerableFailureException {
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST) return;
+		}
+		LOG.info("Adding contact to remote contact list");
+		mailboxApi.addContact(mailboxProperties, contact);
+		updateContactList();
+	}
+
+	@IoExecutor
+	private void removeContact(ContactId c) {
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST) return;
+			apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
+					apiCallRemoveContact(c)));
+		}
+	}
+
+	@IoExecutor
+	private void apiCallRemoveContact(ContactId c)
+			throws IOException, ApiException {
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST) return;
+		}
+		LOG.info("Removing contact from remote contact list");
+		try {
+			mailboxApi.deleteContact(mailboxProperties, c);
+		} catch (TolerableFailureException e) {
+			// Catch this so we can continue to the next update
+			logException(LOG, INFO, e);
+		}
+		updateContactList();
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof ContactAddedEvent) {
+			LOG.info("Contact added");
+			onContactAdded(((ContactAddedEvent) e).getContactId());
+		} else if (e instanceof ContactRemovedEvent) {
+			LOG.info("Contact removed");
+			onContactRemoved(((ContactRemovedEvent) e).getContactId());
+		}
+	}
+
+	@EventExecutor
+	private void onContactAdded(ContactId c) {
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST &&
+					state != State.WAITING_FOR_CHANGES) {
+				return;
+			}
+			updates.add(new Update(true, c));
+			if (state == State.WAITING_FOR_CHANGES) {
+				state = State.UPDATING_CONTACT_LIST;
+				ioExecutor.execute(this::updateContactList);
+			}
+		}
+	}
+
+	@EventExecutor
+	private void onContactRemoved(ContactId c) {
+		synchronized (lock) {
+			if (state != State.UPDATING_CONTACT_LIST &&
+					state != State.WAITING_FOR_CHANGES) {
+				return;
+			}
+			updates.add(new Update(false, c));
+			if (state == State.WAITING_FOR_CHANGES) {
+				state = State.UPDATING_CONTACT_LIST;
+				ioExecutor.execute(this::updateContactList);
+			}
+		}
+	}
+
+	/**
+	 * An update that should be applied to the remote contact list.
+	 */
+	private static class Update {
+
+		/**
+		 * True if the contact should be added, false if the contact should be
+		 * removed.
+		 */
+		private final boolean add;
+		private final ContactId contactId;
+
+		private Update(boolean add, ContactId contactId) {
+			this.add = add;
+			this.contactId = contactId;
+		}
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java
index 159d0262fd6dd893994c4c38d285173c5358d74a..7c2dd18813f3c6ec1bc55efb97ddb0f5105d536e 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java
@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.db.TransactionManager;
 import org.briarproject.bramble.api.mailbox.MailboxProperties;
 import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
+import org.briarproject.bramble.api.mailbox.MailboxVersion;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
 import org.briarproject.bramble.test.BrambleMockTestCase;
@@ -15,8 +16,10 @@ import org.jmock.lib.action.DoAllAction;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
+import static java.util.Collections.singletonList;
 import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
 import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
 import static org.junit.Assert.assertFalse;
@@ -39,6 +42,8 @@ public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
 	private final MailboxProperties properties =
 			getMailboxProperties(true, CLIENT_SUPPORTS);
 	private final long now = System.currentTimeMillis();
+	private final List<MailboxVersion> serverSupports =
+			singletonList(new MailboxVersion(123, 456));
 
 	@Test
 	public void testObserverIsCalledWhenCheckSucceeds() throws Exception {
@@ -62,12 +67,13 @@ public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
 		// When the check succeeds, the success should be recorded in the DB
 		// and the observer should be called
 		context.checking(new DbExpectations() {{
-			oneOf(mailboxApi).checkStatus(properties);
-			will(returnValue(true));
+			oneOf(mailboxApi).getServerSupports(properties);
+			will(returnValue(serverSupports));
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(now));
 			oneOf(db).transaction(with(false), withDbRunnable(txn));
-			oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, now);
+			oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, now,
+					serverSupports);
 			oneOf(observer).onConnectivityCheckSucceeded();
 		}});
 
@@ -97,7 +103,7 @@ public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
 		// When the check fails, the failure should be recorded in the DB and
 		// the observer should not be called
 		context.checking(new DbExpectations() {{
-			oneOf(mailboxApi).checkStatus(properties);
+			oneOf(mailboxApi).getServerSupports(properties);
 			will(throwException(new IOException()));
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(now));
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorkerTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorkerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..12232122e18a44a1ca9fe49bd401ea270b608dc4
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorkerTest.java
@@ -0,0 +1,406 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.Cancellable;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
+import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.NoSuchContactException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.mailbox.MailboxUpdate;
+import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
+import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
+import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
+import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
+import org.briarproject.bramble.test.BrambleMockTestCase;
+import org.briarproject.bramble.test.CaptureArgumentAction;
+import org.briarproject.bramble.test.DbExpectations;
+import org.briarproject.bramble.test.RunAction;
+import org.jmock.Expectations;
+import org.jmock.lib.action.DoAllAction;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
+import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
+import static org.briarproject.bramble.test.TestUtils.getContact;
+import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class OwnMailboxContactListWorkerTest extends BrambleMockTestCase {
+
+	private final Executor ioExecutor = context.mock(Executor.class);
+	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
+	private final EventBus eventBus = context.mock(EventBus.class);
+	private final ConnectivityChecker connectivityChecker =
+			context.mock(ConnectivityChecker.class);
+	private final MailboxApiCaller mailboxApiCaller =
+			context.mock(MailboxApiCaller.class);
+	private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
+	private final MailboxUpdateManager mailboxUpdateManager =
+			context.mock(MailboxUpdateManager.class);
+	private final Cancellable apiCall = context.mock(Cancellable.class);
+
+	private final MailboxProperties mailboxProperties =
+			getMailboxProperties(true, CLIENT_SUPPORTS);
+	private final Contact contact1 = getContact(), contact2 = getContact();
+	private final MailboxProperties contactProperties1 =
+			getMailboxProperties(false, CLIENT_SUPPORTS);
+	private final MailboxProperties contactProperties2 =
+			getMailboxProperties(false, CLIENT_SUPPORTS);
+	private final MailboxUpdateWithMailbox update1 =
+			new MailboxUpdateWithMailbox(CLIENT_SUPPORTS, contactProperties1);
+	private final MailboxUpdateWithMailbox update2 =
+			new MailboxUpdateWithMailbox(CLIENT_SUPPORTS, contactProperties2);
+
+	private final OwnMailboxContactListWorker worker =
+			new OwnMailboxContactListWorker(ioExecutor, db, eventBus,
+					connectivityChecker, mailboxApiCaller, mailboxApi,
+					mailboxUpdateManager, mailboxProperties);
+
+	@Test
+	public void testChecksConnectivityWhenStartedAndRemovesObserverWhenDestroyed() {
+		// When the worker is started it should start a connectivity check
+		expectStartConnectivityCheck();
+
+		worker.start();
+
+		// When the worker is destroyed it should remove the connectivity
+		// observer and event listener
+		expectRemoveConnectivityObserverAndEventListener();
+
+		worker.destroy();
+	}
+
+	@Test
+	public void testUpdatesContactListWhenConnectivityCheckSucceeds()
+			throws Exception {
+		// When the worker is started it should start a connectivity check
+		expectStartConnectivityCheck();
+
+		worker.start();
+
+		// When the connectivity check succeeds, the worker should start a
+		// task to fetch the remote contact list
+		AtomicReference<ApiCall> fetchList = new AtomicReference<>();
+		expectStartTaskToFetchRemoteContactList(fetchList);
+
+		worker.onConnectivityCheckSucceeded();
+
+		// When the fetch task runs it should fetch the remote contact list,
+		// load the local contact list, and find the differences. Contact 2
+		// needs to be added and contact 1 needs to be removed. The worker
+		// should load the mailbox update for contact 2 and start a task to
+		// add contact 2 to the mailbox
+		expectFetchRemoteContactList(singletonList(contact1.getId()));
+		expectRunTaskOnIoExecutor();
+		expectLoadLocalContactList(singletonList(contact2));
+		expectRunTaskOnIoExecutor();
+		expectLoadMailboxUpdate(contact2, update2);
+		AtomicReference<ApiCall> addContact = new AtomicReference<>();
+		expectStartTaskToAddContact(addContact);
+
+		assertFalse(fetchList.get().callApi());
+
+		// When the add-contact task runs it should add contact 2 to the
+		// mailbox, then continue with the next update
+		AtomicReference<MailboxContact> added = new AtomicReference<>();
+		expectAddContactToMailbox(added);
+		AtomicReference<ApiCall> removeContact = new AtomicReference<>();
+		expectStartTaskToRemoveContact(removeContact);
+
+		assertFalse(addContact.get().callApi());
+
+		// Check that the added contact has the expected properties
+		MailboxContact expected = new MailboxContact(contact2.getId(),
+				contactProperties2.getAuthToken(),
+				requireNonNull(contactProperties2.getInboxId()),
+				requireNonNull(contactProperties2.getOutboxId()));
+		assertMailboxContactEquals(expected, added.get());
+
+		// When the remove-contact task runs it should remove contact 1 from
+		// the mailbox
+		expectRemoveContactFromMailbox(contact1);
+
+		assertFalse(removeContact.get().callApi());
+
+		// When the worker is destroyed it should remove the connectivity
+		// observer and event listener
+		expectRemoveConnectivityObserverAndEventListener();
+
+		worker.destroy();
+	}
+
+	@Test
+	public void testHandlesEventsAfterMakingInitialUpdates() throws Exception {
+		// When the worker is started it should start a connectivity check
+		expectStartConnectivityCheck();
+
+		worker.start();
+
+		// When the connectivity check succeeds, the worker should start a
+		// task to fetch the remote contact list
+		AtomicReference<ApiCall> fetchList = new AtomicReference<>();
+		expectStartTaskToFetchRemoteContactList(fetchList);
+
+		worker.onConnectivityCheckSucceeded();
+
+		// When the fetch task runs it should fetch the remote contact list,
+		// load the local contact list, and find the differences. The lists
+		// are the same, so the worker should wait for changes
+		expectFetchRemoteContactList(emptyList());
+		expectRunTaskOnIoExecutor();
+		expectLoadLocalContactList(emptyList());
+
+		assertFalse(fetchList.get().callApi());
+
+		// When a contact is added, the worker should load the contact's
+		// mailbox update and start a task to add the contact to the mailbox
+		expectRunTaskOnIoExecutor();
+		expectLoadMailboxUpdate(contact1, update1);
+		AtomicReference<ApiCall> addContact = new AtomicReference<>();
+		expectStartTaskToAddContact(addContact);
+
+		worker.eventOccurred(new ContactAddedEvent(contact1.getId(), true));
+
+		// When the add-contact task runs it should add contact 1 to the
+		// mailbox
+		AtomicReference<MailboxContact> added = new AtomicReference<>();
+		expectAddContactToMailbox(added);
+
+		assertFalse(addContact.get().callApi());
+
+		// Check that the added contact has the expected properties
+		MailboxContact expected = new MailboxContact(contact1.getId(),
+				contactProperties1.getAuthToken(),
+				requireNonNull(contactProperties1.getInboxId()),
+				requireNonNull(contactProperties1.getOutboxId()));
+		assertMailboxContactEquals(expected, added.get());
+
+		// When the contact is removed again, the worker should start a task
+		// to remove the contact from the mailbox
+		expectRunTaskOnIoExecutor();
+		AtomicReference<ApiCall> removeContact = new AtomicReference<>();
+		expectStartTaskToRemoveContact(removeContact);
+
+		worker.eventOccurred(new ContactRemovedEvent(contact1.getId()));
+
+		// When the remove-contact task runs it should remove the contact from
+		// the mailbox
+		expectRemoveContactFromMailbox(contact1);
+
+		assertFalse(removeContact.get().callApi());
+
+		// When the worker is destroyed it should remove the connectivity
+		// observer and event listener
+		expectRemoveConnectivityObserverAndEventListener();
+
+		worker.destroy();
+	}
+
+
+	@Test
+	public void testHandlesNoSuchContactException() throws Exception {
+		// When the worker is started it should start a connectivity check
+		expectStartConnectivityCheck();
+
+		worker.start();
+
+		// When the connectivity check succeeds, the worker should start a
+		// task to fetch the remote contact list
+		AtomicReference<ApiCall> fetchList = new AtomicReference<>();
+		expectStartTaskToFetchRemoteContactList(fetchList);
+
+		worker.onConnectivityCheckSucceeded();
+
+		// When the fetch task runs it should fetch the remote contact list,
+		// load the local contact list, and find the differences. Contact 1
+		// needs to be added, so the worker should submit a task to the
+		// IO executor to load the contact's mailbox update
+		expectFetchRemoteContactList(emptyList());
+		expectRunTaskOnIoExecutor();
+		expectLoadLocalContactList(singletonList(contact1));
+		AtomicReference<Runnable> loadUpdate = new AtomicReference<>();
+		context.checking(new Expectations() {{
+			oneOf(ioExecutor).execute(with(any(Runnable.class)));
+			will(new CaptureArgumentAction<>(loadUpdate, Runnable.class, 0));
+		}});
+
+		assertFalse(fetchList.get().callApi());
+
+		// Before the contact's mailbox update can be loaded, the contact
+		// is removed
+		worker.eventOccurred(new ContactRemovedEvent(contact1.getId()));
+
+		// When the load-update task runs, a NoSuchContactException is thrown.
+		// The worker should abandon adding the contact and move on to the
+		// next update, which is the removal of the same contact. The worker
+		// should start a task to remove the contact from the mailbox
+		Transaction txn = new Transaction(null, false);
+		context.checking(new DbExpectations() {{
+			oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
+			oneOf(mailboxUpdateManager).getLocalUpdate(txn, contact1.getId());
+			will(throwException(new NoSuchContactException()));
+		}});
+		AtomicReference<ApiCall> removeContact = new AtomicReference<>();
+		expectStartTaskToRemoveContact(removeContact);
+
+		loadUpdate.get().run();
+
+		// When the remove-contact task runs it should remove the contact from
+		// the mailbox. The contact was never added, so the call throws a
+		// TolerableFailureException
+		context.checking(new Expectations() {{
+			oneOf(mailboxApi).deleteContact(mailboxProperties,
+					contact1.getId());
+			will(throwException(new TolerableFailureException()));
+		}});
+
+		assertFalse(removeContact.get().callApi());
+
+		// When the worker is destroyed it should remove the connectivity
+		// observer and event listener
+		expectRemoveConnectivityObserverAndEventListener();
+
+		worker.destroy();
+	}
+
+	@Test
+	public void testCancelsApiCallWhenDestroyed() {
+		// When the worker is started it should start a connectivity check
+		expectStartConnectivityCheck();
+
+		worker.start();
+
+		// When the connectivity check succeeds, the worker should start a
+		// task to fetch the remote contact list
+		AtomicReference<ApiCall> fetchList = new AtomicReference<>();
+		expectStartTaskToFetchRemoteContactList(fetchList);
+
+		worker.onConnectivityCheckSucceeded();
+
+		// The worker is destroyed before the task runs. The worker should
+		// cancel the task remove the connectivity observer and event listener
+		context.checking(new Expectations() {{
+			oneOf(apiCall).cancel();
+		}});
+		expectRemoveConnectivityObserverAndEventListener();
+
+		worker.destroy();
+	}
+
+	private void expectStartConnectivityCheck() {
+		context.checking(new Expectations() {{
+			oneOf(connectivityChecker).checkConnectivity(
+					with(mailboxProperties), with(worker));
+		}});
+	}
+
+	private void expectRemoveConnectivityObserverAndEventListener() {
+		context.checking(new Expectations() {{
+			oneOf(connectivityChecker).removeObserver(worker);
+			oneOf(eventBus).removeListener(worker);
+		}});
+	}
+
+	private void expectStartTaskToFetchRemoteContactList(
+			AtomicReference<ApiCall> task) {
+		context.checking(new Expectations() {{
+			oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
+			will(new DoAllAction(
+					new CaptureArgumentAction<>(task, ApiCall.class, 0),
+					returnValue(apiCall)
+			));
+		}});
+	}
+
+	private void expectFetchRemoteContactList(List<ContactId> remote)
+			throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(mailboxApi).getContacts(mailboxProperties);
+			will(returnValue(remote));
+		}});
+	}
+
+	private void expectLoadLocalContactList(List<Contact> local)
+			throws Exception {
+		Transaction txn = new Transaction(null, true);
+
+		context.checking(new DbExpectations() {{
+			oneOf(db).transaction(with(true), withDbRunnable(txn));
+			oneOf(db).getContacts(txn);
+			will(returnValue(local));
+		}});
+	}
+
+	private void expectLoadMailboxUpdate(Contact c, MailboxUpdate update)
+			throws Exception {
+		Transaction txn = new Transaction(null, true);
+
+		context.checking(new DbExpectations() {{
+			oneOf(db).transactionWithResult(with(true),
+					withDbCallable(txn));
+			oneOf(mailboxUpdateManager).getLocalUpdate(txn, c.getId());
+			will(returnValue(update));
+		}});
+	}
+
+	private void expectStartTaskToAddContact(AtomicReference<ApiCall> task) {
+		context.checking(new Expectations() {{
+			oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
+			will(new DoAllAction(
+					new CaptureArgumentAction<>(task, ApiCall.class, 0),
+					returnValue(apiCall)
+			));
+		}});
+	}
+
+	private void expectAddContactToMailbox(
+			AtomicReference<MailboxContact> added) throws Exception {
+		context.checking(new DbExpectations() {{
+			oneOf(mailboxApi).addContact(with(mailboxProperties),
+					with(any(MailboxContact.class)));
+			will(new CaptureArgumentAction<>(added, MailboxContact.class, 1));
+		}});
+	}
+
+	private void expectStartTaskToRemoveContact(AtomicReference<ApiCall> task) {
+		context.checking(new DbExpectations() {{
+			oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
+			will(new DoAllAction(
+					new CaptureArgumentAction<>(task, ApiCall.class, 0),
+					returnValue(apiCall)
+			));
+		}});
+	}
+
+	private void expectRemoveContactFromMailbox(Contact c) throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(mailboxApi).deleteContact(mailboxProperties, c.getId());
+		}});
+	}
+
+	private void expectRunTaskOnIoExecutor() {
+		context.checking(new Expectations() {{
+			oneOf(ioExecutor).execute(with(any(Runnable.class)));
+			will(new RunAction());
+		}});
+	}
+
+	private void assertMailboxContactEquals(MailboxContact expected,
+			MailboxContact actual) {
+		assertEquals(expected.contactId, actual.contactId);
+		assertEquals(expected.token, actual.token);
+		assertEquals(expected.inboxId, actual.inboxId);
+		assertEquals(expected.outboxId, actual.outboxId);
+	}
+}