diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/Contact.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/Contact.java
index 1d921e8017f62e03100fda3e16f53692eac31155..c7aea8607f21c440279f37eeb2bafada08ed1c2d 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/Contact.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/Contact.java
@@ -6,6 +6,8 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
+
 @Immutable
 @NotNullByDefault
 public class Contact {
@@ -36,6 +38,10 @@ public class Contact {
 		return localAuthorId;
 	}
 
+	public int getType() {
+		return CONTACT;
+	}
+
 	public boolean isVerified() {
 		return verified;
 	}
@@ -44,6 +50,10 @@ public class Contact {
 		return active;
 	}
 
+	public boolean isHidden() {
+		return false;
+	}
+
 	@Override
 	public int hashCode() {
 		return id.hashCode();
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java
index 28857276e85a8048a0cc38d2b12ef9519fa95c1e..ab9daacc9b17a6708c6e421eb65d3bc46af9cc0f 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java
@@ -1,10 +1,14 @@
 package org.briarproject.bramble.api.contact;
 
 import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
+import org.briarproject.bramble.api.properties.TransportProperties;
+
+import java.util.Map;
 
 /**
  * A task for conducting a contact information exchange with a remote peer.
@@ -44,5 +48,23 @@ public interface ContactExchangeTask {
 	void startExchange(ContactExchangeListener listener,
 			LocalAuthor localAuthor, SecretKey masterSecret,
 			DuplexTransportConnection conn, TransportId transportId,
-			boolean alice);
+			boolean alice, int localType, int remoteType);
+
+	class ContactInfo {
+		public final Author author;
+		public final Map<TransportId, TransportProperties> properties;
+		public final byte[] signature;
+		public final long timestamp;
+		public final int type;
+
+		public ContactInfo(Author author,
+				Map<TransportId, TransportProperties> properties,
+				byte[] signature, long timestamp, int type) {
+			this.author = author;
+			this.properties = properties;
+			this.signature = signature;
+			this.timestamp = timestamp;
+			this.type = type;
+		}
+	}
 }
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..b66eb4f080a9777d997e685b319264dfdd251631
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactFactory.java
@@ -0,0 +1,30 @@
+package org.briarproject.bramble.api.contact;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
+import static org.briarproject.bramble.api.contact.ContactTypes.MAILBOX_OWNER;
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+
+@NotNullByDefault
+public class ContactFactory {
+
+	public static Contact createContact(ContactId c, Author author,
+			AuthorId localAuthorId, boolean verified, boolean active,
+			int type) {
+		switch (type) {
+			case CONTACT:
+				return new Contact(c, author, localAuthorId, verified, active);
+			case PRIVATE_MAILBOX:
+				return new PrivateMailbox(c, author, localAuthorId, verified,
+						active);
+			case MAILBOX_OWNER:
+				return new MailboxOwner(c, author, localAuthorId, verified,
+						active);
+			default:
+				throw new IllegalArgumentException("Unknown contact type");
+		}
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java
index 1f9daf48afc077a71ca87fa1a0a00de4db46cd99..23319c2d5c1e5c628b15e806c172429c7848a0d2 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java
@@ -49,6 +49,16 @@ public interface ContactManager {
 			long timestamp, boolean alice, boolean verified, boolean active)
 			throws DbException;
 
+
+	/**
+	 *  Add Repeater Contact
+	 */
+	ContactId addPrivateMailbox(Transaction txn, Author remote, AuthorId local,
+			SecretKey master, long timestamp, boolean alice) throws DbException;
+
+	ContactId addMailboxOwner(Transaction txn, Author remote, AuthorId local,
+			SecretKey master, long timestamp, boolean alice) throws DbException;
+
 	/**
 	 * Returns the contact with the given ID.
 	 */
@@ -77,6 +87,16 @@ public interface ContactManager {
 	 */
 	Collection<Contact> getActiveContacts() throws DbException;
 
+	/**
+	 * Returns all contacts which are visible to the user.
+	 */
+	Collection<Contact> getVisibleContacts() throws DbException;
+
+	/**
+	 * Returns all contacts of the give type.
+	 */
+	Collection<Contact> getContactsByType(int type) throws DbException;
+
 	/**
 	 * Removes a contact and all associated state.
 	 */
@@ -105,6 +125,7 @@ public interface ContactManager {
 	boolean contactExists(AuthorId remoteAuthorId, AuthorId localAuthorId)
 			throws DbException;
 
+
 	interface ContactHook {
 
 		void addingContact(Transaction txn, Contact c) throws DbException;
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactTypes.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..33056f87b8ea4fecbf865858fa738afdb61fc66a
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactTypes.java
@@ -0,0 +1,9 @@
+package org.briarproject.bramble.api.contact;
+
+public interface ContactTypes {
+	int CONTACT = 0;
+	int PRIVATE_MAILBOX = 1;
+	int CONTACT_MAILBOX = 2;
+	int MAILBOX_OWNER = 3;
+	int MAILBOX_CONTACT = 4;
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/MailboxExchangeTask.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/MailboxExchangeTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d7f60b9b7af07d2917271046623bd5ebaf7f4e4
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/MailboxExchangeTask.java
@@ -0,0 +1,8 @@
+package org.briarproject.bramble.api.contact;
+
+public interface MailboxExchangeTask extends ContactExchangeTask {
+		/**
+		 * The current version of the contact exchange protocol.
+		 */
+		byte PROTOCOL_VERSION = 2;
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/MailboxOwner.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/MailboxOwner.java
new file mode 100644
index 0000000000000000000000000000000000000000..067e55ad7ecb8076f8c14a24beabe3ad5845fd74
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/MailboxOwner.java
@@ -0,0 +1,31 @@
+package org.briarproject.bramble.api.contact;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.contact.ContactTypes.MAILBOX_OWNER;
+
+@Immutable
+@NotNullByDefault
+public class MailboxOwner extends Contact {
+
+	public MailboxOwner(ContactId id,
+			Author author,
+			AuthorId localAuthorId,
+			boolean verified, boolean active) {
+		super(id, author, localAuthorId, verified, active);
+	}
+
+	@Override
+	public int getType() {
+		return MAILBOX_OWNER;
+	}
+
+	@Override
+	public boolean isHidden() {
+		return true;
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PrivateMailbox.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PrivateMailbox.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5b6a9c9a7de3812687fbf28d2e0e7808cf0363b
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PrivateMailbox.java
@@ -0,0 +1,31 @@
+package org.briarproject.bramble.api.contact;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+
+@Immutable
+@NotNullByDefault
+public class PrivateMailbox extends Contact {
+
+	public PrivateMailbox(ContactId id,
+			Author author,
+			AuthorId localAuthorId,
+			boolean verified, boolean active) {
+		super(id, author, localAuthorId, verified, active);
+	}
+
+	@Override
+	public int getType() {
+		return PRIVATE_MAILBOX;
+	}
+
+	@Override
+	public boolean isHidden() {
+		return true;
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/RecordTypes.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/RecordTypes.java
index bd24dcf67d892b5a37696f7777b500e57e8a5fbb..0375a1c7b1f9ff2fa97df69b86f91dd35510890f 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/RecordTypes.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/RecordTypes.java
@@ -6,4 +6,5 @@ package org.briarproject.bramble.api.contact;
 public interface RecordTypes {
 
 	byte CONTACT_INFO = 0;
+	byte MAILBOX_INFO = 1;
 }
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
index 914b88a2e2d1db1a8bafa328850ca07dd80ed189..db32449aa36e4c163fa2c70a625e9f875b96569f 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
@@ -79,7 +79,7 @@ public interface DatabaseComponent {
 	 * and returns an ID for the contact.
 	 */
 	ContactId addContact(Transaction txn, Author remote, AuthorId local,
-			boolean verified, boolean active) throws DbException;
+			boolean verified, boolean active, int type) throws DbException;
 
 	/**
 	 * Stores a group.
@@ -208,6 +208,14 @@ public interface DatabaseComponent {
 	Collection<Contact> getContactsByAuthorId(Transaction txn, AuthorId remote)
 			throws DbException;
 
+	/**
+	 * Returns a possibly empty collection of contacts of the given type.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<Contact> getContactsByType(Transaction txn, int type)
+			throws DbException;
+
 	/**
 	 * Returns all contacts associated with the given local pseudonym.
 	 * <p/>
@@ -267,7 +275,7 @@ public interface DatabaseComponent {
 	 * Read-only.
 	 */
 	Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
-		throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns the IDs of any messages that need to be validated.
@@ -487,7 +495,7 @@ public interface DatabaseComponent {
 	 * Removes the given transport keys from the database.
 	 */
 	void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
-		throws DbException;
+			throws DbException;
 
 	/**
 	 * Marks the given contact as verified.
@@ -534,7 +542,7 @@ public interface DatabaseComponent {
 	 * Marks the given transport keys as usable for outgoing streams.
 	 */
 	void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
-		throws DbException;
+			throws DbException;
 
 	/**
 	 * Stores the given transport keys, deleting any keys they have replaced.
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java
index afc40d91b87198f658f2eb39cda20f1955aa88ca..05fe22ae3c61f7d40f105daec552fea8f5882631 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java
@@ -49,6 +49,7 @@ import static org.briarproject.bramble.util.ValidationUtils.checkSize;
 @NotNullByDefault
 class ClientHelperImpl implements ClientHelper {
 
+
 	/**
 	 * Length in bytes of the random salt used for creating local messages for
 	 * storing metadata.
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java
index 69a543058c0d9f1bf1fa94f86b8a85a19a5f8ec1..16bdc9ec52e86ef077c79da2c543a4405a2ce52e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java
@@ -78,6 +78,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
 	private volatile TransportId transportId;
 	private volatile SecretKey masterSecret;
 	private volatile boolean alice;
+	private volatile int localType, remoteType;
 
 	@Inject
 	ContactExchangeTaskImpl(DatabaseComponent db, ClientHelper clientHelper,
@@ -104,13 +105,15 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
 	public void startExchange(ContactExchangeListener listener,
 			LocalAuthor localAuthor, SecretKey masterSecret,
 			DuplexTransportConnection conn, TransportId transportId,
-			boolean alice) {
+			boolean alice, int localType, int remoteType) {
 		this.listener = listener;
 		this.localAuthor = localAuthor;
 		this.conn = conn;
 		this.transportId = transportId;
 		this.masterSecret = masterSecret;
 		this.alice = alice;
+		this.localType = localType;
+		this.remoteType = remoteType;
 		start();
 	}
 
@@ -281,7 +284,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
 		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
 		long timestamp = payload.getLong(3);
 		if (timestamp < 0) throw new FormatException();
-		return new ContactInfo(author, properties, signature, timestamp);
+		return new ContactInfo(author, properties, signature, timestamp, remoteType);
 	}
 
 	private ContactId addContact(Author remoteAuthor, long timestamp,
@@ -311,21 +314,4 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
 			logException(LOG, WARNING, e);
 		}
 	}
-
-	private static class ContactInfo {
-
-		private final Author author;
-		private final Map<TransportId, TransportProperties> properties;
-		private final byte[] signature;
-		private final long timestamp;
-
-		private ContactInfo(Author author,
-				Map<TransportId, TransportProperties> properties,
-				byte[] signature, long timestamp) {
-			this.author = author;
-			this.properties = properties;
-			this.signature = signature;
-			this.timestamp = timestamp;
-		}
-	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
index 02c94fc750aa1cef2edf7673b34d4fa40e65f94d..3d89da84e40d13df9c15335199d380bdf58a4a23 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
@@ -21,6 +21,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.inject.Inject;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.MAILBOX_OWNER;
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
+
 @ThreadSafe
 @NotNullByDefault
 class ContactManagerImpl implements ContactManager {
@@ -45,7 +49,8 @@ class ContactManagerImpl implements ContactManager {
 	public ContactId addContact(Transaction txn, Author remote, AuthorId local,
 			SecretKey master, long timestamp, boolean alice, boolean verified,
 			boolean active) throws DbException {
-		ContactId c = db.addContact(txn, remote, local, verified, active);
+		ContactId c = db.addContact(txn, remote, local, verified, active,
+				CONTACT);
 		keyManager.addContact(txn, c, master, timestamp, alice, active);
 		Contact contact = db.getContact(txn, c);
 		for (ContactHook hook : hooks) hook.addingContact(txn, contact);
@@ -55,7 +60,8 @@ class ContactManagerImpl implements ContactManager {
 	@Override
 	public ContactId addContact(Transaction txn, Author remote, AuthorId local,
 			boolean verified, boolean active) throws DbException {
-		ContactId c = db.addContact(txn, remote, local, verified, active);
+		ContactId c = db.addContact(txn, remote, local, verified, active,
+				CONTACT);
 		Contact contact = db.getContact(txn, c);
 		for (ContactHook hook : hooks) hook.addingContact(txn, contact);
 		return c;
@@ -77,6 +83,53 @@ class ContactManagerImpl implements ContactManager {
 		return c;
 	}
 
+	@Override
+	public ContactId addPrivateMailbox(Transaction txn, Author remote,
+			AuthorId local,
+			SecretKey master, long timestamp, boolean alice)
+			throws DbException {
+
+		Collection<Contact> privateMailbox =
+				db.getContactsByType(txn, PRIVATE_MAILBOX);
+
+		// We only allow a single private mailbox per user and if
+		// the user tries to add the same repeater twice, addContact
+		// throws an exception
+		for (Contact c : privateMailbox)
+			if (!c.getAuthor().getId().equals(remote.getId()))
+				db.removeContact(txn, c.getId());
+
+
+		ContactId c = db.addContact(txn, remote, local, true, true,
+				PRIVATE_MAILBOX);
+		keyManager.addContact(txn, c, master, timestamp, alice, true);
+		Contact contact = db.getContact(txn, c);
+		for (ContactHook hook : hooks) hook.addingContact(txn, contact);
+		return c;
+	}
+
+	@Override
+	public ContactId addMailboxOwner(Transaction txn, Author remote,
+			AuthorId local, SecretKey master, long timestamp, boolean alice) throws DbException {
+		// We only allow a single owner per mailbox
+		Collection<Contact> mbOwner =
+				db.getContactsByType(txn, MAILBOX_OWNER) ;
+
+		// We only allow a single private mailbox per user and if
+		// the user tries to add the same repeater twice, addContact
+		// throws an exception
+		for (Contact c : mbOwner)
+			if (!c.getAuthor().getId().equals(remote.getId()))
+				db.removeContact(txn, c.getId());
+
+		ContactId c = db.addContact(txn, remote, local, true, true,
+				MAILBOX_OWNER);
+		keyManager.addContact(txn, c, master, timestamp, alice, true);
+		Contact contact = db.getContact(txn, c);
+		for (ContactHook hook : hooks) hook.addingContact(txn, contact);
+		return c;
+	}
+
 	@Override
 	public Contact getContact(ContactId c) throws DbException {
 		Contact contact;
@@ -116,8 +169,7 @@ class ContactManagerImpl implements ContactManager {
 		throw new NoSuchContactException();
 	}
 
-	@Override
-	public Collection<Contact> getActiveContacts() throws DbException {
+	private Collection<Contact> getContacts() throws DbException {
 		Collection<Contact> contacts;
 		Transaction txn = db.startTransaction(true);
 		try {
@@ -126,11 +178,39 @@ class ContactManagerImpl implements ContactManager {
 		} finally {
 			db.endTransaction(txn);
 		}
+		return contacts;
+	}
+
+	@Override
+	public Collection<Contact> getActiveContacts() throws DbException {
+		Collection<Contact> contacts = getContacts();
 		List<Contact> active = new ArrayList<>(contacts.size());
 		for (Contact c : contacts) if (c.isActive()) active.add(c);
 		return active;
 	}
 
+	@Override
+	public Collection<Contact> getVisibleContacts() throws DbException {
+		Collection<Contact> contacts = getContacts();
+		List<Contact> visible = new ArrayList<>(contacts.size());
+		for (Contact c : contacts)
+			if (c.isActive() && !c.isHidden()) visible.add(c);
+		return visible;
+	}
+
+	@Override
+	public Collection<Contact> getContactsByType(int type) throws DbException {
+		Collection<Contact> contacts;
+		Transaction txn = db.startTransaction(true);
+		try {
+			contacts = db.getContactsByType(txn, type);
+			db.commitTransaction(txn);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return contacts;
+	}
+
 	@Override
 	public void removeContact(ContactId c) throws DbException {
 		Transaction txn = db.startTransaction(false);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java
index 2cb610972caac67f87f197aaafd15016fe7472d7..3599ade1c005ea96b1ea5a69b7f3ec6611173445 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java
@@ -2,6 +2,7 @@ package org.briarproject.bramble.contact;
 
 import org.briarproject.bramble.api.contact.ContactExchangeTask;
 import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.contact.MailboxExchangeTask;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -28,4 +29,10 @@ public class ContactModule {
 			ContactExchangeTaskImpl contactExchangeTask) {
 		return contactExchangeTask;
 	}
+
+	@Provides
+	MailboxExchangeTask provideMailboxExchangeTask(
+			MailboxExchangeTaskImpl mailboxExchangeTask) {
+		return mailboxExchangeTask;
+	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/MailboxExchangeTaskImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/MailboxExchangeTaskImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d09cb6e654c04ca35e3dece56556e314fc9260d
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/MailboxExchangeTaskImpl.java
@@ -0,0 +1,332 @@
+package org.briarproject.bramble.contact;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.ContactExchangeListener;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.contact.MailboxExchangeTask;
+import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.db.ContactExistsException;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.bramble.api.plugin.ConnectionManager;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.record.Record;
+import org.briarproject.bramble.api.record.RecordReader;
+import org.briarproject.bramble.api.record.RecordReaderFactory;
+import org.briarproject.bramble.api.record.RecordWriter;
+import org.briarproject.bramble.api.record.RecordWriterFactory;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.StreamReaderFactory;
+import org.briarproject.bramble.api.transport.StreamWriter;
+import org.briarproject.bramble.api.transport.StreamWriterFactory;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.contact.ContactTypes.MAILBOX_OWNER;
+import static org.briarproject.bramble.api.contact.RecordTypes.MAILBOX_INFO;
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
+import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.bramble.util.ValidationUtils.checkLength;
+import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+class MailboxExchangeTaskImpl extends Thread implements MailboxExchangeTask {
+	private static final Logger LOG =
+			Logger.getLogger(MailboxExchangeTaskImpl.class.getName());
+
+	private static final String SIGNING_LABEL_EXCHANGE =
+			"org.briarproject.briar.contact/EXCHANGE";
+
+	private final DatabaseComponent db;
+	private final ClientHelper clientHelper;
+	private final RecordReaderFactory recordReaderFactory;
+	private final RecordWriterFactory recordWriterFactory;
+	private final Clock clock;
+	private final ConnectionManager connectionManager;
+	private final ContactManager contactManager;
+	private final TransportPropertyManager transportPropertyManager;
+	private final CryptoComponent crypto;
+	private final StreamReaderFactory streamReaderFactory;
+	private final StreamWriterFactory streamWriterFactory;
+
+	private volatile ContactExchangeListener listener;
+	private volatile LocalAuthor localAuthor;
+	private volatile DuplexTransportConnection conn;
+	private volatile TransportId transportId;
+	private volatile SecretKey masterSecret;
+	private volatile boolean alice;
+	private volatile int localType, remoteType;
+
+	@Inject
+	MailboxExchangeTaskImpl(DatabaseComponent db, ClientHelper clientHelper,
+			RecordReaderFactory recordReaderFactory,
+			RecordWriterFactory recordWriterFactory, Clock clock,
+			ConnectionManager connectionManager, ContactManager contactManager,
+			TransportPropertyManager transportPropertyManager,
+			CryptoComponent crypto, StreamReaderFactory streamReaderFactory,
+			StreamWriterFactory streamWriterFactory) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+		this.recordReaderFactory = recordReaderFactory;
+		this.recordWriterFactory = recordWriterFactory;
+		this.clock = clock;
+		this.connectionManager = connectionManager;
+		this.contactManager = contactManager;
+		this.transportPropertyManager = transportPropertyManager;
+		this.crypto = crypto;
+		this.streamReaderFactory = streamReaderFactory;
+		this.streamWriterFactory = streamWriterFactory;
+	}
+
+	@Override
+	public void startExchange(ContactExchangeListener listener,
+			LocalAuthor localAuthor, SecretKey masterSecret,
+			DuplexTransportConnection conn, TransportId transportId,
+			boolean alice, int localType, int remoteType) {
+		this.listener = listener;
+		this.localAuthor = localAuthor;
+		this.conn = conn;
+		this.transportId = transportId;
+		this.masterSecret = masterSecret;
+		this.alice = alice;
+		this.localType = localType;
+		this.remoteType = remoteType;
+		start();
+
+	}
+
+	@Override
+	public void run() {
+		// Get the transport connection's input and output streams
+		InputStream in;
+		OutputStream out;
+		try {
+			in = conn.getReader().getInputStream();
+			out = conn.getWriter().getOutputStream();
+		} catch (IOException e) {
+			logException(LOG, WARNING, e);
+			listener.contactExchangeFailed();
+			tryToClose(conn);
+			return;
+		}
+
+		// Get the local transport properties
+		Map<TransportId, TransportProperties> localProperties;
+		try {
+			localProperties = transportPropertyManager.getLocalProperties();
+		} catch (DbException e) {
+			logException(LOG, WARNING, e);
+			listener.contactExchangeFailed();
+			tryToClose(conn);
+			return;
+		}
+
+		// Derive the header keys for the transport streams
+		SecretKey aliceHeaderKey = crypto.deriveKey(ALICE_KEY_LABEL,
+				masterSecret, new byte[] {PROTOCOL_VERSION});
+		SecretKey bobHeaderKey = crypto.deriveKey(BOB_KEY_LABEL, masterSecret,
+				new byte[] {PROTOCOL_VERSION});
+
+		// Create the readers
+		InputStream streamReader =
+				streamReaderFactory.createContactExchangeStreamReader(in,
+						alice ? bobHeaderKey : aliceHeaderKey);
+		RecordReader recordReader =
+				recordReaderFactory.createRecordReader(streamReader);
+
+		// Create the writers
+		StreamWriter streamWriter =
+				streamWriterFactory.createContactExchangeStreamWriter(out,
+						alice ? aliceHeaderKey : bobHeaderKey);
+		RecordWriter recordWriter =
+				recordWriterFactory
+						.createRecordWriter(streamWriter.getOutputStream());
+
+		// Derive the nonces to be signed
+		byte[] aliceNonce = crypto.mac(ALICE_NONCE_LABEL, masterSecret,
+				new byte[] {PROTOCOL_VERSION});
+		byte[] bobNonce = crypto.mac(BOB_NONCE_LABEL, masterSecret,
+				new byte[] {PROTOCOL_VERSION});
+		byte[] localNonce = alice ? aliceNonce : bobNonce;
+		byte[] remoteNonce = alice ? bobNonce : aliceNonce;
+
+		// Sign the nonce
+		byte[] localSignature = sign(localAuthor, localNonce);
+
+		// Exchange contact info
+		long localTimestamp = clock.currentTimeMillis();
+		ContactInfo remoteInfo;
+		try {
+			if (alice) {
+				sendContactInfo(recordWriter, localAuthor, localProperties,
+						localSignature, localTimestamp);
+				recordWriter.flush();
+				remoteInfo = receiveContactInfo(recordReader);
+			} else {
+				remoteInfo = receiveContactInfo(recordReader);
+				sendContactInfo(recordWriter, localAuthor, localProperties,
+						localSignature, localTimestamp);
+				recordWriter.flush();
+			}
+			// Send EOF on the outgoing stream
+			streamWriter.sendEndOfStream();
+			// Skip any remaining records from the incoming stream
+			try {
+				while (true) recordReader.readRecord();
+			} catch (EOFException expected) {
+				LOG.info("End of stream");
+			}
+		} catch (IOException e) {
+			logException(LOG, WARNING, e);
+			listener.contactExchangeFailed();
+			tryToClose(conn);
+			return;
+		}
+
+		// Verify the contact's signature
+		if (!verify(remoteInfo.author, remoteNonce, remoteInfo.signature)) {
+			LOG.warning("Invalid signature");
+			listener.contactExchangeFailed();
+			tryToClose(conn);
+			return;
+		}
+
+		// The agreed timestamp is the minimum of the peers' timestamps
+		long timestamp = Math.min(localTimestamp, remoteInfo.timestamp);
+
+		try {
+			ContactId contactId;
+			// Add the Contact/Repeater
+			contactId = addContact(remoteInfo.author, timestamp,
+					remoteInfo.properties);
+
+			// Reuse the connection as a transport connection
+			connectionManager.manageOutgoingConnection(contactId, transportId,
+					conn);
+			// Pseudonym exchange succeeded
+			LOG.info("Pseudonym exchange succeeded");
+			listener.contactExchangeSucceeded(remoteInfo.author);
+		} catch (ContactExistsException e) {
+			logException(LOG, WARNING, e);
+			tryToClose(conn);
+			listener.duplicateContact(remoteInfo.author);
+		} catch (DbException e) {
+			logException(LOG, WARNING, e);
+			tryToClose(conn);
+			listener.contactExchangeFailed();
+		}
+	}
+
+	private byte[] sign(LocalAuthor author, byte[] nonce) {
+		try {
+			return crypto.sign(SIGNING_LABEL_EXCHANGE, nonce,
+					author.getPrivateKey());
+		} catch (GeneralSecurityException e) {
+			throw new AssertionError();
+		}
+	}
+
+	private boolean verify(Author author, byte[] nonce, byte[] signature) {
+		try {
+			return crypto.verifySignature(signature, SIGNING_LABEL_EXCHANGE,
+					nonce, author.getPublicKey());
+		} catch (GeneralSecurityException e) {
+			return false;
+		}
+	}
+
+	private void sendContactInfo(RecordWriter recordWriter, Author author,
+			Map<TransportId, TransportProperties> properties, byte[] signature,
+			long timestamp) throws IOException {
+		BdfList authorList = clientHelper.toList(author);
+		BdfDictionary props = clientHelper.toDictionary(properties);
+		BdfList payload = BdfList.of(authorList, props, signature, timestamp, localType);
+		recordWriter.writeRecord(new Record(PROTOCOL_VERSION, MAILBOX_INFO,
+				clientHelper.toByteArray(payload)));
+		LOG.info("Sent repeater info");
+	}
+
+	private ContactInfo receiveContactInfo(RecordReader recordReader)
+			throws IOException {
+		Record record;
+		do {
+			record = recordReader.readRecord();
+			if (record.getProtocolVersion() != PROTOCOL_VERSION)
+				throw new FormatException();
+		} while (record.getRecordType() != MAILBOX_INFO);
+		LOG.info("Received contact info");
+		BdfList payload = clientHelper.toList(record.getPayload());
+		checkSize(payload, 5);
+		Author author = clientHelper.parseAndValidateAuthor(payload.getList(0));
+		BdfDictionary props = payload.getDictionary(1);
+		Map<TransportId, TransportProperties> properties =
+				clientHelper.parseAndValidateTransportPropertiesMap(props);
+		byte[] signature = payload.getRaw(2);
+		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
+		long timestamp = payload.getLong(3);
+		if (timestamp < 0) throw new FormatException();
+		int type = payload.getLong(4).intValue();
+		if(type != remoteType)
+			throw new AssertionError();
+		return new ContactInfo(author, properties, signature, timestamp, type);
+	}
+
+	private ContactId addContact(Author remoteAuthor, long timestamp,
+			Map<TransportId, TransportProperties> remoteProperties)
+			throws DbException {
+		ContactId contactId;
+		Transaction txn = db.startTransaction(false);
+		try {
+			if (remoteType == MAILBOX_OWNER) {
+				contactId = contactManager.addMailboxOwner(txn, remoteAuthor,
+						localAuthor.getId(), masterSecret, timestamp, alice);
+				//TODO: Do we need transport properties here? (Repeater will not contact the Owner)
+			} else {
+				contactId = contactManager.addPrivateMailbox(txn, remoteAuthor,
+						localAuthor.getId(), masterSecret, timestamp, alice);
+				transportPropertyManager.addRemoteProperties(txn, contactId,
+						remoteProperties);
+			}
+
+			db.commitTransaction(txn);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return contactId;
+	}
+
+	private void tryToClose(DuplexTransportConnection conn) {
+		try {
+			LOG.info("Closing connection");
+			conn.getReader().dispose(true, true);
+			conn.getWriter().dispose(true);
+		} catch (IOException e) {
+			logException(LOG, WARNING, e);
+		}
+	}
+
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
index 66e5cfcf6d72ec90eed0dffa4bbf56c51d38b071..d895d29a4390ea0806b8093fffd8df0be4a8d07e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
@@ -78,7 +78,7 @@ interface Database<T> {
 	 * and returns an ID for the contact.
 	 */
 	ContactId addContact(T txn, Author remote, AuthorId local, boolean verified,
-			boolean active) throws DbException;
+			boolean active, int ype) throws DbException;
 
 	/**
 	 * Stores a group.
@@ -227,6 +227,14 @@ interface Database<T> {
 	Collection<Contact> getContactsByAuthorId(T txn, AuthorId remote)
 			throws DbException;
 
+	/**
+	 * Returns a possibly empty collection of contacts of the given type.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<Contact> getContactsByType(T txn, int type)
+			throws DbException;
+
 	/**
 	 * Returns all contacts associated with the given local pseudonym.
 	 * <p/>
@@ -641,7 +649,7 @@ interface Database<T> {
 	 * Marks the given transport keys as usable for outgoing streams.
 	 */
 	void setTransportKeysActive(T txn, TransportId t, KeySetId k)
-		throws DbException;
+			throws DbException;
 
 	/**
 	 * Updates the transmission count and expiry time of the given message
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
index f2e4de1c6fff7cbd3fbc6e92f52dead50c34b458..85d0304dabb51c0c6805c7d1929205dc6d5627fa 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
@@ -172,7 +172,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 
 	@Override
 	public ContactId addContact(Transaction transaction, Author remote,
-			AuthorId local, boolean verified, boolean active)
+			AuthorId local, boolean verified, boolean active, int type)
 			throws DbException {
 		if (transaction.isReadOnly()) throw new IllegalArgumentException();
 		T txn = unbox(transaction);
@@ -182,7 +182,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			throw new ContactExistsException();
 		if (db.containsContact(txn, remote.getId(), local))
 			throw new ContactExistsException();
-		ContactId c = db.addContact(txn, remote, local, verified, active);
+		ContactId c = db.addContact(txn, remote, local, verified, active, type);
 		transaction.attach(new ContactAddedEvent(c, active));
 		if (active) transaction.attach(new ContactStatusChangedEvent(c, true));
 		return c;
@@ -397,6 +397,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return db.getContactsByAuthorId(txn, remote);
 	}
 
+	@Override
+	public Collection<Contact> getContactsByType(Transaction transaction, int type) throws DbException{
+		T txn = unbox(transaction);
+		return db.getContactsByType(txn, type);
+	}
+
 	@Override
 	public Collection<ContactId> getContacts(Transaction transaction,
 			AuthorId a) throws DbException {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index baa67a29ac6397c6c7e26b17a18feb455924c111..45bba640147292976cc416122031a72bc8c2408c 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -1,6 +1,7 @@
 package org.briarproject.bramble.db;
 
 import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactFactory;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.db.DataTooNewException;
@@ -76,7 +77,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
 abstract class JdbcDatabase implements Database<Connection> {
 
 	// Package access for testing
-	static final int CODE_SCHEMA_VERSION = 39;
+	static final int CODE_SCHEMA_VERSION = 40;
 
 	// Rotation period offsets for incoming transport keys
 	private static final int OFFSET_PREV = -1;
@@ -110,6 +111,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " localAuthorId _HASH NOT NULL,"
 					+ " verified BOOLEAN NOT NULL,"
 					+ " active BOOLEAN NOT NULL,"
+					+ " type INT NOT NULL,"
 					+ " PRIMARY KEY (contactId),"
 					+ " FOREIGN KEY (localAuthorId)"
 					+ " REFERENCES localAuthors (authorId)"
@@ -571,7 +573,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	@Override
 	public ContactId addContact(Connection txn, Author remote, AuthorId local,
-			boolean verified, boolean active) throws DbException {
+			boolean verified, boolean active, int type) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -579,8 +581,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			String sql = "INSERT INTO contacts"
 					+ " (authorId, formatVersion, name, publicKey,"
 					+ " localAuthorId,"
-					+ " verified, active)"
-					+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
+					+ " verified, active, type)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, remote.getId().getBytes());
 			ps.setInt(2, remote.getFormatVersion());
@@ -589,6 +591,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.setBytes(5, local.getBytes());
 			ps.setBoolean(6, verified);
 			ps.setBoolean(7, active);
+			ps.setInt(8, type);
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
@@ -1194,7 +1197,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT authorId, formatVersion, name, publicKey,"
-					+ " localAuthorId, verified, active"
+					+ " localAuthorId, verified, active, type"
 					+ " FROM contacts"
 					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
@@ -1208,11 +1211,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 			AuthorId localAuthorId = new AuthorId(rs.getBytes(5));
 			boolean verified = rs.getBoolean(6);
 			boolean active = rs.getBoolean(7);
+			int type = rs.getInt(8);
 			rs.close();
 			ps.close();
 			Author author =
 					new Author(authorId, formatVersion, name, publicKey);
-			return new Contact(c, author, localAuthorId, verified, active);
+			return ContactFactory
+					.createContact(c, author, localAuthorId, verified, active,
+							type);
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1227,7 +1233,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT contactId, authorId, formatVersion, name,"
-					+ " publicKey, localAuthorId, verified, active"
+					+ " publicKey, localAuthorId, verified, active, type"
 					+ " FROM contacts";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
@@ -1243,8 +1249,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 				AuthorId localAuthorId = new AuthorId(rs.getBytes(6));
 				boolean verified = rs.getBoolean(7);
 				boolean active = rs.getBoolean(8);
-				contacts.add(new Contact(contactId, author, localAuthorId,
-						verified, active));
+				int type = rs.getInt(9);
+				contacts.add(ContactFactory
+						.createContact(contactId, author, localAuthorId,
+								verified, active, type));
 			}
 			rs.close();
 			ps.close();
@@ -1286,7 +1294,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT contactId, formatVersion, name, publicKey,"
-					+ " localAuthorId, verified, active"
+					+ " localAuthorId, verified, active, type"
 					+ " FROM contacts"
 					+ " WHERE authorId = ?";
 			ps = txn.prepareStatement(sql);
@@ -1301,10 +1309,51 @@ abstract class JdbcDatabase implements Database<Connection> {
 				AuthorId localAuthorId = new AuthorId(rs.getBytes(5));
 				boolean verified = rs.getBoolean(6);
 				boolean active = rs.getBoolean(7);
+				int type = rs.getInt(8);
 				Author author =
 						new Author(remote, formatVersion, name, publicKey);
-				contacts.add(new Contact(c, author, localAuthorId, verified,
-						active));
+				contacts.add(ContactFactory
+						.createContact(c, author, localAuthorId, verified,
+								active, type));
+			}
+			rs.close();
+			ps.close();
+			return contacts;
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public Collection<Contact> getContactsByType(Connection txn,
+			int type) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT contactId, authorId, formatVersion, name,"
+					+ " publicKey, localAuthorId, verified, active"
+					+ " FROM contacts"
+					+ " WHERE type = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, type);
+			rs = ps.executeQuery();
+			List<Contact> contacts = new ArrayList<>();
+			while (rs.next()) {
+				ContactId contactId = new ContactId(rs.getInt(1));
+				AuthorId authorId = new AuthorId(rs.getBytes(2));
+				int formatVersion = rs.getInt(3);
+				String name = rs.getString(4);
+				byte[] publicKey = rs.getBytes(5);
+				Author author =
+						new Author(authorId, formatVersion, name, publicKey);
+				AuthorId localAuthorId = new AuthorId(rs.getBytes(6));
+				boolean verified = rs.getBoolean(7);
+				boolean active = rs.getBoolean(8);
+				contacts.add(ContactFactory
+						.createContact(contactId, author, localAuthorId,
+								verified, active, type));
 			}
 			rs.close();
 			ps.close();
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java
index a90d685b185bfb565f34a9d78a275a0953ae0330..2f1e17300ccd2b733c2e3038097ec8ec032737eb 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java
@@ -3,6 +3,7 @@ package org.briarproject.bramble.contact;
 import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.contact.PrivateMailbox;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.NoSuchContactException;
@@ -20,6 +21,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Random;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.test.TestUtils.getSecretKey;
@@ -53,7 +55,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase {
 		context.checking(new Expectations() {{
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn));
-			oneOf(db).addContact(txn, remote, local, verified, active);
+			oneOf(db).addContact(txn, remote, local, verified, active, CONTACT);
 			will(returnValue(contactId));
 			oneOf(keyManager).addContact(txn, contactId, master, timestamp,
 					alice, active);
@@ -127,6 +129,24 @@ public class ContactManagerImplTest extends BrambleMockTestCase {
 		contactManager.getContact(remote.getId(), new AuthorId(getRandomId()));
 	}
 
+	@Test
+	public void testGetContactByType() throws Exception {
+		Transaction txn = new Transaction(null, true);
+		Collection<Contact> regularContacts = Collections.singletonList(contact);
+		Collection<Contact> contacts = new ArrayList<>(regularContacts);
+		contacts.add(new PrivateMailbox(new ContactId(3), remote, local, true, true));
+		context.checking(new Expectations() {{
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getContactsByType(txn, CONTACT);
+			will(returnValue(regularContacts));
+			oneOf(db).commitTransaction(txn);
+			oneOf(db).endTransaction(txn);
+		}});
+
+		assertEquals(regularContacts, contactManager.getContactsByType(CONTACT));
+	}
+
 	@Test
 	public void testActiveContacts() throws Exception {
 		Collection<Contact> activeContacts = Collections.singletonList(contact);
@@ -145,6 +165,24 @@ public class ContactManagerImplTest extends BrambleMockTestCase {
 		assertEquals(activeContacts, contactManager.getActiveContacts());
 	}
 
+	@Test
+	public void testVisibleContacts() throws Exception {
+		Collection<Contact> visibleContacts = Collections.singletonList(contact);
+		Collection<Contact> contacts = new ArrayList<>(visibleContacts);
+		contacts.add(new PrivateMailbox(new ContactId(3), remote, local, true, true));
+		Transaction txn = new Transaction(null, true);
+		context.checking(new Expectations() {{
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getContacts(txn);
+			will(returnValue(contacts));
+			oneOf(db).commitTransaction(txn);
+			oneOf(db).endTransaction(txn);
+		}});
+
+		assertEquals(visibleContacts, contactManager.getVisibleContacts());
+	}
+
 	@Test
 	public void testRemoveContact() throws Exception {
 		Transaction txn = new Transaction(null, false);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
index e16e03da0dc51086f73c9c6ed45ee81483f8a3b1..fe42e5b44efb53602668ceaf9926a9c63a2e172e 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
@@ -61,6 +61,7 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
@@ -162,7 +163,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 					localAuthor.getId());
 			will(returnValue(false));
 			oneOf(database).addContact(txn, author, localAuthor.getId(),
-					true, true);
+					true, true, CONTACT);
 			will(returnValue(contactId));
 			oneOf(eventBus).broadcast(with(any(ContactAddedEvent.class)));
 			oneOf(eventBus).broadcast(with(any(
@@ -213,7 +214,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		try {
 			db.addLocalAuthor(transaction, localAuthor);
 			assertEquals(contactId, db.addContact(transaction, author,
-					localAuthor.getId(), true, true));
+					localAuthor.getId(), true, true, CONTACT));
 			assertEquals(singletonList(contact),
 					db.getContacts(transaction));
 			db.addGroup(transaction, group); // First time - listeners called
@@ -479,7 +480,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		Transaction transaction = db.startTransaction(false);
 		try {
-			db.addContact(transaction, author, localAuthor.getId(), true, true);
+			db.addContact(transaction, author, localAuthor.getId(), true, true,
+					CONTACT);
 			fail();
 		} catch (NoSuchLocalAuthorException expected) {
 			// Expected
@@ -755,7 +757,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 					localAuthor.getId());
 			will(returnValue(false));
 			oneOf(database).addContact(txn, author, localAuthor.getId(),
-					true, true);
+					true, true, CONTACT);
 			will(returnValue(contactId));
 			oneOf(eventBus).broadcast(with(any(ContactAddedEvent.class)));
 			oneOf(eventBus).broadcast(with(any(
@@ -776,7 +778,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		try {
 			db.addLocalAuthor(transaction, localAuthor);
 			assertEquals(contactId, db.addContact(transaction, author,
-					localAuthor.getId(), true, true));
+					localAuthor.getId(), true, true, CONTACT));
 			db.commitTransaction(transaction);
 		} finally {
 			db.endTransaction(transaction);
@@ -1556,7 +1558,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		Transaction transaction = db.startTransaction(false);
 		try {
-			db.addContact(transaction, author, localAuthor.getId(), true, true);
+			db.addContact(transaction, author, localAuthor.getId(), true, true,
+					CONTACT);
 			fail();
 		} catch (ContactExistsException expected) {
 			// Expected
@@ -1586,7 +1589,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		Transaction transaction = db.startTransaction(false);
 		try {
-			db.addContact(transaction, author, localAuthor.getId(), true, true);
+			db.addContact(transaction, author, localAuthor.getId(), true, true,
+					CONTACT);
 			fail();
 		} catch (ContactExistsException expected) {
 			// Expected
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
index e46a75b68460ba81166b20aabbfcddcbca1fe4b8..8e1348df4719e0382f1b1f51d9d7c3a620c6c69f 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
@@ -32,6 +32,7 @@ import java.util.Random;
 import java.util.logging.Logger;
 
 import static java.util.logging.Level.OFF;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
@@ -546,7 +547,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 		db.addLocalAuthor(txn, localAuthor);
 		for (int i = 0; i < CONTACTS; i++) {
 			ContactId c = db.addContact(txn, getAuthor(), localAuthor.getId(),
-					random.nextBoolean(), true);
+					random.nextBoolean(), true, CONTACT);
 			contacts.add(db.getContact(txn, c));
 			contactGroups.put(c, new ArrayList<>());
 			for (int j = 0; j < GROUPS_PER_CONTACT; j++) {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
index 2655e9ef7f571cdbcc80abb2a492e3b0e7c5bdcd..25416de5acdd85de56da67ecb731d2b20492c153 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -48,6 +48,8 @@ import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT_MAILBOX;
 import static org.briarproject.bramble.api.db.Metadata.REMOVE;
 import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@@ -130,7 +132,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		assertFalse(db.containsContact(txn, contactId));
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		assertTrue(db.containsContact(txn, contactId));
 		assertFalse(db.containsGroup(txn, groupId));
 		db.addGroup(txn, group);
@@ -193,7 +195,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true, null);
@@ -224,7 +226,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a shared group and a shared but unvalidated message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, UNKNOWN, true, null);
@@ -269,7 +271,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, an invisible group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addMessage(txn, message, DELIVERED, true, null);
 
@@ -320,7 +322,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a shared group and an unshared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, false, null);
@@ -351,7 +353,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true, null);
@@ -377,7 +379,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact and a visible group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, false);
 
@@ -418,7 +420,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true, null);
@@ -575,7 +577,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact and a shared group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 
@@ -595,7 +597,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 
 		// The group is not in the database
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
@@ -613,7 +615,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, an invisible group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addMessage(txn, message, DELIVERED, true, null);
 
@@ -632,7 +634,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact and a group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 
 		// The group should not be visible to the contact
@@ -684,7 +686,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add the contact, the transport and the transport keys
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, active));
+				true, active, CONTACT));
 		db.addTransport(txn, transportId, 123);
 		assertEquals(keySetId, db.addTransportKeys(txn, contactId, keys));
 		assertEquals(keySetId1, db.addTransportKeys(txn, contactId, keys1));
@@ -776,7 +778,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add the contact, transport and transport keys
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addTransport(txn, transportId, 123);
 		assertEquals(keySetId, db.addTransportKeys(txn, contactId, keys));
 
@@ -819,7 +821,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add the contact, transport and transport keys
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, active));
+				true, active, CONTACT));
 		db.addTransport(txn, transportId, 123);
 		assertEquals(keySetId, db.addTransportKeys(txn, contactId, keys));
 
@@ -860,7 +862,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a contact associated with the local author
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 
 		// Ensure contact is returned from database by Author ID
 		Collection<Contact> contacts =
@@ -890,7 +892,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a contact associated with the local author
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		contacts = db.getContacts(txn, localAuthor.getId());
 		assertEquals(singletonList(contactId), contacts);
 
@@ -912,7 +914,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact - initially there should be no offered messages
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		assertEquals(0, db.countOfferedMessages(txn, contactId));
 
 		// Add some offered messages and count them
@@ -933,6 +935,40 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testGetContactsByType() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		ContactId contactId1 = new ContactId(2);
+		Author author1 = getAuthor();
+
+		// Add a local author - no contacts should be associated
+		db.addLocalAuthor(txn, localAuthor);
+
+		// Add two contact associated with the local author
+		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
+				true, true, CONTACT));
+		assertEquals(contactId1,
+				db.addContact(txn, author1, localAuthor.getId(),
+						true, true, CONTACT_MAILBOX));
+
+		// Ensure contact is returned from database by type
+		Collection<Contact> contacts =
+				db.getContactsByType(txn, CONTACT);
+		assertEquals(1, contacts.size());
+		assertEquals(contactId, contacts.iterator().next().getId());
+		assertEquals(CONTACT, contacts.iterator().next().getType());
+
+		// Ensure no contacts are returned after contact was deleted
+		db.removeContact(txn, contactId);
+		contacts = db.getContactsByType(txn, CONTACT);
+		assertEquals(0, contacts.size());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testGroupMetadata() throws Exception {
 		Database<Connection> db = open(false);
@@ -1504,7 +1540,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true, null);
@@ -1615,9 +1651,11 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add the same contact for each local author
 		ContactId contactId =
-				db.addContact(txn, author, localAuthor.getId(), true, true);
+				db.addContact(txn, author, localAuthor.getId(), true, true,
+						CONTACT);
 		ContactId contactId1 =
-				db.addContact(txn, author, localAuthor1.getId(), true, true);
+				db.addContact(txn, author, localAuthor1.getId(), true, true,
+						CONTACT);
 
 		// The contacts should be distinct
 		assertNotEquals(contactId, contactId1);
@@ -1637,7 +1675,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true, null);
@@ -1682,7 +1720,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 
 		// The contact should be active
 		Contact contact = db.getContact(txn, contactId);
@@ -1737,7 +1775,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a contact, a group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
-				true, true));
+				true, true, CONTACT));
 		db.addGroup(txn, group);
 		db.addMessage(txn, message, UNKNOWN, false, null);
 
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
index 2dc00d67f63d6cd1e96507df45627f3de7a63d00..04fa53f9f3375a1daff46d58a728782d5868b588 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
@@ -198,7 +198,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 			try {
 				long start = now();
 				List<ContactListItem> contacts = new ArrayList<>();
-				for (Contact c : contactManager.getActiveContacts()) {
+				for (Contact c : contactManager.getVisibleContacts()) {
 					try {
 						ContactId id = c.getId();
 						GroupCount count =
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contactselection/ContactSelectorControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/contactselection/ContactSelectorControllerImpl.java
index 9f6380d0faeb96b9c4313fd42a1645ca2d757705..2c33ee63d99498f92921a973265be7f32e45ea2a 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contactselection/ContactSelectorControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contactselection/ContactSelectorControllerImpl.java
@@ -44,7 +44,7 @@ public abstract class ContactSelectorControllerImpl
 		runOnDbThread(() -> {
 			try {
 				Collection<SelectableContactItem> contacts = new ArrayList<>();
-				for (Contact c : contactManager.getActiveContacts()) {
+				for (Contact c : contactManager.getVisibleContacts()) {
 					// was this contact already selected?
 					boolean selected = selection.contains(c.getId());
 					// can this contact be selected?
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java
index e053c50c1d65f744bd27773472dd5be8ededc7e8..5117cb09b811ebf7c360ae4dc55a6a45555c9fde 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java
@@ -24,6 +24,7 @@ import javax.inject.Inject;
 
 import static android.widget.Toast.LENGTH_LONG;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.util.LogUtils.logException;
 
 @MethodsNotNullByDefault
@@ -51,7 +52,7 @@ public class ContactExchangeActivity extends KeyAgreementActivity implements
 		getSupportActionBar().setTitle(string.add_contact_title);
 	}
 
-	private void startContactExchange(KeyAgreementResult result) {
+	protected void startContactExchange(KeyAgreementResult result) {
 		runOnDbThread(() -> {
 			LocalAuthor localAuthor;
 			// Load the local pseudonym
@@ -67,7 +68,7 @@ public class ContactExchangeActivity extends KeyAgreementActivity implements
 			contactExchangeTask.startExchange(ContactExchangeActivity.this,
 					localAuthor, result.getMasterKey(),
 					result.getConnection(), result.getTransportId(),
-					result.wasAlice());
+					result.wasAlice(), CONTACT, CONTACT);
 		});
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
index 6a05b9a1542b00d463ddad795f864b97079a2d7e..bc87cea69e290b1e351d55190a2843896baa34c9 100644
--- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
@@ -49,6 +49,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT_MAILBOX;
 import static org.briarproject.briar.api.blog.BlogConstants.KEY_AUTHOR;
 import static org.briarproject.briar.api.blog.BlogConstants.KEY_COMMENT;
 import static org.briarproject.briar.api.blog.BlogConstants.KEY_ORIGINAL_MSG_ID;
@@ -99,6 +101,12 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		Blog b = blogFactory.createBlog(c.getAuthor());
 		// TODO we might want to reconsider removing b, if otherwise shared
 		if (db.containsGroup(txn, b.getId())) removeBlog(txn, b);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
index 651bd8a97336a8d5d352ab24b831eae2c5bfff2f..2928ed24866d6d60ff282f4afe6ace968206aa4f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
@@ -40,6 +40,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.util.LogUtils.logException;
 import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
 import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_AUTH;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
index 64fc1b40d48ef99feca8a52c6869a1d2fcd105e2..90a2e6ee11ce93fc1bff95e002de73c35bffca43 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
@@ -47,6 +47,8 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT_MAILBOX;
 import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
 import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 import static org.briarproject.briar.introduction.IntroducerState.START;
@@ -119,6 +121,12 @@ class IntroductionManagerImpl extends ConversationClientImpl
 
 	@Override
 	public void addingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		// Create a group to share with the contact
 		Group g = getContactGroup(c);
 		db.addGroup(txn, g);
@@ -138,6 +146,12 @@ class IntroductionManagerImpl extends ConversationClientImpl
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		removeSessionWithIntroducer(txn, c);
 		abortOrRemoveSessionWithIntroducee(txn, c);
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
index a4aef1eca325f79485c255c311510cd9d58219d4..cec30893369cd4d7b378065279c6ae639e07b9f3 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -48,6 +48,8 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT_MAILBOX;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.briar.privategroup.invitation.CreatorState.START;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
@@ -113,6 +115,12 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 
 	@Override
 	public void addingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		// Create a group to share with the contact
 		Group g = getContactGroup(c);
 		db.addGroup(txn, g);
@@ -137,6 +145,12 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		// Remove the contact group (all messages will be removed with it)
 		db.removeGroup(txn, getContactGroup(c));
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
index 5c77c7be942df58dd618bfc5fd70d055699d4bc9..5af30f69eec1e7340085675a1b650a3be632450a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
@@ -23,6 +23,9 @@ import org.briarproject.briar.api.client.MessageTracker;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT_MAILBOX;
+
 @Immutable
 @NotNullByDefault
 class BlogSharingManagerImpl extends SharingManagerImpl<Blog>
@@ -70,6 +73,12 @@ class BlogSharingManagerImpl extends SharingManagerImpl<Blog>
 
 	@Override
 	public void addingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		// Create a group to share with the contact
 		super.addingContact(txn, c);
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
index fc57a6daace13c60480ea5e1c4e2518b4920ad1f..2e7de3035069b677eb92dc146d18c3236caf159e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
@@ -43,6 +43,8 @@ import java.util.Map.Entry;
 
 import javax.annotation.Nullable;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT_MAILBOX;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.briar.sharing.MessageType.ABORT;
 import static org.briarproject.briar.sharing.MessageType.ACCEPT;
@@ -104,6 +106,12 @@ abstract class SharingManagerImpl<S extends Shareable>
 
 	@Override
 	public void addingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		// Create a group to share with the contact
 		Group g = getContactGroup(c);
 		db.addGroup(txn, g);
@@ -122,6 +130,12 @@ abstract class SharingManagerImpl<S extends Shareable>
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				return;
+			case CONTACT_MAILBOX:
+				return;
+		}
 		// Remove the contact group (all messages will be removed with it)
 		db.removeGroup(txn, getContactGroup(c));
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java b/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java
index 811f3a351008aaca919437fc2519d3d47087ba57..ca500bb60b1d9d39f527cf4a671955d36f9939ef 100644
--- a/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java
@@ -53,6 +53,7 @@ import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
index b3161af64dce727a64b1fafa8dba50879ce68094..9cecc0cc6115d85a2736a8198884586fec083bc9 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
@@ -41,6 +41,7 @@ import java.util.Collection;
 import java.util.Map;
 import java.util.concurrent.TimeoutException;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java
index 1007afe5e08866139119b5d3b3d129d5c1d8d434..f65ecfed48a608108b55d8670b3fcb215ccd0e9e 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java
@@ -41,6 +41,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.InputStream;
 
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
 import static org.briarproject.bramble.test.TestPluginConfigModule.MAX_LATENCY;
 import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID;
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
index 481a574869a56d0f455a7f6673b7bf1f56fb5f32..557e853dcb62557a395ad659ada6be05bfc6b352 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
@@ -62,6 +62,7 @@ import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import static junit.framework.Assert.assertNotNull;
+import static org.briarproject.bramble.api.contact.ContactTypes.CONTACT;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;