From 54e812a7c8c08d8a6d3fb7c46c3f8bf3aeb76d18 Mon Sep 17 00:00:00 2001
From: Julian Dehm <goapunk@riseup.net>
Date: Tue, 25 Sep 2018 20:03:25 +0200
Subject: [PATCH] first test

---
 .../properties/TransportPropertyManager.java  |   4 +
 .../TransportPropertyManagerImpl.java         |  16 +-
 .../MailboxIntroductionSucceededEvent.java    |  22 +
 .../mailbox/AbstractIntroduceeSession.java    |   4 +-
 .../briar/mailbox/AbstractProtocolEngine.java |  48 +-
 .../mailbox/IntroduceeAcceptMessage.java      |  52 ++
 .../mailbox/IntroduceeProtocolEngine.java     |  86 ++-
 .../mailbox/IntroduceeRequestMessage.java     |   9 +-
 .../briar/mailbox/IntroduceeSession.java      |  41 +-
 .../briar/mailbox/MailboxAcceptMessage.java   |  41 ++
 .../briar/mailbox/MailboxAuthMessage.java     |  15 +-
 .../mailbox/MailboxIntroductionCrypto.java    |  14 +-
 .../MailboxIntroductionCryptoImpl.java        |  22 +-
 .../MailboxIntroductionManagerImpl.java       | 108 +++-
 .../mailbox/MailboxIntroductionValidator.java |  95 +++-
 .../briar/mailbox/MailboxMessageEncoder.java  |  20 +-
 .../mailbox/MailboxMessageEncoderImpl.java    |  44 +-
 .../briar/mailbox/MailboxMessageParser.java   |   8 +-
 .../mailbox/MailboxMessageParserImpl.java     |  54 +-
 .../briar/mailbox/MailboxProtocolEngine.java  | 102 +++-
 .../briar/mailbox/MailboxSession.java         |  63 +--
 .../briar/mailbox/MailboxSessionParser.java   |   3 +
 .../mailbox/MailboxSessionParserImpl.java     |  22 +-
 .../briar/mailbox/MailboxState.java           |   2 +-
 .../briar/mailbox/OwnerProtocolEngine.java    |  63 ++-
 .../briar/mailbox/OwnerSession.java           |   5 +-
 .../briar/mailbox/ProtocolEngine.java         |   4 +-
 .../briarproject/briar/mailbox/Session.java   |   4 -
 .../briar/forum/ForumManagerTest.java         |   4 +
 .../MailboxIntroductionIntegrationTest.java   |  29 +-
 mailbox-android/build.gradle                  |   1 +
 .../mailbox/android/AndroidComponent.java     |   5 +-
 .../android/MailboxApplicationImpl.java       |   2 +
 mailbox-core/build.gradle                     |  39 ++
 .../briar/MailboxCoreEagerSingletons.java     |   9 +
 .../briarproject/briar/MailboxCoreModule.java |  15 +
 .../briar/mailbox/AbortMessage.java           |  27 +
 .../mailbox/AbstractIntroduceeSession.java    | 157 ++++++
 .../AbstractMailboxIntroductionMessage.java   |  45 ++
 .../briar/mailbox/AbstractProtocolEngine.java | 207 ++++++++
 .../briar/mailbox/BdfIncomingMessageHook.java |  66 +++
 .../briar/mailbox/ContactAcceptMessage.java   |  52 ++
 .../briar/mailbox/ContactRequestMessage.java  |  42 ++
 .../briar/mailbox/DeclineMessage.java         |  28 +
 .../briar/mailbox/Introducee.java             |  77 +++
 .../mailbox/IntroduceeAcceptMessage.java      |  52 ++
 .../mailbox/IntroduceeProtocolEngine.java     | 274 ++++++++++
 .../mailbox/IntroduceeRequestMessage.java     |  48 ++
 .../briar/mailbox/IntroduceeSession.java      | 151 ++++++
 .../briar/mailbox/IntroduceeState.java        |  36 ++
 .../briar/mailbox/IntroductionConstants.java  |  49 ++
 .../briar/mailbox/MailboxAcceptMessage.java   |  41 ++
 .../briar/mailbox/MailboxAuthMessage.java     |  51 ++
 .../mailbox/MailboxIntroductionCrypto.java    | 100 ++++
 .../MailboxIntroductionCryptoImpl.java        | 232 +++++++++
 .../MailboxIntroductionManagerImpl.java       | 490 ++++++++++++++++++
 .../mailbox/MailboxIntroductionModule.java    |  96 ++++
 .../mailbox/MailboxIntroductionValidator.java | 248 +++++++++
 .../briar/mailbox/MailboxMessageEncoder.java  |  66 +++
 .../mailbox/MailboxMessageEncoderImpl.java    | 172 ++++++
 .../briar/mailbox/MailboxMessageParser.java   |  40 ++
 .../mailbox/MailboxMessageParserImpl.java     | 156 ++++++
 .../briar/mailbox/MailboxProtocolEngine.java  | 278 ++++++++++
 .../briar/mailbox/MailboxSession.java         | 139 +++++
 .../briar/mailbox/MailboxSessionEncoder.java  |  18 +
 .../mailbox/MailboxSessionEncoderImpl.java    | 157 ++++++
 .../briar/mailbox/MailboxSessionParser.java   |  28 +
 .../mailbox/MailboxSessionParserImpl.java     | 230 ++++++++
 .../briar/mailbox/MailboxState.java           |  37 ++
 .../briar/mailbox/MessageMetadata.java        |  55 ++
 .../briar/mailbox/MessageType.java            |  30 ++
 .../briar/mailbox/OwnerProtocolEngine.java    | 218 ++++++++
 .../briar/mailbox/OwnerSession.java           |  52 ++
 .../briar/mailbox/OwnerState.java             |  36 ++
 .../briar/mailbox/PeerSession.java            |  25 +
 .../briar/mailbox/ProtocolEngine.java         |  46 ++
 .../briar/mailbox/RequestMessage.java         |  28 +
 .../briarproject/briar/mailbox/Session.java   |  44 ++
 .../org/briarproject/briar/mailbox/State.java |   7 +
 mailbox-core/witness.gradle                   |   4 +
 settings.gradle                               |   1 +
 81 files changed, 5300 insertions(+), 211 deletions(-)
 create mode 100644 briar-api/src/main/java/org/briarproject/briar/api/mailbox/event/MailboxIntroductionSucceededEvent.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
 create mode 100644 mailbox-core/build.gradle
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreEagerSingletons.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreModule.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/BdfIncomingMessageHook.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoder.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoderImpl.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageMetadata.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/Session.java
 create mode 100644 mailbox-core/src/main/java/org/briarproject/briar/mailbox/State.java
 create mode 100644 mailbox-core/witness.gradle

diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
index 1c64f5b6d..164d1f5a2 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
@@ -35,6 +35,10 @@ public interface TransportPropertyManager {
 	void addRemoteProperties(Transaction txn, ContactId c,
 			Map<TransportId, TransportProperties> props) throws DbException;
 
+	Map<TransportId, TransportProperties> getLocalAnonymizedProperties(
+			Transaction txn)
+			throws DbException;
+
 	/**
 	 * Returns the local transport properties for all transports.
 	 */
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
index 4b058a6da..7851663bc 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
@@ -108,7 +108,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	@Override
 	public void onClientVisibilityChanging(Transaction txn, Contact c,
 			Visibility v) throws DbException {
-		if(!getApplicableContactTypes().contains(c.getType())) return;
+		if (!getApplicableContactTypes().contains(c.getType())) return;
 		// Apply the client's visibility to the contact group
 		Group g = getContactGroup(c);
 		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
@@ -154,7 +154,19 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			throws DbException {
 		Map<TransportId, TransportProperties> properties = getLocalProperties();
 		for (Entry e : properties.entrySet()) {
-			e.setValue(Collections.EMPTY_MAP);
+			e.setValue(new TransportProperties(Collections.EMPTY_MAP));
+		}
+		return properties;
+	}
+
+	@Override
+	public Map<TransportId, TransportProperties> getLocalAnonymizedProperties(
+			Transaction txn)
+			throws DbException {
+		Map<TransportId, TransportProperties> properties =
+				getLocalProperties(txn);
+		for (Entry e : properties.entrySet()) {
+			e.setValue(new TransportProperties(Collections.EMPTY_MAP));
 		}
 		return properties;
 	}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/mailbox/event/MailboxIntroductionSucceededEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/mailbox/event/MailboxIntroductionSucceededEvent.java
new file mode 100644
index 000000000..e23ee8a69
--- /dev/null
+++ b/briar-api/src/main/java/org/briarproject/briar/api/mailbox/event/MailboxIntroductionSucceededEvent.java
@@ -0,0 +1,22 @@
+package org.briarproject.briar.api.mailbox.event;
+
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class MailboxIntroductionSucceededEvent extends Event {
+
+	private final Contact contact;
+
+	public MailboxIntroductionSucceededEvent(Contact contact) {
+		this.contact = contact;
+	}
+
+	public Contact getContact() {
+		return contact;
+	}
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java
index 76ece2095..6bf182a55 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java
@@ -16,7 +16,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-abstract class AbstractIntroduceeSession extends Session<IntroduceeState>
+abstract class AbstractIntroduceeSession<S extends State> extends Session<S>
 		implements PeerSession {
 
 	final GroupId contactGroupId;
@@ -28,7 +28,7 @@ abstract class AbstractIntroduceeSession extends Session<IntroduceeState>
 	@Nullable
 	final Map<TransportId, KeySetId> transportKeys;
 
-	AbstractIntroduceeSession(SessionId sessionId, IntroduceeState state,
+	AbstractIntroduceeSession(SessionId sessionId, S state,
 			long requestTimestamp, GroupId contactGroupId, Author introducer,
 			Local local, Remote remote, @Nullable byte[] masterKey,
 			@Nullable Map<TransportId, KeySetId> transportKeys,
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
index 5397d7b7e..57753e1c2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
@@ -11,12 +11,18 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 
+import java.util.Map;
+
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
@@ -25,6 +31,7 @@ import static org.briarproject.briar.mailbox.MessageType.DECLINE;
 import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_ACCEPT;
 import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_REQUEST;
 import static org.briarproject.briar.mailbox.MessageType.MAILBOX_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_AUTH;
 import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
 
 @Immutable
@@ -42,6 +49,8 @@ abstract class AbstractProtocolEngine<S extends Session>
 	protected final MailboxMessageEncoder messageEncoder;
 	protected final Clock clock;
 	protected final MailboxIntroductionCrypto crypto;
+	protected final KeyManager keyManager;
+	protected final TransportPropertyManager transportPropertyManager;
 
 	AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
 			ContactManager contactManager,
@@ -49,7 +58,9 @@ abstract class AbstractProtocolEngine<S extends Session>
 			MessageTracker messageTracker, IdentityManager identityManager,
 			MailboxMessageParser messageParser,
 			MailboxMessageEncoder messageEncoder, Clock clock,
-			MailboxIntroductionCrypto crypto) {
+			MailboxIntroductionCrypto crypto,
+			KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
 		this.db = db;
 		this.clientHelper = clientHelper;
 		this.contactManager = contactManager;
@@ -60,6 +71,8 @@ abstract class AbstractProtocolEngine<S extends Session>
 		this.messageEncoder = messageEncoder;
 		this.clock = clock;
 		this.crypto = crypto;
+		this.keyManager = keyManager;
+		this.transportPropertyManager = transportPropertyManager;
 	}
 
 	Message sendMailboxRequestMessage(Transaction txn, PeerSession s,
@@ -78,8 +91,9 @@ abstract class AbstractProtocolEngine<S extends Session>
 			byte[] ephemeralPublicKey, long acceptTimestamp,
 			long messageCounter) throws DbException {
 		Message m = messageEncoder
-				.encodeIntroduceeRequestMessage(s.getContactGroupId(),
+				.encodeMailboxAcceptMessage(s.getContactGroupId(),
 						timestamp, s.getLastLocalMessageId(), s.getSessionId(),
+						MAILBOX_ACCEPT,
 						ephemeralPublicKey,
 						acceptTimestamp,
 						messageCounter);
@@ -89,12 +103,14 @@ abstract class AbstractProtocolEngine<S extends Session>
 	}
 
 	Message sendIntroduceeRequestMessage(Transaction txn, PeerSession s,
-			long timestamp,
+			long timestamp, Author author,
 			byte[] ephemeralPublicKey, long acceptTimestamp,
 			long messageCounter) throws DbException {
 		Message m = messageEncoder
 				.encodeIntroduceeRequestMessage(s.getContactGroupId(),
 						timestamp, s.getLastLocalMessageId(), s.getSessionId(),
+						INTRODUCEE_REQUEST,
+						author,
 						ephemeralPublicKey,
 						acceptTimestamp,
 						messageCounter);
@@ -104,33 +120,33 @@ abstract class AbstractProtocolEngine<S extends Session>
 	}
 
 	Message sendIntroduceeResponseMessage(Transaction txn, PeerSession s,
+			MessageId previousMessage,
 			long timestamp, byte[] ephemeralPublicKey, byte[] mac,
-			byte[] signature, long messageCounter) throws DbException {
+			byte[] signature, long acceptTimestamp, long messageCounter)
+			throws DbException {
 		Message m = messageEncoder
 				.encodeIntroduceeAcceptMessage(s.getContactGroupId(),
 						timestamp,
-						s.getLastLocalMessageId(), s.getSessionId(),
-						ephemeralPublicKey, mac, signature, messageCounter);
+						previousMessage, s.getSessionId(),
+						ephemeralPublicKey, mac, signature, acceptTimestamp,
+						messageCounter);
 		sendMessage(txn, INTRODUCEE_ACCEPT, s.getSessionId(), m,
 				messageCounter);
 		return m;
 	}
 
-	/*
-	Message sendContactAcceptMessage(Transaction txn, PeerSession s,
+	Message sendMailboxAuthMessage(Transaction txn, PeerSession s,
 			long timestamp,
-			byte[] ephemeralPublicKey, long acceptTimestamp,
 			Map<TransportId, TransportProperties> transportProperties,
-			boolean visible) throws DbException {
+			byte[] mac, byte[] signature, long messageCounter)
+			throws DbException {
 		Message m = messageEncoder
-				.encodeAcceptMessage(s.getContactGroupId(), timestamp,
-						s.getLastLocalMessageId(), s.getSessionId(),
-						ephemeralPublicKey, acceptTimestamp,
-						transportProperties);
-		sendMessage(txn, CONTACT_ACCEPT, s.getSessionId(), m);
+				.encodeMailboxAuthMessage(s.getContactGroupId(), timestamp,
+						s.getLastRemoteMessageId(), s.getSessionId(),
+						transportProperties, mac, signature, messageCounter);
+		sendMessage(txn, MAILBOX_AUTH, s.getSessionId(), m, messageCounter);
 		return m;
 	}
-	*/
 
 	/*
 	Message sendMailboxInfotMessage(Transaction txn, PeerSession s,
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java
new file mode 100644
index 000000000..ebb481692
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java
@@ -0,0 +1,52 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class IntroduceeAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected IntroduceeAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey, byte[] mac,
+			byte[] signature, long acceptTimestamp) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java
index 8ac793092..c2ad06076 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.mailbox;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.crypto.KeyPair;
 import org.briarproject.bramble.api.crypto.SecretKey;
@@ -12,14 +13,20 @@ import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionSucceededEvent;
 
 import java.security.GeneralSecurityException;
+import java.util.Map;
 import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
@@ -28,6 +35,8 @@ import javax.inject.Inject;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.bramble.util.LogUtils.logException;
 import static org.briarproject.briar.mailbox.IntroduceeState.AWAIT_AUTH;
+import static org.briarproject.briar.mailbox.IntroduceeState.LOCAL_ACCEPTED;
+import static org.briarproject.briar.mailbox.IntroduceeState.MAILBOX_ADDED;
 
 class IntroduceeProtocolEngine
 		extends AbstractProtocolEngine<IntroduceeSession> {
@@ -42,10 +51,11 @@ class IntroduceeProtocolEngine
 			MessageTracker messageTracker, IdentityManager identityManager,
 			MailboxMessageParser messageParser,
 			MailboxMessageEncoder messageEncoder, Clock clock,
-			MailboxIntroductionCrypto crypto) {
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
 		super(db, clientHelper, contactManager, contactGroupFactory,
 				messageTracker, identityManager, messageParser, messageEncoder,
-				clock, crypto);
+				clock, crypto, keyManager, transportPropertyManager);
 	}
 
 	@Override
@@ -75,7 +85,7 @@ class IntroduceeProtocolEngine
 
 	@Override
 	public IntroduceeSession onMailboxAcceptMessage(Transaction txn,
-			IntroduceeSession session, IntroduceeRequestMessage m)
+			IntroduceeSession session, MailboxAcceptMessage m)
 			throws DbException, FormatException {
 		throw new UnsupportedOperationException();
 	}
@@ -120,22 +130,29 @@ class IntroduceeProtocolEngine
 		long localTimestamp = clock.currentTimeMillis();
 		try {
 			SecretKey secretKey = crypto.deriveMasterKey(publicKey, privateKey,
-					m.getEphemeralPublicKey());
-			SecretKey aliceMacKey = crypto.deriveMacKey(secretKey, false);
+					m.getEphemeralPublicKey(), false);
+			SecretKey aliceMacKey = crypto.deriveMacKey(secretKey, true);
 			SecretKey bobMacKey = crypto.deriveMacKey(secretKey, false);
+
+			s = IntroduceeSession
+					.addLocalAccept(s, LOCAL_ACCEPTED, m, publicKey, privateKey,
+							localTimestamp, secretKey.getBytes(),
+							aliceMacKey.getBytes(), bobMacKey.getBytes());
+
 			byte[] mac = crypto.authMac(bobMacKey, s, localAuthor.getId());
 			byte[] signature =
 					crypto.sign(bobMacKey, localAuthor.getPrivateKey());
+
 			// Send ephemeral public key and timestamp back
 			Message reply =
-					sendIntroduceeResponseMessage(txn, s, localTimestamp,
-							publicKey, mac, signature, s.getMessageCounter());
+					sendIntroduceeResponseMessage(txn, s, m.getMessageId(),
+							localTimestamp,
+							publicKey, mac, signature, localTimestamp,
+							s.getMessageCounter());
 			//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
 			// Move to the AWAIT_REMOTE_RESPONSE state
 			return IntroduceeSession
-					.addLocalAuth(s, AWAIT_AUTH, reply, secretKey, aliceMacKey,
-							bobMacKey,
-							localTimestamp);
+					.addLocalAuth(s, AWAIT_AUTH, reply);
 		} catch (GeneralSecurityException e) {
 			logException(LOG, WARNING, e);
 			return abort(txn, s);
@@ -177,9 +194,48 @@ class IntroduceeProtocolEngine
 
 	@Override
 	public IntroduceeSession onAuthMessage(Transaction txn,
-			IntroduceeSession session,
+			IntroduceeSession s,
 			MailboxAuthMessage m) throws DbException, FormatException {
-		return null;
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getLastLocalMessageId(),
+				m.getPreviousMessageId()))
+			return abort(txn, s);
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		try {
+			crypto.verifyAuthMac(m.getMac(), s, localAuthor.getId());
+			crypto.verifySignature(m.getSignature(), s);
+			long timestamp = Math.min(s.getLocal().acceptTimestamp,
+					s.getRemote().acceptTimestamp);
+			if (timestamp == -1) throw new AssertionError();
+			contactManager
+					.addContact(txn, s.getRemote().author, localAuthor.getId(),
+							false, true);
+			// Only add transport properties and keys when the contact was added
+			// This will be changed once we have a way to reset state for peers
+			// that were contacts already at some point in the past.
+			Contact c = contactManager
+					.getContact(txn, s.getRemote().author.getId(),
+							localAuthor.getId());
+			// add the keys to the new contact
+			//noinspection ConstantConditions
+			Map<TransportId, KeySetId> keys = keyManager
+					.addContact(txn, c.getId(), new SecretKey(s.masterKey),
+							timestamp, s.getLocal().alice, true);
+			// add signed transport properties for the contact
+			//noinspection ConstantConditions
+			transportPropertyManager.addRemoteProperties(txn, c.getId(),
+					m.getTransportProperties());
+			// Broadcast MailboxIntroductionSucceededEvent, because contact got added
+			MailboxIntroductionSucceededEvent e =
+					new MailboxIntroductionSucceededEvent(c);
+			txn.attach(e);
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+		return IntroduceeSession
+				.clear(s, MAILBOX_ADDED, s.getLastLocalMessageId(),
+						clock.currentTimeMillis(), m.getMessageId());
 	}
 
 	@Override
@@ -189,6 +245,12 @@ class IntroduceeProtocolEngine
 		return null;
 	}
 
+	@Override
+	public IntroduceeSession onIntroduceeAcceptMessage(Transaction txn,
+			IntroduceeSession session, IntroduceeAcceptMessage acceptMessage) {
+		throw new UnsupportedOperationException();
+	}
+
 
 	private boolean isInvalidDependency(AbstractIntroduceeSession s,
 			@Nullable MessageId dependency) {
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java
index 88146af1d..3e5ea3d3e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java
@@ -1,5 +1,6 @@
 package org.briarproject.briar.mailbox;
 
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
@@ -13,15 +14,17 @@ import javax.annotation.concurrent.Immutable;
 class IntroduceeRequestMessage extends AbstractMailboxIntroductionMessage {
 
 	private final SessionId sessionId;
+	private final Author author;
 	private final byte[] ephemeralPublicKey;
 	private final long acceptTimestamp;
 
 	protected IntroduceeRequestMessage(MessageId messageId, GroupId groupId,
 			long timestamp, @Nullable MessageId previousMessageId,
-			SessionId sessionId, byte[] ephemeralPublicKey,
+			SessionId sessionId, Author author, byte[] ephemeralPublicKey,
 			long acceptTimestamp) {
 		super(messageId, groupId, timestamp, previousMessageId);
 		this.sessionId = sessionId;
+		this.author = author;
 		this.ephemeralPublicKey = ephemeralPublicKey;
 		this.acceptTimestamp = acceptTimestamp;
 	}
@@ -30,6 +33,10 @@ class IntroduceeRequestMessage extends AbstractMailboxIntroductionMessage {
 		return sessionId;
 	}
 
+	public Author getAuthor() {
+		return author;
+	}
+
 	public byte[] getEphemeralPublicKey() {
 		return ephemeralPublicKey;
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
index 825ada662..29270beea 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
@@ -1,6 +1,5 @@
 package org.briarproject.briar.mailbox;
 
-import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.plugin.TransportId;
@@ -22,7 +21,7 @@ import static org.briarproject.briar.mailbox.IntroduceeState.START;
 
 @Immutable
 @NotNullByDefault
-class IntroduceeSession extends AbstractIntroduceeSession {
+class IntroduceeSession extends AbstractIntroduceeSession<IntroduceeState> {
 
 
 	IntroduceeSession(SessionId sessionId, IntroduceeState state,
@@ -46,34 +45,34 @@ class IntroduceeSession extends AbstractIntroduceeSession {
 	}
 
 	static IntroduceeSession addLocalAccept(IntroduceeSession s,
-			IntroduceeState state, Message m, byte[] ephemeralPublicKey,
-			byte[] ephemeralPrivateKey, long acceptTimestamp) {
-		Local local = new Local(false, m.getId(), m.getTimestamp(),
-				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp, null);
+			IntroduceeState state, IntroduceeRequestMessage m,
+			byte[] ephemeralPublicKey,
+			byte[] ephemeralPrivateKey, long acceptTimestamp, byte[] masterKey,
+			byte[] aliceMackey, byte[] bobMacKey) {
+		Local local = new Local(false, s.local.lastMessageId,
+				s.local.lastMessageTimestamp,
+				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp,
+				bobMacKey);
+		Remote remote = new Remote(true, m.getAuthor(), m.getMessageId(),
+				m.getEphemeralPublicKey(), null, m.getAcceptTimestamp(),
+				aliceMackey);
 		return new IntroduceeSession(s.getSessionId(), state, m.getTimestamp(),
-				s.contactGroupId, s.introducer, local, s.remote, s.masterKey,
-				s.transportKeys, s.getMessageCounter() + 1);
+				s.contactGroupId, s.introducer, local, remote, masterKey,
+				s.transportKeys, s.getMessageCounter());
 	}
 
 	static IntroduceeSession addLocalAuth(IntroduceeSession s,
 			IntroduceeState state,
-			Message m, SecretKey masterKey,
-			SecretKey aliceMacKey,
-			SecretKey bobMacKey, long acceptTimestamp) {
+			Message m) {
 		// add mac key and sent message
 		Local local = new Local(false, m.getId(), m.getTimestamp(),
-				null, null,
-				acceptTimestamp, bobMacKey.getBytes());
-		// just add the mac key
-		Remote remote = new Remote(true, s.remote.author,
-				s.remote.lastMessageId, null, null,
-				s.remote.acceptTimestamp,
-				aliceMacKey.getBytes());
-		// add master key
+				s.local.ephemeralPublicKey, s.local.ephemeralPrivateKey,
+				s.local.acceptTimestamp, s.local.macKey);
+
 		return new IntroduceeSession(s.getSessionId(), state,
 				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
-				remote, masterKey.getBytes(), s.transportKeys,
-				s.getMessageCounter() + 1);
+				s.remote, s.masterKey, s.transportKeys,
+				s.getMessageCounter());
 	}
 
 	static IntroduceeSession awaitAuth(IntroduceeSession s,
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
new file mode 100644
index 000000000..6b740de82
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
@@ -0,0 +1,41 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MailboxAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+
+	protected MailboxAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
index da930637d..b66882a70 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
@@ -17,22 +17,17 @@ import javax.annotation.concurrent.Immutable;
 class MailboxAuthMessage extends AbstractMailboxIntroductionMessage {
 
 	private final SessionId sessionId;
-	private final byte[] ephemeralPublicKey;
-	private final long acceptTimestamp;
 	private final Map<TransportId, TransportProperties> transportProperties;
 	private final byte[] mac;
 	private final byte[] signature;
 
 	protected MailboxAuthMessage(MessageId messageId, GroupId groupId,
 			long timestamp, @Nullable MessageId previousMessageId,
-			SessionId sessionId, byte[] ephemeralPublicKey,
-			long acceptTimestamp,
+			SessionId sessionId,
 			Map<TransportId, TransportProperties> transportProperties,
 			byte[] mac, byte[] signature) {
 		super(messageId, groupId, timestamp, previousMessageId);
 		this.sessionId = sessionId;
-		this.ephemeralPublicKey = ephemeralPublicKey;
-		this.acceptTimestamp = acceptTimestamp;
 		this.transportProperties = transportProperties;
 		this.mac = mac;
 		this.signature = signature;
@@ -42,14 +37,6 @@ class MailboxAuthMessage extends AbstractMailboxIntroductionMessage {
 		return sessionId;
 	}
 
-	public byte[] getEphemeralPublicKey() {
-		return ephemeralPublicKey;
-	}
-
-	public long getAcceptTimestamp() {
-		return acceptTimestamp;
-	}
-
 	public byte[] getMac() {
 		return mac;
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
index ea4d38f63..21a39bfbb 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
@@ -15,15 +15,8 @@ interface MailboxIntroductionCrypto {
 	 * Returns the {@link SessionId} based on the introducer
 	 * and the two introducees.
 	 */
-	SessionId getSessionId(Author introducer, Author local, Author remote);
-
-	/**
-	 * Returns true if the local author is alice
-	 * <p>
-	 * Alice is the Author whose unique ID has the lower ID,
-	 * comparing the IDs as byte strings.
-	 */
-	boolean isAlice(AuthorId local, AuthorId remote);
+	SessionId getSessionId(Author introducer, Author local, Author remote,
+			boolean isAlice);
 
 	/**
 	 * Generates an agreement key pair.
@@ -40,7 +33,8 @@ interface MailboxIntroductionCrypto {
 
 	@SuppressWarnings("ConstantConditions")
 	SecretKey deriveMasterKey(byte[] ephemeralPublicKey,
-			byte[] ephemeralPrivateKey, byte[] remoteEphemeralPublicKey)
+			byte[] ephemeralPrivateKey, byte[] remoteEphemeralPublicKey,
+			boolean alice)
 			throws GeneralSecurityException;
 
 	/**
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
index debe6e83f..0de274163 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
@@ -49,8 +49,7 @@ class MailboxIntroductionCryptoImpl implements MailboxIntroductionCrypto {
 
 	@Override
 	public SessionId getSessionId(Author introducer, Author local,
-			Author remote) {
-		boolean isAlice = isAlice(local.getId(), remote.getId());
+			Author remote, boolean isAlice) {
 		byte[] hash = crypto.hash(
 				LABEL_SESSION_ID,
 				introducer.getId().getBytes(),
@@ -65,11 +64,6 @@ class MailboxIntroductionCryptoImpl implements MailboxIntroductionCrypto {
 		return crypto.generateAgreementKeyPair();
 	}
 
-	@Override
-	public boolean isAlice(AuthorId local, AuthorId remote) {
-		return local.compareTo(remote) < 0;
-	}
-
 	@Override
 	@SuppressWarnings("ConstantConditions")
 	public SecretKey deriveMasterKey(AbstractIntroduceeSession s)
@@ -83,19 +77,7 @@ class MailboxIntroductionCryptoImpl implements MailboxIntroductionCrypto {
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
-	public SecretKey deriveMasterKey(byte[] ephemeralPublicKey,
-			byte[] ephemeralPrivateKey, byte[] remoteEphemeralPublicKey)
-			throws GeneralSecurityException {
-		return deriveMasterKey(
-				ephemeralPublicKey,
-				ephemeralPrivateKey,
-				remoteEphemeralPublicKey,
-				false
-		);
-	}
-
-	SecretKey deriveMasterKey(byte[] publicKey, byte[] privateKey,
+	public SecretKey deriveMasterKey(byte[] publicKey, byte[] privateKey,
 			byte[] remotePublicKey, boolean alice)
 			throws GeneralSecurityException {
 		KeyParser kp = crypto.getAgreementKeyParser();
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
index 3a81dd373..4192afbe8 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
@@ -17,11 +17,14 @@ import org.briarproject.bramble.api.db.Metadata;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.mailbox.MailboxInfo;
 import org.briarproject.bramble.api.sync.Client;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
@@ -31,6 +34,7 @@ import org.briarproject.briar.client.BdfIncomingMessageHook;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Map;
+import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
@@ -38,12 +42,14 @@ import javax.inject.Inject;
 
 import static org.briarproject.bramble.api.contact.ContactManager.ContactHook;
 import static org.briarproject.bramble.api.contact.ContactType.MAILBOX_OWNER;
+import static org.briarproject.bramble.api.contact.ContactType.PRIVATE_MAILBOX;
 import static org.briarproject.bramble.api.contact.ContactType.values;
 import static org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import static org.briarproject.briar.api.mailbox.Role.INTRODUCEE;
 import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
 import static org.briarproject.briar.api.mailbox.Role.OWNER;
 import static org.briarproject.briar.mailbox.IntroductionConstants.GROUP_KEY_CONTACT_ID;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_REQUEST;
 import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
 
 class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
@@ -52,6 +58,7 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 
 	private static final Logger LOG =
 			Logger.getLogger(MailboxIntroductionManagerImpl.class.getName());
+	private final Executor ioExecutor;
 	private final ClientVersioningManager clientVersioningManager;
 	private final ContactGroupFactory contactGroupFactory;
 	private final ContactManager contactManager;
@@ -59,15 +66,18 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 	//	private final SessionEncoder sessionEncoder;
 	private final MailboxSessionParser sessionParser;
 	private final OwnerProtocolEngine ownerProtocolEngine;
+	private final IntroduceeProtocolEngine introduceeProtocolEngine;
 	private final MailboxSessionEncoder sessionEncoder;
 	private final MailboxIntroductionCrypto crypto;
 	private final MailboxProtocolEngine mailboxProtocolEngine;
 	private final IdentityManager identityManager;
+	private final Clock clock;
 
 	private final Group localGroup;
 
 	@Inject
-	MailboxIntroductionManagerImpl(DatabaseComponent db,
+	MailboxIntroductionManagerImpl(@IoExecutor Executor ioExecutor,
+			DatabaseComponent db,
 			ClientHelper clientHelper,
 			ClientVersioningManager clientVersioningManager,
 			MetadataParser metadataParser,
@@ -75,23 +85,28 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 			ContactManager contactManager, MailboxMessageParser messageParser,
 			MailboxSessionParser sessionParser,
 			OwnerProtocolEngine ownerProtocolEngine,
+			IntroduceeProtocolEngine introduceeProtocolEngine,
 			MailboxSessionEncoder sessionEncoder,
 			MailboxIntroductionCrypto crypto,
 			MailboxProtocolEngine mailboxProtocolEngine,
-			IdentityManager identityManager) {
+			IdentityManager identityManager,
+			Clock clock) {
 		super(db, clientHelper, metadataParser);
+		this.ioExecutor = ioExecutor;
 		this.clientVersioningManager = clientVersioningManager;
 		this.contactGroupFactory = contactGroupFactory;
 		this.contactManager = contactManager;
 		this.messageParser = messageParser;
 		this.sessionParser = sessionParser;
 		this.ownerProtocolEngine = ownerProtocolEngine;
+		this.introduceeProtocolEngine = introduceeProtocolEngine;
 		this.sessionEncoder = sessionEncoder;
 		this.crypto = crypto;
 		this.mailboxProtocolEngine = mailboxProtocolEngine;
 		this.identityManager = identityManager;
 		this.localGroup =
 				contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
+		this.clock = clock;
 	}
 
 	@Override
@@ -170,24 +185,19 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 		MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
 		// Look up the session, if there is one
 		SessionId sessionId = meta.getSessionId();
-		MailboxSession newMailboxSession = null;
-		if (sessionId == null) {
-			if (meta.getMessageType() != MAILBOX_REQUEST)
-				throw new AssertionError();
-			newMailboxSession = createNewMailboxSession(txn, m, body);
-			sessionId = newMailboxSession.getSessionId();
-		}
+		IntroduceeSession newMailboxSession = null;
+		if (sessionId == null) new AssertionError();
 		StoredSession ss = getSession(txn, sessionId);
 		// Handle the message
 		Session session;
 		MessageId storageId;
 		if (ss == null) {
-			if (meta.getMessageType() != MAILBOX_REQUEST)
-				throw new FormatException();
+			if (meta.getMessageType() == INTRODUCEE_REQUEST)
+				newMailboxSession = createNewIntroduceeSession(txn, m, body);
 			if (newMailboxSession == null) throw new AssertionError();
 			storageId = createStorageId(txn);
 			session = handleMessage(txn, m, body, meta.getMessageType(),
-					newMailboxSession, mailboxProtocolEngine);
+					newMailboxSession, introduceeProtocolEngine);
 		} else {
 			storageId = ss.storageId;
 			long messageCounter =
@@ -203,8 +213,8 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 						ownerProtocolEngine);
 			} else if (role == INTRODUCEE) {
 				session = handleMessage(txn, m, body, meta.getMessageType(),
-						sessionParser.parseMailboxSession(m.getGroupId(),
-								ss.bdfSession), mailboxProtocolEngine);
+						sessionParser.parseIntroduceeSession(m.getGroupId(),
+								ss.bdfSession), introduceeProtocolEngine);
 			} else throw new AssertionError();
 		}
 		// Store the updated session
@@ -219,12 +229,27 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 		Author local = identityManager.getLocalAuthor(txn);
 		Author remote = messageParser.parseRequestMessage(m, body).getAuthor();
 		if (local.equals(remote)) throw new FormatException();
-		SessionId sessionId = crypto.getSessionId(owner, local, remote);
-		boolean alice = crypto.isAlice(local.getId(), remote.getId());
+		SessionId sessionId = crypto.getSessionId(owner, local, remote, true);
 		return MailboxSession
-				.getInitial(m.getGroupId(), sessionId, owner, alice, remote);
+				.getInitial(m.getGroupId(), sessionId, owner, true, remote);
 	}
 
+	private IntroduceeSession createNewIntroduceeSession(Transaction txn,
+			Message m,
+			BdfList body)
+			throws DbException, FormatException {
+		ContactId ownerId = getContactId(txn, m.getGroupId());
+		Author owner = db.getContact(txn, ownerId).getAuthor();
+		Author local = identityManager.getLocalAuthor(txn);
+		Author remote = messageParser.parseIntroduceeRequestMessage(m, body)
+				.getAuthor();
+		if (local.equals(remote)) throw new FormatException();
+		SessionId sessionId = crypto.getSessionId(owner, local, remote, false);
+		return IntroduceeSession
+				.getInitial(m.getGroupId(), sessionId, owner, false, remote);
+	}
+
+
 	/*
 	private IntroduceeSession createNewIntroduceeSession(Transaction txn,
 			Message m, BdfList body) throws DbException, FormatException {
@@ -252,15 +277,27 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 				return engine.onRequestMessage(txn, session, request);
 			}
 			case MAILBOX_ACCEPT: {
-				IntroduceeRequestMessage acceptMessage =
+				MailboxAcceptMessage acceptMessage =
 						messageParser.parseMailboxAcceptMessage(m, body);
 				return engine
 						.onMailboxAcceptMessage(txn, session, acceptMessage);
 			}
-			case INTRODUCEE_REQUEST:
+			case INTRODUCEE_REQUEST: {
 				IntroduceeRequestMessage acceptMessage =
-						messageParser.parseMailboxAcceptMessage(m, body);
-				return engine.onIntroduceeRequestMessage(txn, session, acceptMessage);
+						messageParser.parseIntroduceeRequestMessage(m, body);
+				return engine.onIntroduceeRequestMessage(txn, session,
+						acceptMessage);
+			}
+			case INTRODUCEE_ACCEPT: {
+				IntroduceeAcceptMessage acceptMessage =
+						messageParser.parseIntroduceeAcceptMessage(m, body);
+				return engine
+						.onIntroduceeAcceptMessage(txn, session, acceptMessage);
+			}
+			case MAILBOX_AUTH:
+				MailboxAuthMessage authMessage =
+						messageParser.parseMailboxAuthMessage(m, body);
+				return engine.onAuthMessage(txn, session, authMessage);
 			case ABORT: {
 				AbortMessage abort = messageParser.parseAbortMessage(m, body);
 				return engine.onAbortMessage(txn, session, abort);
@@ -279,14 +316,15 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 	public void addingContact(Transaction txn, Contact c) throws DbException {
 		switch (c.getType()) {
 			case PRIVATE_MAILBOX:
+				break;
 			case MAILBOX_OWNER:
+				break;
 			case CONTACT:
 				contactAdded(txn, c);
 				break;
 			default:
 				return;
 		}
-
 	}
 
 	@Override
@@ -306,7 +344,23 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 		} catch (FormatException e) {
 			throw new AssertionError(e);
 		}
-		LOG.info("Contact added");
+		Collection<Contact> pm = db.getContactsByType(txn, PRIVATE_MAILBOX);
+		if (pm.isEmpty()) return;
+		Collection<MailboxInfo> mailboxes = db.getContactMailboxes(txn);
+		for (MailboxInfo mailboxInfo : mailboxes) {
+			if (mailboxInfo.getContactId().equals(c.getId())) return;
+		}
+		ioExecutor.execute(
+				() -> {
+					try {
+						makeIntroduction((PrivateMailbox) pm.iterator().next(),
+								c,
+								clock.currentTimeMillis());
+					} catch (DbException e) {
+						LOG.warning(
+								"Mailbox introduction failed: " + e.toString());
+					}
+				});
 	}
 
 	@Override
@@ -338,7 +392,7 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 			Author introducer = identityManager.getLocalAuthor(txn);
 			SessionId sessionId =
 					crypto.getSessionId(introducer, privateMailbox.getAuthor(),
-							contact.getAuthor());
+							contact.getAuthor(), true);
 			StoredSession ss = getSession(txn, sessionId);
 			// Create or parse the session
 			OwnerSession session;
@@ -375,9 +429,11 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 		BdfDictionary d;
 		if (session.getRole() == OWNER) {
 			d = sessionEncoder.encodeIntroducerSession((OwnerSession) session);
-		} else if (session.getRole() == MAILBOX) {
+		} else if (session.getRole() == MAILBOX ||
+				session.getRole() == INTRODUCEE) {
 			d = sessionEncoder
-					.encodeIntroduceeSession((AbstractIntroduceeSession) session);
+					.encodeIntroduceeSession(
+							(AbstractIntroduceeSession) session);
 		} else {
 			throw new AssertionError();
 		}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
index 144fd6d88..8725fc9f2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
@@ -22,8 +22,11 @@ import javax.annotation.concurrent.Immutable;
 import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
 import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
 import static org.briarproject.bramble.util.ValidationUtils.checkLength;
 import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_REQUEST;
 import static org.briarproject.briar.mailbox.MessageType.MAILBOX_ACCEPT;
 import static org.briarproject.briar.mailbox.MessageType.MAILBOX_AUTH;
 
@@ -51,8 +54,12 @@ class MailboxIntroductionValidator extends BdfMessageValidator {
 				return validateRequestMessage(m, body);
 			case MAILBOX_ACCEPT:
 				return validateAcceptMessage(m, body);
+			case INTRODUCEE_REQUEST:
+				return validateIntroduceeRequestMessage(m, body);
 			case MAILBOX_AUTH:
 				return validateAuthMessage(m, body);
+			case INTRODUCEE_ACCEPT:
+				return validateIntroduceeAcceptMessage(m, body);
 			case DECLINE:
 			case ABORT:
 				return validateOtherMessage(type, m, body);
@@ -111,9 +118,82 @@ class MailboxIntroductionValidator extends BdfMessageValidator {
 		}
 	}
 
+	private BdfMessageContext validateIntroduceeRequestMessage(Message m,
+			BdfList body)
+			throws FormatException {
+		checkSize(body, 7);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		BdfList authorList = body.getList(2);
+		clientHelper.parseAndValidateAuthor(authorList);
+
+		byte[] previousMessageId = body.getOptionalRaw(3);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		byte[] ephemeralPublicKey = body.getRaw(4);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		long timestamp = body.getLong(5);
+		if (timestamp < 0) throw new FormatException();
+
+		long messageCounter = body.getLong(6);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(INTRODUCEE_REQUEST, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateIntroduceeAcceptMessage(Message m,
+			BdfList body)
+			throws FormatException {
+		checkSize(body, 8);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		byte[] mac = body.getRaw(4);
+		checkLength(mac, MAC_BYTES);
+
+		byte[] signature = body.getRaw(5);
+		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
+
+		long timestamp = body.getLong(6);
+		if (timestamp < 0) throw new FormatException();
+
+		long messageCounter = body.getLong(7);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(INTRODUCEE_ACCEPT, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
 	private BdfMessageContext validateAuthMessage(Message m, BdfList body)
 			throws FormatException {
-		checkSize(body, 5);
+		checkSize(body, 7);
 
 		byte[] sessionIdBytes = body.getRaw(1);
 		checkLength(sessionIdBytes, UniqueId.LENGTH);
@@ -121,16 +201,23 @@ class MailboxIntroductionValidator extends BdfMessageValidator {
 		byte[] previousMessageId = body.getRaw(2);
 		checkLength(previousMessageId, UniqueId.LENGTH);
 
-		byte[] mac = body.getRaw(3);
+		BdfDictionary transportProperties = body.getDictionary(3);
+		if (transportProperties.size() < 1) throw new FormatException();
+		clientHelper
+				.parseAndValidateTransportPropertiesMap(transportProperties);
+
+		byte[] mac = body.getRaw(4);
 		checkLength(mac, MAC_BYTES);
 
-		byte[] signature = body.getRaw(4);
+		byte[] signature = body.getRaw(5);
 		checkLength(signature, 1, MAX_SIGNATURE_BYTES);
 
+		long messageCounter = body.getLong(6);
+
 		SessionId sessionId = new SessionId(sessionIdBytes);
 		BdfDictionary meta = messageEncoder
 				.encodeMetadata(MAILBOX_AUTH, sessionId, m.getTimestamp(),
-						false, 0);
+						false, messageCounter);
 		MessageId dependency = new MessageId(previousMessageId);
 		return new BdfMessageContext(meta,
 				Collections.singletonList(dependency));
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java
index ed8414e5c..76ab219ed 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java
@@ -3,11 +3,15 @@ package org.briarproject.briar.mailbox;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
 
+import java.util.Map;
+
 import javax.annotation.Nullable;
 
 @NotNullByDefault
@@ -31,14 +35,28 @@ interface MailboxMessageEncoder {
 			long timestamp,
 			@Nullable MessageId previousMessageId, SessionId sessionId,
 			byte[] ephemeralPublicKey, byte[] mac, byte[] signature,
+			long acceptTimestamp,
+			long messageCounter);
+
+	Message encodeMailboxAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, MessageType type, byte[] ephemeralPublicKey,
+			long acceptTimestamp,
 			long messageCounter);
 
 	Message encodeIntroduceeRequestMessage(GroupId contactGroupId,
 			long timestamp, @Nullable MessageId previousMessageId,
-			SessionId sessionId,
+			SessionId sessionId, MessageType type, Author author,
 			byte[] ephemeralPublicKey, long acceptTimestamp,
 			long messageCounter);
 
+	Message encodeMailboxAuthMessage(GroupId contactGroupId,
+			long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature,
+			long messageCounter);
+
 	Message encodeDeclineMessage(GroupId contactGroupId, long timestamp,
 			@Nullable MessageId previousMessageId, SessionId sessionId);
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java
index c1bdc76d8..6fe872150 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java
@@ -6,12 +6,16 @@ import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageFactory;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
 
+import java.util.Map;
+
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
@@ -23,7 +27,8 @@ import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_SESSI
 import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_TIMESTAMP;
 import static org.briarproject.briar.mailbox.MessageType.ABORT;
 import static org.briarproject.briar.mailbox.MessageType.DECLINE;
-import static org.briarproject.briar.mailbox.MessageType.MAILBOX_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_AUTH;
 import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
 
 @NotNullByDefault
@@ -82,12 +87,26 @@ class MailboxMessageEncoderImpl implements MailboxMessageEncoder {
 		return createMessage(contactGroupId, timestamp, body);
 	}
 
+	@Override
+	public Message encodeMailboxAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, MessageType type,
+			byte[] ephemeralPublicKey,
+			long acceptTimestamp, long messageCounter) {
+		BdfList body = BdfList.of(type.getValue(), sessionId,
+				previousMessageId, ephemeralPublicKey, acceptTimestamp,
+				messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
 	@Override
 	public Message encodeIntroduceeRequestMessage(GroupId contactGroupId,
 			long timestamp, @Nullable MessageId previousMessageId,
-			SessionId sessionId, byte[] ephemeralPublicKey,
+			SessionId sessionId, MessageType type, Author author,
+			byte[] ephemeralPublicKey,
 			long acceptTimestamp, long messageCounter) {
-		BdfList body = BdfList.of(MAILBOX_ACCEPT.getValue(), sessionId,
+		BdfList body = BdfList.of(type.getValue(), sessionId,
+				clientHelper.toList(author),
 				previousMessageId, ephemeralPublicKey, acceptTimestamp,
 				messageCounter);
 		return createMessage(contactGroupId, timestamp, body);
@@ -97,10 +116,23 @@ class MailboxMessageEncoderImpl implements MailboxMessageEncoder {
 	public Message encodeIntroduceeAcceptMessage(GroupId contactGroupId,
 			long timestamp, @Nullable MessageId previousMessageId,
 			SessionId sessionId, byte[] ephemeralPublicKey, byte[] mac,
-			byte[] signature, long messageCounter) {
-		BdfList body = BdfList.of(MAILBOX_ACCEPT.getValue(), sessionId,
+			byte[] signature, long acceptTimestamp, long messageCounter) {
+		BdfList body = BdfList.of(INTRODUCEE_ACCEPT.getValue(), sessionId,
 				previousMessageId, ephemeralPublicKey, mac, signature,
-				timestamp,
+				acceptTimestamp,
+				messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeMailboxAuthMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac,
+			byte[] signature, long messageCounter) {
+		BdfList body = BdfList.of(MAILBOX_AUTH.getValue(), sessionId,
+				previousMessageId, clientHelper.toDictionary(transportProperties), mac, signature,
 				messageCounter);
 		return createMessage(contactGroupId, timestamp, body);
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java
index 6ee90e89a..4c928a78d 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java
@@ -19,9 +19,15 @@ interface MailboxMessageParser {
 	RequestMessage parseRequestMessage(Message m, BdfList body)
 			throws FormatException;
 
-	IntroduceeRequestMessage parseMailboxAcceptMessage(Message m, BdfList body)
+	MailboxAcceptMessage parseMailboxAcceptMessage(Message m, BdfList body)
 			throws FormatException;
 
+	IntroduceeRequestMessage parseIntroduceeRequestMessage(Message m,
+			BdfList body) throws FormatException;
+
+	IntroduceeAcceptMessage parseIntroduceeAcceptMessage(Message m,
+			BdfList body) throws FormatException;
+
 	DeclineMessage parseDeclineMessage(Message m, BdfList body)
 			throws FormatException;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java
index 944bf0e08..a040aa3d7 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java
@@ -7,10 +7,14 @@ import org.briarproject.bramble.api.data.BdfEntry;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
 
+import java.util.Map;
+
 import javax.inject.Inject;
 
 import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
@@ -67,7 +71,7 @@ class MailboxMessageParserImpl implements MailboxMessageParser {
 	}
 
 	@Override
-	public IntroduceeRequestMessage parseMailboxAcceptMessage(Message m,
+	public MailboxAcceptMessage parseMailboxAcceptMessage(Message m,
 			BdfList body) throws FormatException {
 		SessionId sessionId = new SessionId(body.getRaw(1));
 		byte[] previousMsgBytes = body.getOptionalRaw(2);
@@ -75,11 +79,42 @@ class MailboxMessageParserImpl implements MailboxMessageParser {
 				new MessageId(previousMsgBytes));
 		byte[] ephemeralPublicKey = body.getRaw(3);
 		long acceptTimestamp = body.getLong(4);
-		return new IntroduceeRequestMessage(m.getId(), m.getGroupId(),
+		return new MailboxAcceptMessage(m.getId(), m.getGroupId(),
 				m.getTimestamp(), previousMessageId, sessionId,
 				ephemeralPublicKey, acceptTimestamp);
 	}
 
+	@Override
+	public IntroduceeRequestMessage parseIntroduceeRequestMessage(Message m,
+			BdfList body) throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		Author author = clientHelper.parseAndValidateAuthor(body.getList(2));
+		byte[] previousMsgBytes = body.getOptionalRaw(3);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		byte[] ephemeralPublicKey = body.getRaw(4);
+		long acceptTimestamp = body.getLong(5);
+		return new IntroduceeRequestMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId, author,
+				ephemeralPublicKey, acceptTimestamp);
+	}
+
+	@Override
+	public IntroduceeAcceptMessage parseIntroduceeAcceptMessage(Message m,
+			BdfList body) throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		byte[] mac = body.getRaw(4);
+		byte[] signature = body.getRaw(5);
+		long acceptTimestamp = body.getLong(6);
+		return new IntroduceeAcceptMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId,
+				ephemeralPublicKey, mac, signature, acceptTimestamp);
+	}
+
 	@Override
 	public DeclineMessage parseDeclineMessage(Message m, BdfList body)
 			throws FormatException {
@@ -94,16 +129,17 @@ class MailboxMessageParserImpl implements MailboxMessageParser {
 	@Override
 	public MailboxAuthMessage parseMailboxAuthMessage(Message m, BdfList body)
 			throws FormatException {
-		/*
 		SessionId sessionId = new SessionId(body.getRaw(1));
 		byte[] previousMsgBytes = body.getRaw(2);
 		MessageId previousMessageId = new MessageId(previousMsgBytes);
-		byte[] mac = body.getRaw(3);
-		byte[] signature = body.getRaw(4);
-		return new MailboxAuthMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
-				previousMessageId, sessionId, mac);
-				*/
-		return null;
+		Map<TransportId, TransportProperties> transportProperties = clientHelper
+				.parseAndValidateTransportPropertiesMap(body.getDictionary(3));
+		byte[] mac = body.getRaw(4);
+		byte[] signature = body.getRaw(5);
+		return new MailboxAuthMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(),
+				previousMessageId, sessionId, transportProperties, mac,
+				signature);
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
index e134fc57c..82f96d31b 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
@@ -3,28 +3,46 @@ package org.briarproject.briar.mailbox;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.data.BdfDictionary;
 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.IdentityManager;
 import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionSucceededEvent;
+
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
-import static org.briarproject.briar.mailbox.IntroduceeState.AWAIT_REMOTE_RESPONSE;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.briar.mailbox.MailboxState.AWAIT_REMOTE_RESPONSE;
+import static org.briarproject.briar.mailbox.MailboxState.CONTACT_ADDED;
 
 class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
 
+	private final static Logger LOG =
+			Logger.getLogger(MailboxProtocolEngine.class.getName());
+
 	@Inject
 	MailboxProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
 			ContactManager contactManager,
@@ -32,10 +50,12 @@ class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
 			MessageTracker messageTracker, IdentityManager identityManager,
 			MailboxMessageParser messageParser,
 			MailboxMessageEncoder messageEncoder, Clock clock,
-			MailboxIntroductionCrypto crypto) {
+			MailboxIntroductionCrypto crypto,
+			KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
 		super(db, clientHelper, contactManager, contactGroupFactory,
 				messageTracker, identityManager, messageParser, messageEncoder,
-				clock, crypto);
+				clock, crypto, keyManager, transportPropertyManager);
 	}
 
 	@Override
@@ -67,7 +87,7 @@ class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
 			case LOCAL_DECLINED:
 			case LOCAL_ACCEPTED:
 			case AWAIT_REMOTE_RESPONSE:
-			case MAILBOX_ADDED:
+			case CONTACT_ADDED:
 				return abort(txn, session);
 			default:
 				throw new AssertionError();
@@ -101,7 +121,8 @@ class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
 		//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
 		// Move to the AWAIT_REMOTE_RESPONSE state
 		return MailboxSession
-				.addLocalAccept(s, AWAIT_REMOTE_RESPONSE, reply, publicKey,
+				.addLocalAccept(s, AWAIT_REMOTE_RESPONSE, reply,
+						publicKey,
 						privateKey, localTimestamp);
 	}
 
@@ -126,7 +147,7 @@ class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
 
 	@Override
 	public MailboxSession onMailboxAcceptMessage(Transaction txn,
-			MailboxSession session, IntroduceeRequestMessage m)
+			MailboxSession session, MailboxAcceptMessage m)
 			throws DbException, FormatException {
 		return null;
 	}
@@ -165,6 +186,75 @@ class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
 		return null;
 	}
 
+	@Override
+	public MailboxSession onIntroduceeAcceptMessage(Transaction txn,
+			MailboxSession s, IntroduceeAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getLastLocalMessageId(),
+				m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Broadcast IntroductionRequestReceivedEvent
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		try {
+			SecretKey secretKey =
+					crypto.deriveMasterKey(s.local.ephemeralPublicKey,
+							s.local.ephemeralPrivateKey,
+							m.getEphemeralPublicKey(), true);
+			SecretKey aliceMacKey = crypto.deriveMacKey(secretKey, true);
+			SecretKey bobMacKey = crypto.deriveMacKey(secretKey, false);
+
+			s = MailboxSession.addIntroduceeAccept(s, m.getMessageId(),
+					m.getEphemeralPublicKey(), secretKey, aliceMacKey,
+					bobMacKey,
+					m.getAcceptTimestamp());
+			crypto.verifyAuthMac(m.getMac(), s, localAuthor.getId());
+			crypto.verifySignature(m.getSignature(), s);
+			long timestamp = Math.min(s.getLocal().acceptTimestamp,
+					s.getRemote().acceptTimestamp);
+			if (timestamp == -1) throw new AssertionError();
+			byte[] mac = crypto.authMac(aliceMacKey, s, localAuthor.getId());
+			byte[] signature =
+					crypto.sign(aliceMacKey, localAuthor.getPrivateKey());
+
+			contactManager
+					.addContact(txn, s.getRemote().author, localAuthor.getId(),
+							false, true);
+			// Only add transport properties and keys when the contact was added
+			// This will be changed once we have a way to reset state for peers
+			// that were contacts already at some point in the past.
+			Contact c = contactManager
+					.getContact(txn, s.getRemote().author.getId(),
+							localAuthor.getId());
+			// add the keys to the new contact
+			//noinspection ConstantConditions
+			Map<TransportId, KeySetId> keys = keyManager
+					.addContact(txn, c.getId(), secretKey,
+							timestamp, s.getLocal().alice, true);
+			// add signed transport properties for the contact
+			//noinspection ConstantConditions
+			transportPropertyManager.addRemoteProperties(txn, c.getId(),
+					transportPropertyManager.getLocalAnonymizedProperties(txn));
+			// Broadcast MailboxIntroductionSucceededEvent, because contact got added
+			MailboxIntroductionSucceededEvent e =
+					new MailboxIntroductionSucceededEvent(c);
+			txn.attach(e);
+			Map<TransportId, TransportProperties> transportProperties =
+					transportPropertyManager.getLocalProperties(txn);
+			Message reply =
+					sendMailboxAuthMessage(txn, s, clock.currentTimeMillis(),
+							transportProperties, mac, signature,
+							s.getMessageCounter());
+			//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
+			return MailboxSession
+					.clear(s, CONTACT_ADDED, reply.getId(),
+							s.getLocalTimestamp(), m.getMessageId());
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+
+	}
+
 
 	private boolean isInvalidDependency(AbstractIntroduceeSession s,
 			@Nullable MessageId dependency) {
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
index c9974ba92..a9a75c8b6 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
@@ -17,15 +17,14 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
-import static org.briarproject.briar.mailbox.IntroduceeState.AWAIT_REMOTE_RESPONSE;
-import static org.briarproject.briar.mailbox.IntroduceeState.START;
+import static org.briarproject.briar.mailbox.MailboxState.START;
 
 @Immutable
 @NotNullByDefault
-class MailboxSession extends AbstractIntroduceeSession {
+class MailboxSession extends AbstractIntroduceeSession<MailboxState> {
 
 
-	MailboxSession(SessionId sessionId, IntroduceeState state,
+	MailboxSession(SessionId sessionId, MailboxState state,
 			long requestTimestamp, GroupId contactGroupId, Author introducer,
 			Local local, Remote remote, @Nullable byte[] masterKey,
 			@Nullable Map<TransportId, KeySetId> transportKeys,
@@ -41,50 +40,22 @@ class MailboxSession extends AbstractIntroduceeSession {
 		Remote remote =
 				new Remote(!localIsAlice, remoteAuthor, null, null, null, -1,
 						null);
-		return new MailboxSession(sessionId, START, -1, contactGroupId,
+		return new MailboxSession(sessionId, START, -1,
+				contactGroupId,
 				introducer, local, remote, null, null, 0);
 	}
 
 	static MailboxSession addLocalAccept(MailboxSession s,
-			IntroduceeState state, Message m, byte[] ephemeralPublicKey,
+			MailboxState state, Message m, byte[] ephemeralPublicKey,
 			byte[] ephemeralPrivateKey, long acceptTimestamp) {
 		Local local = new Local(s.local.alice, m.getId(), m.getTimestamp(),
 				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp, null);
 		return new MailboxSession(s.getSessionId(), state, m.getTimestamp(),
 				s.contactGroupId, s.introducer, local, s.remote, s.masterKey,
-				s.transportKeys, s.getMessageCounter() + 1);
+				s.transportKeys, s.getMessageCounter());
 	}
 
-	static MailboxSession addLocalAuth(MailboxSession s, IntroduceeState state,
-			Message m, SecretKey masterKey, SecretKey aliceMacKey,
-			SecretKey bobMacKey) {
-		// add mac key and sent message
-		Local local = new Local(s.local.alice, m.getId(), m.getTimestamp(),
-				s.local.ephemeralPublicKey, s.local.ephemeralPrivateKey,
-				s.local.acceptTimestamp,
-				s.local.alice ? aliceMacKey.getBytes() : bobMacKey.getBytes());
-		// just add the mac key
-		Remote remote = new Remote(s.remote.alice, s.remote.author,
-				s.remote.lastMessageId, s.remote.ephemeralPublicKey,
-				s.remote.transportProperties, s.remote.acceptTimestamp,
-				s.remote.alice ? aliceMacKey.getBytes() : bobMacKey.getBytes());
-		// add master key
-		return new MailboxSession(s.getSessionId(), state,
-				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
-				remote, masterKey.getBytes(), s.transportKeys,
-				s.getMessageCounter());
-	}
-
-	static MailboxSession awaitAuth(MailboxSession s, MailboxAuthMessage m,
-			Message sent, @Nullable Map<TransportId, KeySetId> transportKeys) {
-		Local local = new Local(s.local, sent.getId(), sent.getTimestamp());
-		Remote remote = new Remote(s.remote, m.getMessageId());
-		return new MailboxSession(s.getSessionId(), AWAIT_REMOTE_RESPONSE,
-				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
-				remote, null, transportKeys, s.getMessageCounter());
-	}
-
-	static MailboxSession clear(MailboxSession s, IntroduceeState state,
+	static MailboxSession clear(MailboxSession s, MailboxState state,
 			@Nullable MessageId lastLocalMessageId, long localTimestamp,
 			@Nullable MessageId lastRemoteMessageId) {
 		Local local =
@@ -98,6 +69,24 @@ class MailboxSession extends AbstractIntroduceeSession {
 				remote, null, null, s.getMessageCounter());
 	}
 
+	static MailboxSession addIntroduceeAccept(MailboxSession s,
+			MessageId lastMessageId,
+			byte[] ephemeralPublicKey, SecretKey masterKey,
+			SecretKey aliceMacKey,
+			SecretKey bobMacKey, long acceptTimestamp) {
+		Local local = new Local(s.local.alice, s.local.lastMessageId,
+				s.local.lastMessageTimestamp, s.local.ephemeralPublicKey,
+				s.local.ephemeralPrivateKey, s.local.acceptTimestamp,
+				aliceMacKey.getBytes());
+		Remote remote =
+				new Remote(false, s.remote.author, lastMessageId,
+						ephemeralPublicKey, null, acceptTimestamp,
+						bobMacKey.getBytes());
+		return new MailboxSession(s.getSessionId(), s.getState(),
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, masterKey.getBytes(), null, s.getMessageCounter());
+	}
+
 	@Override
 	Role getRole() {
 		return MAILBOX;
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java
index 0d42aefd7..b3b439dd8 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java
@@ -22,4 +22,7 @@ interface MailboxSessionParser {
 	MailboxSession parseMailboxSession(GroupId introducerGroupId,
 			BdfDictionary d) throws FormatException;
 
+	IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException;
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java
index b7beb49ff..e74ee5238 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java
@@ -22,6 +22,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.briar.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
 import static org.briarproject.briar.api.mailbox.Role.OWNER;
 import static org.briarproject.briar.api.mailbox.Role.fromValue;
 import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Local;
@@ -109,6 +110,25 @@ class MailboxSessionParserImpl implements MailboxSessionParser {
 	@Override
 	public MailboxSession parseMailboxSession(GroupId introducerGroupId,
 			BdfDictionary d) throws FormatException {
+		if (getRole(d) != MAILBOX) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		long sessionCounter = getSessionCounter(d);
+		MailboxState state = MailboxState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		Author introducer = getAuthor(d, SESSION_KEY_INTRODUCER);
+		Local local = parseLocal(d.getDictionary(SESSION_KEY_LOCAL));
+		Remote remote = parseRemote(d.getDictionary(SESSION_KEY_REMOTE));
+		byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY);
+		Map<TransportId, KeySetId> transportKeys = parseTransportKeys(
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS));
+		return new MailboxSession(sessionId, state, requestTimestamp,
+				introducerGroupId, introducer, local, remote, masterKey,
+				transportKeys, sessionCounter);
+	}
+
+	@Override
+	public IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException {
 		if (getRole(d) != INTRODUCEE) throw new IllegalArgumentException();
 		SessionId sessionId = getSessionId(d);
 		long sessionCounter = getSessionCounter(d);
@@ -120,7 +140,7 @@ class MailboxSessionParserImpl implements MailboxSessionParser {
 		byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY);
 		Map<TransportId, KeySetId> transportKeys = parseTransportKeys(
 				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS));
-		return new MailboxSession(sessionId, state, requestTimestamp,
+		return new IntroduceeSession(sessionId, state, requestTimestamp,
 				introducerGroupId, introducer, local, remote, masterKey,
 				transportKeys, sessionCounter);
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
index 6a44a0c4d..ecffd8110 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
 enum MailboxState implements State {
 
 	START(0),
-	AWAIT_LOCAL_RESPONSES(1),
+	AWAIT_LOCAL_RESPONSE(1),
 	LOCAL_DECLINED(2),
 	LOCAL_ACCEPTED(3),
 	AWAIT_REMOTE_RESPONSE(4),
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
index 0cfd4c22d..d54397a74 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
@@ -3,20 +3,26 @@ package org.briarproject.briar.mailbox;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactManager;
 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.IdentityManager;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.mailbox.event.MailboxIntroductionResponseReceivedEvent;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionSucceededEvent;
 
 import javax.inject.Inject;
 
+import static org.briarproject.briar.mailbox.OwnerState.ADDED;
+import static org.briarproject.briar.mailbox.OwnerState.AWAIT_AUTH_M;
 import static org.briarproject.briar.mailbox.OwnerState.AWAIT_RESPONSE_B;
 import static org.briarproject.briar.mailbox.OwnerState.AWAIT_RESPONSE_M;
 
@@ -29,10 +35,11 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 			MessageTracker messageTracker, IdentityManager identityManager,
 			MailboxMessageParser messageParser,
 			MailboxMessageEncoder messageEncoder, Clock clock,
-			MailboxIntroductionCrypto crypto) {
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
 		super(db, clientHelper, contactManager, contactGroupFactory,
 				messageTracker, identityManager, messageParser, messageEncoder,
-				clock, crypto);
+				clock, crypto, keyManager, transportPropertyManager);
 	}
 
 	@Override
@@ -74,7 +81,7 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 
 	@Override
 	public OwnerSession onMailboxAcceptMessage(Transaction txn, OwnerSession s,
-			IntroduceeRequestMessage m) throws DbException, FormatException {
+			MailboxAcceptMessage m) throws DbException, FormatException {
 		switch (s.getState()) {
 			case START:
 			case AWAIT_RESPONSE_M:
@@ -105,20 +112,22 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 	}
 
 	private OwnerSession onMailboxAccept(Transaction txn, OwnerSession s,
-			IntroduceeRequestMessage m) throws DbException {
+			MailboxAcceptMessage m) throws DbException {
 		// The dependency, if any, must be the last remote message
 		if (isInvalidDependency(s.getMailbox().lastRemoteMessageId,
 				m.getPreviousMessageId())) return abort(txn, s);
 		Message forward = sendIntroduceeRequestMessage(txn, s.getIntroducee(),
-				clock.currentTimeMillis(), m.getEphemeralPublicKey(),
-				m.getAcceptTimestamp(), s.getMessageCounter());
+				clock.currentTimeMillis(), s.getMailbox().author,
+				m.getEphemeralPublicKey(),
+				m.getAcceptTimestamp(),
+				s.getMessageCounter());
 		broadcastMailboxIntroductionResponseReceived(txn, s.getMailbox().author,
 				s.getIntroducee().author);
 		return new OwnerSession(s.getSessionId(), AWAIT_RESPONSE_B,
 				s.getRequestTimestamp(),
 				new Introducee(s.getMailbox(), m.getMessageId()),
 				new Introducee(s.getIntroducee(), forward),
-				s.getMessageCounter() + 1);
+				s.getMessageCounter());
 	}
 
 	@Override
@@ -128,9 +137,24 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 	}
 
 	@Override
-	public OwnerSession onAuthMessage(Transaction txn, OwnerSession session,
+	public OwnerSession onAuthMessage(Transaction txn, OwnerSession s,
 			MailboxAuthMessage m) throws DbException, FormatException {
-		return null;
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getMailbox().getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendMailboxAuthMessage(txn, s.getIntroducee(),
+				clock.currentTimeMillis(), m.getTransportProperties(),
+				m.getMac(), m.getSignature(), s.getMessageCounter());
+		Contact c = contactManager
+				.getContact(txn, s.getIntroducee().author.getId(),
+						identityManager.getLocalAuthor().getId());
+		db.setMailboxForContact(txn, c.getId(), null, c.getId());
+		txn.attach(new MailboxIntroductionSucceededEvent(c));
+		return new OwnerSession(s.getSessionId(), ADDED,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), m.getMessageId()),
+				new Introducee(s.getIntroducee(), forward),
+				s.getMessageCounter());
 	}
 
 	@Override
@@ -139,6 +163,26 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 		return null;
 	}
 
+	@Override
+	public OwnerSession onIntroduceeAcceptMessage(Transaction txn,
+			OwnerSession s, IntroduceeAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getIntroducee().getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendIntroduceeResponseMessage(txn, s.getMailbox(),
+				s.getMailbox().lastRemoteMessageId,
+				clock.currentTimeMillis(),
+				m.getEphemeralPublicKey(),
+				m.getMac(), m.getSignature(),
+				m.getAcceptTimestamp(),
+				s.getMessageCounter());
+		return new OwnerSession(s.getSessionId(), AWAIT_AUTH_M,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), forward),
+				new Introducee(s.getIntroducee(), m.getMessageId()),
+				s.getMessageCounter());
+	}
+
 	private OwnerSession onLocalRequest(Transaction txn, OwnerSession s,
 			long timestamp) throws DbException {
 		// Send REQUEST messages
@@ -172,5 +216,4 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 				new MailboxIntroductionResponseReceivedEvent(from, to);
 		txn.attach(e);
 	}
-
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
index ec7c9c448..d07b5ac22 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
@@ -8,7 +8,6 @@ import org.briarproject.briar.api.mailbox.Role;
 
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
 import static org.briarproject.briar.api.mailbox.Role.OWNER;
 
 @Immutable
@@ -32,6 +31,10 @@ class OwnerSession extends Session<OwnerState> {
 				new Introducee(sessionId, groupIdB, authorB), sessionCounter);
 	}
 
+	public static OwnerSession finished(OwnerSession s) {
+		return null;
+	}
+
 	@Override
 	Role getRole() {
 		return OWNER;
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
index edcd5b63e..2ab36fcc1 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
@@ -21,7 +21,7 @@ interface ProtocolEngine<S extends Session> {
 			throws DbException, FormatException;
 
 	S onMailboxAcceptMessage(Transaction txn, S session,
-			IntroduceeRequestMessage m)
+			MailboxAcceptMessage m)
 			throws DbException, FormatException;
 
 	S onIntroduceeRequestMessage(Transaction txn,
@@ -41,4 +41,6 @@ interface ProtocolEngine<S extends Session> {
 	S onAbortMessage(Transaction txn, S session, AbortMessage m)
 			throws DbException, FormatException;
 
+	S onIntroduceeAcceptMessage(Transaction txn, S session,
+			IntroduceeAcceptMessage acceptMessage) throws DbException;
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/Session.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/Session.java
index 5e7a75bac..ab65947ed 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/Session.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/Session.java
@@ -41,8 +41,4 @@ abstract class Session<S extends State> {
 		return messageCounter;
 	}
 
-	synchronized void increaseMessageCounter() {
-		messageCounter++;
-	}
-
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java
index d53af1730..52625831f 100644
--- a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java
@@ -7,6 +7,7 @@ import org.briarproject.briar.api.forum.ForumManager;
 import org.briarproject.briar.api.forum.ForumPost;
 import org.briarproject.briar.api.forum.ForumPostHeader;
 import org.briarproject.briar.api.forum.ForumSharingManager;
+import org.briarproject.briar.mailbox.DaggerMailboxIntroductionIntegrationTestComponent;
 import org.briarproject.briar.test.BriarIntegrationTest;
 import org.briarproject.briar.test.BriarIntegrationTestComponent;
 import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent;
@@ -69,6 +70,9 @@ public class ForumManagerTest
 		c2 = DaggerBriarIntegrationTestComponent.builder()
 				.testDatabaseModule(new TestDatabaseModule(t2Dir)).build();
 		injectEagerSingletons(c2);
+		cMailbox = DaggerMailboxIntroductionIntegrationTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t3Dir)).build();
+		injectEagerSingletons(cMailbox);
 	}
 
 	private ForumPost createForumPost(GroupId groupId,
diff --git a/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTest.java
index 403904d21..10ca4da97 100644
--- a/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTest.java
@@ -25,11 +25,10 @@ import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.introduction.IntroductionMessage;
 import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
-import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
-import org.briarproject.briar.api.introduction.event.IntroductionSucceededEvent;
 import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
 import org.briarproject.briar.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
 import org.briarproject.briar.api.mailbox.event.MailboxIntroductionResponseReceivedEvent;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionSucceededEvent;
 import org.briarproject.briar.test.BriarIntegrationTest;
 import org.briarproject.briar.test.BriarIntegrationTestComponent;
 import org.junit.Before;
@@ -145,6 +144,20 @@ public class MailboxIntroductionIntegrationTest extends
 		// sync/forward RESPONSE message to the contact
 		sync0To1(1, true);
 		eventWaiter.await(TIMEOUT, 1);
+		sync1To0(1, true);
+		sync0ToMailbox(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listenerMailbox.succeeded);
+		assertEquals(contact1From0.getAuthor(),
+				listenerMailbox.introduceeContact.getAuthor());
+		syncMailboxTo0(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener0.success);
+		sync0To1(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener1.succeeded);
+		assertEquals(privateMailboxFrom0.getAuthor(),
+				listener1.introduceeContact.getAuthor());
 	}
 
 
@@ -220,6 +233,7 @@ public class MailboxIntroductionIntegrationTest extends
 		private volatile boolean succeeded = false;
 		private volatile boolean answerRequests = true;
 		private volatile SessionId sessionId;
+		private volatile Contact introduceeContact;
 
 		private final int introducee;
 		private final boolean accept;
@@ -256,12 +270,14 @@ public class MailboxIntroductionIntegrationTest extends
 				// only broadcast for DECLINE messages in introducee role
 				latestEvent = e;
 				eventWaiter.resume();
-			} else if (e instanceof IntroductionSucceededEvent) {
+			} else if (e instanceof MailboxIntroductionSucceededEvent) {
 				latestEvent = e;
 				succeeded = true;
-				Contact contact = ((IntroductionSucceededEvent) e).getContact();
+				Contact contact =
+						((MailboxIntroductionSucceededEvent) e).getContact();
 				eventWaiter
 						.assertFalse(contact.getId().equals(contactId0From1));
+				introduceeContact = contact;
 				eventWaiter.resume();
 			} else if (e instanceof IntroductionAbortedEvent) {
 				latestEvent = e;
@@ -283,6 +299,7 @@ public class MailboxIntroductionIntegrationTest extends
 
 		private volatile boolean response1Received = false;
 		private volatile boolean response2Received = false;
+		private volatile boolean success = false;
 
 		@Override
 		public void eventOccurred(Event e) {
@@ -294,6 +311,10 @@ public class MailboxIntroductionIntegrationTest extends
 					response1Received = true;
 				}
 				eventWaiter.resume();
+			} else if (e instanceof MailboxIntroductionSucceededEvent) {
+				latestEvent = e;
+				success = true;
+				eventWaiter.resume();
 			}
 		}
 
diff --git a/mailbox-android/build.gradle b/mailbox-android/build.gradle
index 9484df25e..043812701 100644
--- a/mailbox-android/build.gradle
+++ b/mailbox-android/build.gradle
@@ -4,6 +4,7 @@ apply from: 'witness.gradle'
 
 dependencies {
 	implementation project(path: ':bramble-core', configuration: 'default')
+	implementation project(path: ':mailbox-core', configuration: 'default')
 	implementation project(':bramble-android')
 
 	def lifecycle_version = "1.1.1"
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AndroidComponent.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AndroidComponent.java
index 4d20692cf..ab916491e 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AndroidComponent.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AndroidComponent.java
@@ -23,6 +23,8 @@ import org.briarproject.bramble.api.settings.SettingsManager;
 import org.briarproject.bramble.api.system.AndroidExecutor;
 import org.briarproject.bramble.api.system.LocationUtils;
 import org.briarproject.bramble.plugin.tor.CircumventionProvider;
+import org.briarproject.briar.MailboxCoreEagerSingletons;
+import org.briarproject.briar.MailboxCoreModule;
 import org.briarproject.mailbox.android.login.SignInReminderReceiver;
 import org.briarproject.mailbox.android.reporting.BriarReportSender;
 import org.briarproject.mailbox.api.android.AndroidNotificationManager;
@@ -42,11 +44,12 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreModule.class,
 		BrambleAndroidModule.class,
+		MailboxCoreModule.class,
 		BriarAccountModule.class,
 		AppModule.class
 })
 public interface AndroidComponent
-		extends BrambleCoreEagerSingletons {
+		extends BrambleCoreEagerSingletons, MailboxCoreEagerSingletons {
 
 	// Exposed objects
 	@CryptoExecutor
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplicationImpl.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplicationImpl.java
index fe5842c1a..da14c0ecc 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplicationImpl.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplicationImpl.java
@@ -13,6 +13,7 @@ import org.acra.ACRA;
 import org.acra.ReportingInteractionMode;
 import org.acra.annotation.ReportsCrashes;
 import org.briarproject.bramble.BrambleCoreModule;
+import org.briarproject.briar.MailboxCoreModule;
 import org.briarproject.mailbox.R;
 import org.briarproject.mailbox.android.logging.CachingLogHandler;
 import org.briarproject.mailbox.android.reporting.BriarReportPrimer;
@@ -119,6 +120,7 @@ public class MailboxApplicationImpl extends Application
 		// We need to load the eager singletons directly after making the
 		// dependency graphs
 		BrambleCoreModule.initEagerSingletons(androidComponent);
+		MailboxCoreModule.initEagerSingletons(androidComponent);
 		AndroidEagerSingletons.initEagerSingletons(androidComponent);
 		return androidComponent;
 	}
diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle
new file mode 100644
index 000000000..6fb4231dc
--- /dev/null
+++ b/mailbox-core/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'java-library'
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+apply plugin: 'ru.vyarus.animalsniffer'
+apply plugin: 'net.ltgt.apt'
+apply plugin: 'idea'
+apply plugin: 'witness'
+apply from: 'witness.gradle'
+
+dependencies {
+	implementation project(path: ':briar-api', configuration: 'default')
+	implementation 'com.rometools:rome:1.7.3'
+	implementation 'org.jdom:jdom2:2.0.6'
+	implementation 'com.squareup.okhttp3:okhttp:3.8.0'
+	implementation 'org.jsoup:jsoup:1.10.3'
+
+	apt 'com.google.dagger:dagger-compiler:2.0.2'
+
+	testImplementation project(path: ':bramble-core', configuration: 'default')
+	testImplementation project(path: ':bramble-core', configuration: 'testOutput')
+	testImplementation project(path: ':bramble-api', configuration: 'testOutput')
+	testImplementation 'net.jodah:concurrentunit:0.4.2'
+	testImplementation 'junit:junit:4.12'
+	testImplementation "org.jmock:jmock:2.8.2"
+	testImplementation "org.jmock:jmock-junit4:2.8.2"
+	testImplementation "org.jmock:jmock-legacy:2.8.2"
+	testImplementation "org.hamcrest:hamcrest-library:1.3"
+	testImplementation "org.hamcrest:hamcrest-core:1.3"
+
+	testApt 'com.google.dagger:dagger-compiler:2.0.2'
+
+	signature 'org.codehaus.mojo.signature:java16:1.1@signature'
+}
+
+tasks.withType(Test) {
+	// Use entropy-gathering device specified on command line, if any
+	systemProperty 'java.security.egd', System.getProperty('java.security.egd')
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreEagerSingletons.java b/mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreEagerSingletons.java
new file mode 100644
index 000000000..7693b2121
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreEagerSingletons.java
@@ -0,0 +1,9 @@
+package org.briarproject.briar;
+
+import org.briarproject.briar.mailbox.MailboxIntroductionModule;
+
+public interface MailboxCoreEagerSingletons {
+
+	void inject(MailboxIntroductionModule.EagerSingletons init);
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreModule.java b/mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreModule.java
new file mode 100644
index 000000000..a82fafbbd
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/MailboxCoreModule.java
@@ -0,0 +1,15 @@
+package org.briarproject.briar;
+
+import org.briarproject.briar.mailbox.MailboxIntroductionModule;
+
+import dagger.Module;
+
+@Module(includes = {
+		MailboxIntroductionModule.class,
+})
+public class MailboxCoreModule {
+
+	public static void initEagerSingletons(MailboxCoreEagerSingletons c) {
+		c.inject(new MailboxIntroductionModule.EagerSingletons());
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java
new file mode 100644
index 000000000..eb326edb4
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java
@@ -0,0 +1,27 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class AbortMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+
+	protected AbortMessage(MessageId messageId, GroupId groupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java
new file mode 100644
index 000000000..6bf182a55
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractIntroduceeSession.java
@@ -0,0 +1,157 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class AbstractIntroduceeSession<S extends State> extends Session<S>
+		implements PeerSession {
+
+	final GroupId contactGroupId;
+	final Author introducer;
+	final Local local;
+	final Remote remote;
+	@Nullable
+	final byte[] masterKey;
+	@Nullable
+	final Map<TransportId, KeySetId> transportKeys;
+
+	AbstractIntroduceeSession(SessionId sessionId, S state,
+			long requestTimestamp, GroupId contactGroupId, Author introducer,
+			Local local, Remote remote, @Nullable byte[] masterKey,
+			@Nullable Map<TransportId, KeySetId> transportKeys,
+			long sessionCounter) {
+		super(sessionId, state, requestTimestamp, sessionCounter);
+		this.contactGroupId = contactGroupId;
+		this.introducer = introducer;
+		this.local = local;
+		this.remote = remote;
+		this.masterKey = masterKey;
+		this.transportKeys = transportKeys;
+	}
+
+	@Override
+	public GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	@Override
+	public long getLocalTimestamp() {
+		return local.lastMessageTimestamp;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastLocalMessageId() {
+		return local.lastMessageId;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return remote.lastMessageId;
+	}
+
+	Author getIntroducer() {
+		return introducer;
+	}
+
+	public Local getLocal() {
+		return local;
+	}
+
+	public Remote getRemote() {
+		return remote;
+	}
+
+	@Nullable
+	byte[] getMasterKey() {
+		return masterKey;
+	}
+
+	@Nullable
+	Map<TransportId, KeySetId> getTransportKeys() {
+		return transportKeys;
+	}
+
+	abstract static class Common {
+		final boolean alice;
+		@Nullable
+		final MessageId lastMessageId;
+		@Nullable
+		final byte[] ephemeralPublicKey;
+
+		final long acceptTimestamp;
+		@Nullable
+		final byte[] macKey;
+
+		private Common(boolean alice, @Nullable MessageId lastMessageId,
+				@Nullable byte[] ephemeralPublicKey, @Nullable
+				long acceptTimestamp, @Nullable byte[] macKey) {
+			this.alice = alice;
+			this.lastMessageId = lastMessageId;
+			this.ephemeralPublicKey = ephemeralPublicKey;
+			this.acceptTimestamp = acceptTimestamp;
+			this.macKey = macKey;
+		}
+	}
+
+	static class Local extends Common {
+		final long lastMessageTimestamp;
+		@Nullable
+		final byte[] ephemeralPrivateKey;
+
+		Local(boolean alice, @Nullable MessageId lastMessageId,
+				long lastMessageTimestamp, @Nullable byte[] ephemeralPublicKey,
+				@Nullable byte[] ephemeralPrivateKey, long acceptTimestamp,
+				@Nullable byte[] macKey) {
+			super(alice, lastMessageId, ephemeralPublicKey, acceptTimestamp,
+					macKey);
+			this.lastMessageTimestamp = lastMessageTimestamp;
+			this.ephemeralPrivateKey = ephemeralPrivateKey;
+		}
+
+		Local(Local s, @Nullable MessageId lastMessageId,
+				long lastMessageTimestamp) {
+			this(s.alice, lastMessageId, lastMessageTimestamp,
+					s.ephemeralPublicKey, s.ephemeralPrivateKey,
+					s.acceptTimestamp, s.macKey);
+		}
+	}
+
+	static class Remote extends Common {
+		final Author author;
+		@Nullable
+		final Map<TransportId, TransportProperties> transportProperties;
+
+		Remote(boolean alice, Author author,
+				@Nullable MessageId lastMessageId,
+				@Nullable byte[] ephemeralPublicKey, @Nullable
+				Map<TransportId, TransportProperties> transportProperties,
+				long acceptTimestamp, @Nullable byte[] macKey) {
+			super(alice, lastMessageId, ephemeralPublicKey, acceptTimestamp,
+					macKey);
+			this.author = author;
+			this.transportProperties = transportProperties;
+		}
+
+		Remote(Remote s, @Nullable MessageId lastMessageId) {
+			this(s.alice, s.author, lastMessageId, s.ephemeralPublicKey,
+					s.transportProperties, s.acceptTimestamp, s.macKey);
+		}
+
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java
new file mode 100644
index 000000000..376480baf
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java
@@ -0,0 +1,45 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class AbstractMailboxIntroductionMessage {
+
+	private final MessageId messageId;
+	private final GroupId groupId;
+	private final long timestamp;
+	@Nullable
+	private final MessageId previousMessageId;
+
+	AbstractMailboxIntroductionMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId) {
+		this.messageId = messageId;
+		this.groupId = groupId;
+		this.timestamp = timestamp;
+		this.previousMessageId = previousMessageId;
+	}
+
+	MessageId getMessageId() {
+		return messageId;
+	}
+
+	GroupId getGroupId() {
+		return groupId;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	@Nullable
+	MessageId getPreviousMessageId() {
+		return previousMessageId;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
new file mode 100644
index 000000000..042f062a0
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
@@ -0,0 +1,207 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.data.BdfDictionary;
+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.IdentityManager;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.mailbox.MessageType.ABORT;
+import static org.briarproject.briar.mailbox.MessageType.DECLINE;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_REQUEST;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_AUTH;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
+
+@Immutable
+@NotNullByDefault
+abstract class AbstractProtocolEngine<S extends Session>
+		implements ProtocolEngine<S> {
+
+	protected final DatabaseComponent db;
+	protected final ClientHelper clientHelper;
+	protected final ContactManager contactManager;
+	protected final ContactGroupFactory contactGroupFactory;
+	protected final IdentityManager identityManager;
+	protected final MailboxMessageParser messageParser;
+	protected final MailboxMessageEncoder messageEncoder;
+	protected final Clock clock;
+	protected final MailboxIntroductionCrypto crypto;
+	protected final KeyManager keyManager;
+	protected final TransportPropertyManager transportPropertyManager;
+
+	AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager,
+			MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto,
+			KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+		this.contactManager = contactManager;
+		this.contactGroupFactory = contactGroupFactory;
+		this.identityManager = identityManager;
+		this.messageParser = messageParser;
+		this.messageEncoder = messageEncoder;
+		this.clock = clock;
+		this.crypto = crypto;
+		this.keyManager = keyManager;
+		this.transportPropertyManager = transportPropertyManager;
+	}
+
+	Message sendMailboxRequestMessage(Transaction txn, PeerSession s,
+			long timestamp, Author introduceeAuthor, long messageCounter)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeRequestMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), introduceeAuthor,
+						messageCounter);
+		sendMessage(txn, MAILBOX_REQUEST, s.getSessionId(), m, messageCounter);
+		return m;
+	}
+
+	Message sendMailboxAcceptMessage(Transaction txn, PeerSession s,
+			long timestamp,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			long messageCounter) throws DbException {
+		Message m = messageEncoder
+				.encodeMailboxAcceptMessage(s.getContactGroupId(),
+						timestamp, s.getLastLocalMessageId(), s.getSessionId(),
+						MAILBOX_ACCEPT,
+						ephemeralPublicKey,
+						acceptTimestamp,
+						messageCounter);
+		sendMessage(txn, MAILBOX_ACCEPT, s.getSessionId(), m,
+				messageCounter);
+		return m;
+	}
+
+	Message sendIntroduceeRequestMessage(Transaction txn, PeerSession s,
+			long timestamp, Author author,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			long messageCounter) throws DbException {
+		Message m = messageEncoder
+				.encodeIntroduceeRequestMessage(s.getContactGroupId(),
+						timestamp, s.getLastLocalMessageId(), s.getSessionId(),
+						INTRODUCEE_REQUEST,
+						author,
+						ephemeralPublicKey,
+						acceptTimestamp,
+						messageCounter);
+		sendMessage(txn, INTRODUCEE_REQUEST, s.getSessionId(), m,
+				messageCounter);
+		return m;
+	}
+
+	Message sendIntroduceeResponseMessage(Transaction txn, PeerSession s,
+			MessageId previousMessage,
+			long timestamp, byte[] ephemeralPublicKey, byte[] mac,
+			byte[] signature, long acceptTimestamp, long messageCounter)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeIntroduceeAcceptMessage(s.getContactGroupId(),
+						timestamp,
+						previousMessage, s.getSessionId(),
+						ephemeralPublicKey, mac, signature, acceptTimestamp,
+						messageCounter);
+		sendMessage(txn, INTRODUCEE_ACCEPT, s.getSessionId(), m,
+				messageCounter);
+		return m;
+	}
+
+	Message sendMailboxAuthMessage(Transaction txn, PeerSession s,
+			long timestamp,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature, long messageCounter)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeMailboxAuthMessage(s.getContactGroupId(), timestamp,
+						s.getLastRemoteMessageId(), s.getSessionId(),
+						transportProperties, mac, signature, messageCounter);
+		sendMessage(txn, MAILBOX_AUTH, s.getSessionId(), m, messageCounter);
+		return m;
+	}
+
+	/*
+	Message sendMailboxInfotMessage(Transaction txn, PeerSession s,
+			long timestamp,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties,
+			boolean visible) throws DbException {
+		Message m = messageEncoder
+				.encodeMaiAcceptMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId(),
+						ephemeralPublicKey, acceptTimestamp,
+						transportProperties);
+		sendMessage(txn, MAILBOX_AUTH, s.getSessionId(), m);
+		return m;
+	}
+	*/
+
+	Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp,
+			boolean visible) throws DbException {
+		Message m = messageEncoder
+				.encodeDeclineMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId());
+		sendMessage(txn, DECLINE, s.getSessionId(), m, 0);
+		return m;
+	}
+
+	Message sendAbortMessage(Transaction txn, PeerSession s, long timestamp)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeAbortMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId());
+		sendMessage(txn, ABORT, s.getSessionId(), m, 0);
+		return m;
+	}
+
+	private void sendMessage(Transaction txn, MessageType type,
+			SessionId sessionId, Message m, long messageCounter)
+			throws DbException {
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, sessionId, m.getTimestamp(), true,
+						messageCounter);
+		try {
+			clientHelper.addLocalMessage(txn, m, meta, true);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	boolean isInvalidDependency(@Nullable MessageId lastRemoteMessageId,
+			@Nullable MessageId dependency) {
+		if (dependency == null) return lastRemoteMessageId != null;
+		return lastRemoteMessageId == null ||
+				!dependency.equals(lastRemoteMessageId);
+	}
+
+	long getLocalTimestamp(long localTimestamp, long requestTimestamp) {
+		return Math.max(clock.currentTimeMillis(),
+				Math.max(localTimestamp, requestTimestamp) + 1);
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/BdfIncomingMessageHook.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/BdfIncomingMessageHook.java
new file mode 100644
index 000000000..d3dd5b47f
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/BdfIncomingMessageHook.java
@@ -0,0 +1,66 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.data.MetadataParser;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.InvalidMessageException;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public abstract class BdfIncomingMessageHook implements IncomingMessageHook {
+
+	protected final DatabaseComponent db;
+	protected final ClientHelper clientHelper;
+	protected final MetadataParser metadataParser;
+
+	protected BdfIncomingMessageHook(DatabaseComponent db,
+			ClientHelper clientHelper, MetadataParser metadataParser) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+		this.metadataParser = metadataParser;
+	}
+
+	/**
+	 * Called once for each incoming message that passes validation.
+	 *
+	 * @return whether or not this message should be shared
+	 * @throws DbException Should only be used for real database errors.
+	 * If this is thrown, delivery will be attempted again at next startup,
+	 * whereas if a FormatException is thrown, the message will be permanently
+	 * invalidated.
+	 * @throws FormatException Use this for any non-database error
+	 * that occurs while handling remotely created data.
+	 * This includes errors that occur while handling locally created data
+	 * in a context controlled by remotely created data
+	 * (for example, parsing the metadata of a dependency
+	 * of an incoming message).
+	 * Never rethrow DbException as FormatException!
+	 */
+	protected abstract boolean incomingMessage(Transaction txn, Message m,
+			BdfList body, BdfDictionary meta) throws DbException,
+			FormatException;
+
+	@Override
+	public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
+			throws DbException, InvalidMessageException {
+		try {
+			BdfList body = clientHelper.toList(m);
+			BdfDictionary metaDictionary = metadataParser.parse(meta);
+			return incomingMessage(txn, m, body, metaDictionary);
+		} catch (FormatException e) {
+			throw new InvalidMessageException(e);
+		}
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java
new file mode 100644
index 000000000..8797283b4
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java
@@ -0,0 +1,52 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class ContactAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected ContactAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp, byte[] mac, byte[] signature) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java
new file mode 100644
index 000000000..676ee4500
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java
@@ -0,0 +1,42 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class ContactRequestMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+
+	protected ContactRequestMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp,
+			long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java
new file mode 100644
index 000000000..b7da465ea
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java
@@ -0,0 +1,28 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class DeclineMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+
+	protected DeclineMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java
new file mode 100644
index 000000000..d4459605f
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java
@@ -0,0 +1,77 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+final class Introducee implements PeerSession {
+	final SessionId sessionId;
+	final GroupId groupId;
+	final Author author;
+	final long localTimestamp;
+	@Nullable
+	final MessageId lastLocalMessageId, lastRemoteMessageId;
+
+	Introducee(SessionId sessionId, GroupId groupId, Author author,
+			long localTimestamp,
+			@Nullable MessageId lastLocalMessageId,
+			@Nullable MessageId lastRemoteMessageId) {
+		this.sessionId = sessionId;
+		this.groupId = groupId;
+		this.localTimestamp = localTimestamp;
+		this.author = author;
+		this.lastLocalMessageId = lastLocalMessageId;
+		this.lastRemoteMessageId = lastRemoteMessageId;
+	}
+
+	Introducee(Introducee i, Message sent) {
+		this(i.sessionId, i.groupId, i.author, sent.getTimestamp(),
+				sent.getId(), i.lastRemoteMessageId);
+	}
+
+	Introducee(Introducee i, MessageId remoteMessageId) {
+		this(i.sessionId, i.groupId, i.author, i.localTimestamp,
+				i.lastLocalMessageId, remoteMessageId);
+	}
+
+	Introducee(SessionId sessionId, GroupId groupId,
+			Author author) {
+		this(sessionId, groupId, author, -1, null, null);
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	@Override
+	public GroupId getContactGroupId() {
+		return groupId;
+	}
+
+	@Override
+	public long getLocalTimestamp() {
+		return localTimestamp;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastLocalMessageId() {
+		return lastLocalMessageId;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return lastRemoteMessageId;
+	}
+
+}
+
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java
new file mode 100644
index 000000000..ebb481692
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeAcceptMessage.java
@@ -0,0 +1,52 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class IntroduceeAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected IntroduceeAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey, byte[] mac,
+			byte[] signature, long acceptTimestamp) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java
new file mode 100644
index 000000000..78153f39b
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeProtocolEngine.java
@@ -0,0 +1,274 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.data.BdfDictionary;
+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.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionSucceededEvent;
+
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.briar.mailbox.IntroduceeState.AWAIT_AUTH;
+import static org.briarproject.briar.mailbox.IntroduceeState.LOCAL_ACCEPTED;
+import static org.briarproject.briar.mailbox.IntroduceeState.MAILBOX_ADDED;
+
+class IntroduceeProtocolEngine
+		extends AbstractProtocolEngine<IntroduceeSession> {
+
+	private final static Logger LOG =
+			Logger.getLogger(IntroduceeProtocolEngine.class.getName());
+
+	@Inject
+	IntroduceeProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager,
+			MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		super(db, clientHelper, contactManager, contactGroupFactory,
+				identityManager, messageParser, messageEncoder,
+				clock, crypto, keyManager, transportPropertyManager);
+	}
+
+	@Override
+	public IntroduceeSession onRequestAction(Transaction txn,
+			IntroduceeSession session, long timestamp) throws DbException {
+		return null;
+	}
+
+	@Override
+	public IntroduceeSession onAcceptAction(Transaction txn,
+			IntroduceeSession session, long timestamp) throws DbException {
+		return null;
+	}
+
+	@Override
+	public IntroduceeSession onDeclineAction(Transaction txn,
+			IntroduceeSession session, long timestamp) throws DbException {
+		return null;
+	}
+
+	@Override
+	public IntroduceeSession onRequestMessage(Transaction txn,
+			IntroduceeSession session, RequestMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public IntroduceeSession onMailboxAcceptMessage(Transaction txn,
+			IntroduceeSession session, MailboxAcceptMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public IntroduceeSession onIntroduceeRequestMessage(Transaction txn,
+			IntroduceeSession session, IntroduceeRequestMessage m)
+			throws DbException, FormatException {
+		switch (session.getState()) {
+			case START:
+				return onRemoteRequest(txn, session, m);
+			case AWAIT_LOCAL_RESPONSE:
+			case LOCAL_DECLINED:
+			case LOCAL_ACCEPTED:
+			case AWAIT_REMOTE_RESPONSE:
+			case MAILBOX_ADDED:
+				return abort(txn, session);
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private IntroduceeSession onRemoteRequest(Transaction txn,
+			IntroduceeSession s,
+			IntroduceeRequestMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+
+		// Add SessionId to message metadata
+		addSessionId(txn, m.getMessageId(), s.getSessionId());
+
+		// Broadcast IntroductionRequestReceivedEvent
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		txn.attach(new MailboxIntroductionRequestReceivedEvent(
+				localAuthor.getId()));
+
+		// Create ephemeral key pair and get local transport properties
+		KeyPair keyPair = crypto.generateKeyPair();
+		byte[] publicKey = keyPair.getPublic().getEncoded();
+		byte[] privateKey = keyPair.getPrivate().getEncoded();
+		long localTimestamp = clock.currentTimeMillis();
+		try {
+			SecretKey secretKey = crypto.deriveMasterKey(publicKey, privateKey,
+					m.getEphemeralPublicKey(), false);
+			SecretKey aliceMacKey = crypto.deriveMacKey(secretKey, true);
+			SecretKey bobMacKey = crypto.deriveMacKey(secretKey, false);
+
+			s = IntroduceeSession
+					.addLocalAccept(s, LOCAL_ACCEPTED, m, publicKey, privateKey,
+							localTimestamp, secretKey.getBytes(),
+							aliceMacKey.getBytes(), bobMacKey.getBytes());
+
+			byte[] mac = crypto.authMac(bobMacKey, s, localAuthor.getId());
+			byte[] signature =
+					crypto.sign(bobMacKey, localAuthor.getPrivateKey());
+
+			// Send ephemeral public key and timestamp back
+			Message reply =
+					sendIntroduceeResponseMessage(txn, s, m.getMessageId(),
+							localTimestamp,
+							publicKey, mac, signature, localTimestamp,
+							s.getMessageCounter());
+			//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
+			// Move to the AWAIT_REMOTE_RESPONSE state
+			return IntroduceeSession
+					.addLocalAuth(s, AWAIT_AUTH, reply);
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+	}
+
+	private IntroduceeSession abort(Transaction txn, IntroduceeSession s)
+			throws DbException {
+		/*
+		// Mark the request message unavailable to answer
+		markRequestsUnavailableToAnswer(txn, s);
+
+		// Send an ABORT message
+		Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s));
+
+		// Broadcast abort event for testing
+		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
+
+		// Reset the session back to initial state
+		return IntroduceeSession.clear(s, START, sent.getId(),
+				sent.getTimestamp(), s.getLastRemoteMessageId());
+				*/
+		return null;
+	}
+
+	@Override
+	public IntroduceeSession onContactAcceptMessage(Transaction txn,
+			IntroduceeSession session, IntroduceeRequestMessage m)
+			throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public IntroduceeSession onDeclineMessage(Transaction txn,
+			IntroduceeSession session, DeclineMessage m)
+			throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public IntroduceeSession onAuthMessage(Transaction txn,
+			IntroduceeSession s,
+			MailboxAuthMessage m) throws DbException, FormatException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getLastLocalMessageId(),
+				m.getPreviousMessageId()))
+			return abort(txn, s);
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		try {
+			crypto.verifyAuthMac(m.getMac(), s, localAuthor.getId());
+			crypto.verifySignature(m.getSignature(), s);
+			long timestamp = Math.min(s.getLocal().acceptTimestamp,
+					s.getRemote().acceptTimestamp);
+			if (timestamp == -1) throw new AssertionError();
+			contactManager
+					.addContact(txn, s.getRemote().author, localAuthor.getId(),
+							false, true);
+			// Only add transport properties and keys when the contact was added
+			// This will be changed once we have a way to reset state for peers
+			// that were contacts already at some point in the past.
+			Contact c = contactManager
+					.getContact(txn, s.getRemote().author.getId(),
+							localAuthor.getId());
+			// add the keys to the new contact
+			//noinspection ConstantConditions
+			Map<TransportId, KeySetId> keys = keyManager
+					.addContact(txn, c.getId(), new SecretKey(s.masterKey),
+							timestamp, s.getLocal().alice, true);
+			// add signed transport properties for the contact
+			//noinspection ConstantConditions
+			transportPropertyManager.addRemoteProperties(txn, c.getId(),
+					m.getTransportProperties());
+			// Broadcast MailboxIntroductionSucceededEvent, because contact got added
+			MailboxIntroductionSucceededEvent e =
+					new MailboxIntroductionSucceededEvent(c);
+			txn.attach(e);
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+		return IntroduceeSession
+				.clear(s, MAILBOX_ADDED, s.getLastLocalMessageId(),
+						clock.currentTimeMillis(), m.getMessageId());
+	}
+
+	@Override
+	public IntroduceeSession onAbortMessage(Transaction txn,
+			IntroduceeSession session, AbortMessage m)
+			throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public IntroduceeSession onIntroduceeAcceptMessage(Transaction txn,
+			IntroduceeSession session, IntroduceeAcceptMessage acceptMessage) {
+		throw new UnsupportedOperationException();
+	}
+
+
+	private boolean isInvalidDependency(AbstractIntroduceeSession s,
+			@Nullable MessageId dependency) {
+		return isInvalidDependency(s.getLastRemoteMessageId(), dependency);
+	}
+
+	private long getLocalTimestamp(AbstractIntroduceeSession s) {
+		return getLocalTimestamp(s.getLocalTimestamp(),
+				s.getRequestTimestamp());
+	}
+
+	private void addSessionId(Transaction txn, MessageId m, SessionId sessionId)
+			throws DbException {
+		BdfDictionary meta = new BdfDictionary();
+		messageEncoder.addSessionId(meta, sessionId);
+		try {
+			clientHelper.mergeMessageMetadata(txn, m, meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java
new file mode 100644
index 000000000..3e5ea3d3e
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeRequestMessage.java
@@ -0,0 +1,48 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class IntroduceeRequestMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final Author author;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+
+	protected IntroduceeRequestMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, Author author, byte[] ephemeralPublicKey,
+			long acceptTimestamp) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.author = author;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public Author getAuthor() {
+		return author;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
new file mode 100644
index 000000000..29270beea
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
@@ -0,0 +1,151 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.Role;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.briar.mailbox.IntroduceeState.AWAIT_REMOTE_RESPONSE;
+import static org.briarproject.briar.mailbox.IntroduceeState.START;
+
+@Immutable
+@NotNullByDefault
+class IntroduceeSession extends AbstractIntroduceeSession<IntroduceeState> {
+
+
+	IntroduceeSession(SessionId sessionId, IntroduceeState state,
+			long requestTimestamp, GroupId contactGroupId, Author introducer,
+			Local local, Remote remote, @Nullable byte[] masterKey,
+			@Nullable Map<TransportId, KeySetId> transportKeys,
+			long sessionCounter) {
+		super(sessionId, state, requestTimestamp, contactGroupId, introducer,
+				local, remote, masterKey, transportKeys, sessionCounter);
+	}
+
+	static IntroduceeSession getInitial(GroupId contactGroupId,
+			SessionId sessionId, Author introducer, boolean localIsAlice,
+			Author remoteAuthor) {
+		Local local = new Local(localIsAlice, null, -1, null, null, -1, null);
+		Remote remote =
+				new Remote(!localIsAlice, remoteAuthor, null, null, null, -1,
+						null);
+		return new IntroduceeSession(sessionId, START, -1, contactGroupId,
+				introducer, local, remote, null, null, 0);
+	}
+
+	static IntroduceeSession addLocalAccept(IntroduceeSession s,
+			IntroduceeState state, IntroduceeRequestMessage m,
+			byte[] ephemeralPublicKey,
+			byte[] ephemeralPrivateKey, long acceptTimestamp, byte[] masterKey,
+			byte[] aliceMackey, byte[] bobMacKey) {
+		Local local = new Local(false, s.local.lastMessageId,
+				s.local.lastMessageTimestamp,
+				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp,
+				bobMacKey);
+		Remote remote = new Remote(true, m.getAuthor(), m.getMessageId(),
+				m.getEphemeralPublicKey(), null, m.getAcceptTimestamp(),
+				aliceMackey);
+		return new IntroduceeSession(s.getSessionId(), state, m.getTimestamp(),
+				s.contactGroupId, s.introducer, local, remote, masterKey,
+				s.transportKeys, s.getMessageCounter());
+	}
+
+	static IntroduceeSession addLocalAuth(IntroduceeSession s,
+			IntroduceeState state,
+			Message m) {
+		// add mac key and sent message
+		Local local = new Local(false, m.getId(), m.getTimestamp(),
+				s.local.ephemeralPublicKey, s.local.ephemeralPrivateKey,
+				s.local.acceptTimestamp, s.local.macKey);
+
+		return new IntroduceeSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				s.remote, s.masterKey, s.transportKeys,
+				s.getMessageCounter());
+	}
+
+	static IntroduceeSession awaitAuth(IntroduceeSession s,
+			MailboxAuthMessage m,
+			Message sent, @Nullable Map<TransportId, KeySetId> transportKeys) {
+		Local local = new Local(s.local, sent.getId(), sent.getTimestamp());
+		Remote remote = new Remote(s.remote, m.getMessageId());
+		return new IntroduceeSession(s.getSessionId(), AWAIT_REMOTE_RESPONSE,
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, null, transportKeys, s.getMessageCounter());
+	}
+
+	static IntroduceeSession clear(IntroduceeSession s, IntroduceeState state,
+			@Nullable MessageId lastLocalMessageId, long localTimestamp,
+			@Nullable MessageId lastRemoteMessageId) {
+		Local local =
+				new Local(s.local.alice, lastLocalMessageId, localTimestamp,
+						null, null, -1, null);
+		Remote remote =
+				new Remote(s.remote.alice, s.remote.author, lastRemoteMessageId,
+						null, null, -1, null);
+		return new IntroduceeSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, null, null, s.getMessageCounter());
+	}
+
+	@Override
+	Role getRole() {
+		return INTRODUCEE;
+	}
+
+	@Override
+	public GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	@Override
+	public long getLocalTimestamp() {
+		return local.lastMessageTimestamp;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastLocalMessageId() {
+		return local.lastMessageId;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return remote.lastMessageId;
+	}
+
+	Author getIntroducer() {
+		return introducer;
+	}
+
+	public Local getLocal() {
+		return local;
+	}
+
+	public Remote getRemote() {
+		return remote;
+	}
+
+	@Nullable
+	byte[] getMasterKey() {
+		return masterKey;
+	}
+
+	@Nullable
+	Map<TransportId, KeySetId> getTransportKeys() {
+		return transportKeys;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java
new file mode 100644
index 000000000..13b2bc316
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java
@@ -0,0 +1,36 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum IntroduceeState implements State {
+
+	START(0),
+	AWAIT_LOCAL_RESPONSE(1),
+	LOCAL_DECLINED(2),
+	LOCAL_ACCEPTED(3),
+	AWAIT_REMOTE_RESPONSE(4),
+	AWAIT_AUTH(5),
+	MAILBOX_ADDED(6);
+
+	private final int value;
+
+	IntroduceeState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static IntroduceeState fromValue(int value) throws FormatException {
+		for (IntroduceeState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java
new file mode 100644
index 000000000..3e2d61274
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java
@@ -0,0 +1,49 @@
+package org.briarproject.briar.mailbox;
+
+interface IntroductionConstants {
+
+	// Group metadata keys
+	String GROUP_KEY_CONTACT_ID = "contactId";
+
+	// Message metadata keys
+	String MSG_KEY_MESSAGE_TYPE = "messageType";
+	String MSG_KEY_SESSION_ID = "sessionId";
+	String MSG_KEY_TIMESTAMP = "timestamp";
+	String MSG_KEY_LOCAL = "local";
+	String MSG_KEY_COUNTER = "counter";
+	String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
+
+	// Session Keys
+	String SESSION_KEY_SESSION_ID = "sessionId";
+	String SESSION_KEY_COUNTER = "counter";
+	String SESSION_KEY_ROLE = "role";
+	String SESSION_KEY_STATE = "state";
+	String SESSION_KEY_REQUEST_TIMESTAMP = "requestTimestamp";
+	String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp";
+	String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
+	String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId";
+
+	// Session Keys Introducer
+	String SESSION_KEY_INTRODUCEE_A = "introduceeA";
+	String SESSION_KEY_INTRODUCEE_B = "introduceeB";
+	String SESSION_KEY_GROUP_ID = "groupId";
+	String SESSION_KEY_AUTHOR = "author";
+
+	// Session Keys Introducee
+	String SESSION_KEY_INTRODUCER = "introducer";
+	String SESSION_KEY_LOCAL = "local";
+	String SESSION_KEY_REMOTE = "remote";
+
+	String SESSION_KEY_MASTER_KEY = "masterKey";
+	String SESSION_KEY_TRANSPORT_KEYS = "transportKeys";
+
+	String SESSION_KEY_ALICE = "alice";
+	String SESSION_KEY_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey";
+	String SESSION_KEY_EPHEMERAL_PRIVATE_KEY = "ephemeralPrivateKey";
+	String SESSION_KEY_TRANSPORT_PROPERTIES = "transportProperties";
+	String SESSION_KEY_ACCEPT_TIMESTAMP = "acceptTimestamp";
+	String SESSION_KEY_MAC_KEY = "macKey";
+
+	String SESSION_KEY_REMOTE_AUTHOR = "remoteAuthor";
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
new file mode 100644
index 000000000..6b740de82
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
@@ -0,0 +1,41 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MailboxAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+
+	protected MailboxAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
new file mode 100644
index 000000000..b66882a70
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
@@ -0,0 +1,51 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MailboxAuthMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final Map<TransportId, TransportProperties> transportProperties;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected MailboxAuthMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.transportProperties = transportProperties;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+
+	public Map<TransportId, TransportProperties> getTransportProperties() {
+		return transportProperties;
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
new file mode 100644
index 000000000..21a39bfbb
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
@@ -0,0 +1,100 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.security.GeneralSecurityException;
+
+interface MailboxIntroductionCrypto {
+
+	/**
+	 * Returns the {@link SessionId} based on the introducer
+	 * and the two introducees.
+	 */
+	SessionId getSessionId(Author introducer, Author local, Author remote,
+			boolean isAlice);
+
+	/**
+	 * Generates an agreement key pair.
+	 */
+	KeyPair generateKeyPair();
+
+	/**
+	 * Derives a session master key for Alice or Bob.
+	 *
+	 * @return The secret master key
+	 */
+	SecretKey deriveMasterKey(AbstractIntroduceeSession s)
+			throws GeneralSecurityException;
+
+	@SuppressWarnings("ConstantConditions")
+	SecretKey deriveMasterKey(byte[] ephemeralPublicKey,
+			byte[] ephemeralPrivateKey, byte[] remoteEphemeralPublicKey,
+			boolean alice)
+			throws GeneralSecurityException;
+
+	/**
+	 * Derives a MAC key from the session's master key for Alice or Bob.
+	 *
+	 * @param masterKey The key returned by {@link #deriveMasterKey(AbstractIntroduceeSession)}
+	 * @param alice true for Alice's MAC key, false for Bob's
+	 * @return The MAC key
+	 */
+	SecretKey deriveMacKey(SecretKey masterKey, boolean alice);
+
+	/**
+	 * Generates a MAC that covers both introducee's ephemeral public keys,
+	 * transport properties, Author IDs and timestamps of the accept message.
+	 */
+	byte[] authMac(SecretKey macKey, AbstractIntroduceeSession s,
+			AuthorId localAuthorId);
+
+	/**
+	 * Verifies a received MAC
+	 *
+	 * @param mac The MAC to verify
+	 * as returned by {@link #deriveMasterKey(AbstractIntroduceeSession)}
+	 * @throws GeneralSecurityException if the verification fails
+	 */
+	void verifyAuthMac(byte[] mac, AbstractIntroduceeSession s,
+			AuthorId localAuthorId)
+			throws GeneralSecurityException;
+
+	/**
+	 * Signs a nonce derived from the macKey
+	 * with the local introducee's identity private key.
+	 *
+	 * @param macKey The corresponding MAC key for the signer's role
+	 * @param privateKey The identity private key
+	 * (from {@link LocalAuthor#getPrivateKey()})
+	 * @return The signature as a byte array
+	 */
+	byte[] sign(SecretKey macKey, byte[] privateKey)
+			throws GeneralSecurityException;
+
+	/**
+	 * Verifies the signature on a nonce derived from the MAC key.
+	 *
+	 * @throws GeneralSecurityException if the signature is invalid
+	 */
+	void verifySignature(byte[] signature, AbstractIntroduceeSession s)
+			throws GeneralSecurityException;
+
+	/**
+	 * Generates a MAC using the local MAC key.
+	 */
+	byte[] activateMac(AbstractIntroduceeSession s);
+
+	/**
+	 * Verifies a MAC from an ACTIVATE message.
+	 *
+	 * @throws GeneralSecurityException if the verification fails
+	 */
+	void verifyActivateMac(byte[] mac, AbstractIntroduceeSession s)
+			throws GeneralSecurityException;
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
new file mode 100644
index 000000000..0de274163
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
@@ -0,0 +1,232 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.KeyParser;
+import org.briarproject.bramble.api.crypto.PrivateKey;
+import org.briarproject.bramble.api.crypto.PublicKey;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.security.GeneralSecurityException;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_ACTIVATE_MAC;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_ALICE_MAC_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_MAC;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_NONCE;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_SIGN;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_BOB_MAC_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_MASTER_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_SESSION_ID;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Common;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Local;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Remote;
+
+@Immutable
+@NotNullByDefault
+class MailboxIntroductionCryptoImpl implements MailboxIntroductionCrypto {
+
+	private final CryptoComponent crypto;
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxIntroductionCryptoImpl(
+			CryptoComponent crypto,
+			ClientHelper clientHelper) {
+		this.crypto = crypto;
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public SessionId getSessionId(Author introducer, Author local,
+			Author remote, boolean isAlice) {
+		byte[] hash = crypto.hash(
+				LABEL_SESSION_ID,
+				introducer.getId().getBytes(),
+				isAlice ? local.getId().getBytes() : remote.getId().getBytes(),
+				isAlice ? remote.getId().getBytes() : local.getId().getBytes()
+		);
+		return new SessionId(hash);
+	}
+
+	@Override
+	public KeyPair generateKeyPair() {
+		return crypto.generateAgreementKeyPair();
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public SecretKey deriveMasterKey(AbstractIntroduceeSession s)
+			throws GeneralSecurityException {
+		return deriveMasterKey(
+				s.getLocal().ephemeralPublicKey,
+				s.getLocal().ephemeralPrivateKey,
+				s.getRemote().ephemeralPublicKey,
+				s.getLocal().alice
+		);
+	}
+
+	@Override
+	public SecretKey deriveMasterKey(byte[] publicKey, byte[] privateKey,
+			byte[] remotePublicKey, boolean alice)
+			throws GeneralSecurityException {
+		KeyParser kp = crypto.getAgreementKeyParser();
+		PublicKey remoteEphemeralPublicKey = kp.parsePublicKey(remotePublicKey);
+		PublicKey ephemeralPublicKey = kp.parsePublicKey(publicKey);
+		PrivateKey ephemeralPrivateKey = kp.parsePrivateKey(privateKey);
+		KeyPair keyPair = new KeyPair(ephemeralPublicKey, ephemeralPrivateKey);
+		return crypto.deriveSharedSecret(
+				LABEL_MASTER_KEY,
+				remoteEphemeralPublicKey,
+				keyPair,
+				new byte[] {MAJOR_VERSION},
+				alice ? publicKey : remotePublicKey,
+				alice ? remotePublicKey : publicKey
+		);
+	}
+
+	@Override
+	public SecretKey deriveMacKey(SecretKey masterKey, boolean alice) {
+		return crypto.deriveKey(
+				alice ? LABEL_ALICE_MAC_KEY : LABEL_BOB_MAC_KEY,
+				masterKey
+		);
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public byte[] authMac(SecretKey macKey, AbstractIntroduceeSession s,
+			AuthorId localAuthorId) {
+		// the macKey is not yet available in the session at this point
+		return authMac(macKey, s.getIntroducer().getId(), localAuthorId,
+				s.getLocal(), s.getRemote());
+	}
+
+	byte[] authMac(SecretKey macKey, AuthorId introducerId,
+			AuthorId localAuthorId, Local local, Remote remote) {
+		byte[] inputs = getAuthMacInputs(introducerId, localAuthorId, local,
+				remote.author.getId(), remote);
+		return crypto.mac(
+				LABEL_AUTH_MAC,
+				macKey,
+				inputs
+		);
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public void verifyAuthMac(byte[] mac, AbstractIntroduceeSession s,
+			AuthorId localAuthorId) throws GeneralSecurityException {
+		verifyAuthMac(mac, new SecretKey(s.getRemote().macKey),
+				s.getIntroducer().getId(), localAuthorId, s.getLocal(),
+				s.getRemote().author.getId(), s.getRemote());
+	}
+
+	void verifyAuthMac(byte[] mac, SecretKey macKey, AuthorId introducerId,
+			AuthorId localAuthorId, Common local, AuthorId remoteAuthorId,
+			Common remote) throws GeneralSecurityException {
+		// switch input for verification
+		byte[] inputs = getAuthMacInputs(introducerId, remoteAuthorId, remote,
+				localAuthorId, local);
+		if (!crypto.verifyMac(mac, LABEL_AUTH_MAC, macKey, inputs)) {
+			throw new GeneralSecurityException();
+		}
+	}
+
+	@SuppressWarnings("ConstantConditions")
+	private byte[] getAuthMacInputs(AuthorId introducerId,
+			AuthorId localAuthorId, Common local, AuthorId remoteAuthorId,
+			Common remote) {
+		BdfList localInfo = BdfList.of(
+				localAuthorId,
+				local.acceptTimestamp,
+				local.ephemeralPublicKey
+		);
+		BdfList remoteInfo = BdfList.of(
+				remoteAuthorId,
+				remote.acceptTimestamp,
+				remote.ephemeralPublicKey
+		);
+		BdfList macList = BdfList.of(
+				introducerId,
+				localInfo,
+				remoteInfo
+		);
+		try {
+			return clientHelper.toByteArray(macList);
+		} catch (FormatException e) {
+			throw new AssertionError();
+		}
+	}
+
+	@Override
+	public byte[] sign(SecretKey macKey, byte[] privateKey)
+			throws GeneralSecurityException {
+		return crypto.sign(
+				LABEL_AUTH_SIGN,
+				getNonce(macKey),
+				privateKey
+		);
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public void verifySignature(byte[] signature, AbstractIntroduceeSession s)
+			throws GeneralSecurityException {
+		SecretKey macKey = new SecretKey(s.getRemote().macKey);
+		verifySignature(macKey, s.getRemote().author.getPublicKey(), signature);
+	}
+
+	void verifySignature(SecretKey macKey, byte[] publicKey,
+			byte[] signature) throws GeneralSecurityException {
+		byte[] nonce = getNonce(macKey);
+		if (!crypto.verifySignature(signature, LABEL_AUTH_SIGN, nonce,
+				publicKey)) {
+			throw new GeneralSecurityException();
+		}
+	}
+
+	private byte[] getNonce(SecretKey macKey) {
+		return crypto.mac(LABEL_AUTH_NONCE, macKey);
+	}
+
+	@Override
+	public byte[] activateMac(AbstractIntroduceeSession s) {
+		if (s.getLocal().macKey == null)
+			throw new AssertionError("Local MAC key is null");
+		return activateMac(new SecretKey(s.getLocal().macKey));
+	}
+
+	byte[] activateMac(SecretKey macKey) {
+		return crypto.mac(
+				LABEL_ACTIVATE_MAC,
+				macKey
+		);
+	}
+
+	@Override
+	public void verifyActivateMac(byte[] mac, AbstractIntroduceeSession s)
+			throws GeneralSecurityException {
+		if (s.getRemote().macKey == null)
+			throw new AssertionError("Remote MAC key is null");
+		verifyActivateMac(mac, new SecretKey(s.getRemote().macKey));
+	}
+
+	void verifyActivateMac(byte[] mac, SecretKey macKey)
+			throws GeneralSecurityException {
+		if (!crypto.verifyMac(mac, LABEL_ACTIVATE_MAC, macKey)) {
+			throw new GeneralSecurityException();
+		}
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
new file mode 100644
index 000000000..f7f58897e
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
@@ -0,0 +1,490 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+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.ContactType;
+import org.briarproject.bramble.api.contact.PrivateMailbox;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.data.MetadataParser;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.mailbox.MailboxInfo;
+import org.briarproject.bramble.api.sync.Client;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
+import org.briarproject.briar.api.mailbox.Role;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.contact.ContactManager.ContactHook;
+import static org.briarproject.bramble.api.contact.ContactType.MAILBOX_OWNER;
+import static org.briarproject.bramble.api.contact.ContactType.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.api.contact.ContactType.values;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
+import static org.briarproject.briar.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
+import static org.briarproject.briar.api.mailbox.Role.OWNER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.GROUP_KEY_CONTACT_ID;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_REQUEST;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
+
+class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
+		implements MailboxIntroductionManager, Client, ClientVersioningHook,
+		ContactHook {
+
+	private static final Logger LOG =
+			Logger.getLogger(MailboxIntroductionManagerImpl.class.getName());
+	private final Executor ioExecutor;
+	private final ClientVersioningManager clientVersioningManager;
+	private final ContactGroupFactory contactGroupFactory;
+	private final ContactManager contactManager;
+	private final MailboxMessageParser messageParser;
+	//	private final SessionEncoder sessionEncoder;
+	private final MailboxSessionParser sessionParser;
+	private final OwnerProtocolEngine ownerProtocolEngine;
+	private final IntroduceeProtocolEngine introduceeProtocolEngine;
+	private final MailboxSessionEncoder sessionEncoder;
+	private final MailboxIntroductionCrypto crypto;
+	private final MailboxProtocolEngine mailboxProtocolEngine;
+	private final IdentityManager identityManager;
+	private final Clock clock;
+
+	private final Group localGroup;
+
+	@Inject
+	MailboxIntroductionManagerImpl(@IoExecutor Executor ioExecutor,
+			DatabaseComponent db,
+			ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
+			MetadataParser metadataParser,
+			ContactGroupFactory contactGroupFactory,
+			ContactManager contactManager, MailboxMessageParser messageParser,
+			MailboxSessionParser sessionParser,
+			OwnerProtocolEngine ownerProtocolEngine,
+			IntroduceeProtocolEngine introduceeProtocolEngine,
+			MailboxSessionEncoder sessionEncoder,
+			MailboxIntroductionCrypto crypto,
+			MailboxProtocolEngine mailboxProtocolEngine,
+			IdentityManager identityManager,
+			Clock clock) {
+		super(db, clientHelper, metadataParser);
+		this.ioExecutor = ioExecutor;
+		this.clientVersioningManager = clientVersioningManager;
+		this.contactGroupFactory = contactGroupFactory;
+		this.contactManager = contactManager;
+		this.messageParser = messageParser;
+		this.sessionParser = sessionParser;
+		this.ownerProtocolEngine = ownerProtocolEngine;
+		this.introduceeProtocolEngine = introduceeProtocolEngine;
+		this.sessionEncoder = sessionEncoder;
+		this.crypto = crypto;
+		this.mailboxProtocolEngine = mailboxProtocolEngine;
+		this.identityManager = identityManager;
+		this.localGroup =
+				contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
+		this.clock = clock;
+	}
+
+	@Override
+	public void createLocalState(Transaction txn) throws DbException {
+		// Create a local group to store protocol sessions
+		if (db.containsGroup(txn, localGroup.getId())) return;
+		db.addGroup(txn, localGroup);
+		// Set up groups for communication with any pre-existing contacts
+		for (Contact c : db.getAllContacts(txn)) addingContact(txn, c);
+	}
+
+	@Override
+	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
+			BdfDictionary bdfMeta) throws DbException, FormatException {
+		// For testing
+		ContactId introduceeId = getContactId(txn, m.getGroupId());
+		ContactType type = db.getContactType(txn, introduceeId);
+		if (type == MAILBOX_OWNER) {
+			return incomingMessageMailbox(txn, m, body, bdfMeta);
+		} else return incomingMessageContact(txn, m, body, bdfMeta);
+	}
+
+	private boolean incomingMessageMailbox(Transaction txn, Message m,
+			BdfList body, BdfDictionary bdfMeta)
+			throws DbException, FormatException {
+		// Parse the metadata
+		MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
+		// Look up the session, if there is one
+		SessionId sessionId = meta.getSessionId();
+		MailboxSession newMailboxSession = null;
+		if (sessionId == null) {
+			if (meta.getMessageType() != MAILBOX_REQUEST)
+				throw new AssertionError();
+			newMailboxSession = createNewMailboxSession(txn, m, body);
+			sessionId = newMailboxSession.getSessionId();
+		}
+		StoredSession ss = getSession(txn, sessionId);
+		// Handle the message
+		Session session;
+		MessageId storageId;
+		if (ss == null) {
+			if (meta.getMessageType() != MAILBOX_REQUEST)
+				throw new FormatException();
+			if (newMailboxSession == null) throw new AssertionError();
+			storageId = createStorageId(txn);
+			session = handleMessage(txn, m, body, meta.getMessageType(),
+					newMailboxSession, mailboxProtocolEngine);
+		} else {
+			storageId = ss.storageId;
+			long messageCounter =
+					sessionParser.getMessageCounter(ss.bdfSession);
+			if (meta.getCounter() < messageCounter) {
+				// TODO: ignore or abort?
+				throw new FormatException();
+			}
+			Role role = sessionParser.getRole(ss.bdfSession);
+			if (role == OWNER) {
+				session = handleMessage(txn, m, body, meta.getMessageType(),
+						sessionParser.parseOwnerSession(ss.bdfSession),
+						ownerProtocolEngine);
+			} else if (role == MAILBOX) {
+				session = handleMessage(txn, m, body, meta.getMessageType(),
+						sessionParser.parseMailboxSession(m.getGroupId(),
+								ss.bdfSession), mailboxProtocolEngine);
+			} else throw new AssertionError();
+		}
+		// Store the updated session
+		storeSession(txn, storageId, session);
+		return false;
+	}
+
+	private boolean incomingMessageContact(Transaction txn, Message m,
+			BdfList body, BdfDictionary bdfMeta)
+			throws DbException, FormatException {
+		// Parse the metadata
+		MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
+		// Look up the session, if there is one
+		SessionId sessionId = meta.getSessionId();
+		IntroduceeSession newMailboxSession = null;
+		if (sessionId == null) new AssertionError();
+		StoredSession ss = getSession(txn, sessionId);
+		// Handle the message
+		Session session;
+		MessageId storageId;
+		if (ss == null) {
+			if (meta.getMessageType() == INTRODUCEE_REQUEST)
+				newMailboxSession = createNewIntroduceeSession(txn, m, body);
+			if (newMailboxSession == null) throw new AssertionError();
+			storageId = createStorageId(txn);
+			session = handleMessage(txn, m, body, meta.getMessageType(),
+					newMailboxSession, introduceeProtocolEngine);
+		} else {
+			storageId = ss.storageId;
+			long messageCounter =
+					sessionParser.getMessageCounter(ss.bdfSession);
+			if (meta.getCounter() < messageCounter) {
+				// TODO: ignore or abort?
+				throw new FormatException();
+			}
+			Role role = sessionParser.getRole(ss.bdfSession);
+			if (role == OWNER) {
+				session = handleMessage(txn, m, body, meta.getMessageType(),
+						sessionParser.parseOwnerSession(ss.bdfSession),
+						ownerProtocolEngine);
+			} else if (role == INTRODUCEE) {
+				session = handleMessage(txn, m, body, meta.getMessageType(),
+						sessionParser.parseIntroduceeSession(m.getGroupId(),
+								ss.bdfSession), introduceeProtocolEngine);
+			} else throw new AssertionError();
+		}
+		// Store the updated session
+		storeSession(txn, storageId, session);
+		return false;
+	}
+
+	private MailboxSession createNewMailboxSession(Transaction txn, Message m,
+			BdfList body) throws DbException, FormatException {
+		ContactId ownerId = getContactId(txn, m.getGroupId());
+		Author owner = db.getContact(txn, ownerId).getAuthor();
+		Author local = identityManager.getLocalAuthor(txn);
+		Author remote = messageParser.parseRequestMessage(m, body).getAuthor();
+		if (local.equals(remote)) throw new FormatException();
+		SessionId sessionId = crypto.getSessionId(owner, local, remote, true);
+		return MailboxSession
+				.getInitial(m.getGroupId(), sessionId, owner, true, remote);
+	}
+
+	private IntroduceeSession createNewIntroduceeSession(Transaction txn,
+			Message m,
+			BdfList body)
+			throws DbException, FormatException {
+		ContactId ownerId = getContactId(txn, m.getGroupId());
+		Author owner = db.getContact(txn, ownerId).getAuthor();
+		Author local = identityManager.getLocalAuthor(txn);
+		Author remote = messageParser.parseIntroduceeRequestMessage(m, body)
+				.getAuthor();
+		if (local.equals(remote)) throw new FormatException();
+		SessionId sessionId = crypto.getSessionId(owner, local, remote, false);
+		return IntroduceeSession
+				.getInitial(m.getGroupId(), sessionId, owner, false, remote);
+	}
+
+
+	/*
+	private IntroduceeSession createNewIntroduceeSession(Transaction txn,
+			Message m, BdfList body) throws DbException, FormatException {
+		ContactId ownerId = getContactId(txn, m.getGroupId());
+		Author owner = db.getContact(txn, ownerId).getAuthor();
+		Author local = identityManager.getLocalAuthor(txn);
+		Author remote = messageParser.parseRequestMessage(m, body).getAuthor();
+		if (local.equals(remote)) throw new FormatException();
+		SessionId sessionId = crypto.getSessionId(owner, local, remote);
+		boolean alice = crypto.isAlice(local.getId(), remote.getId());
+		return IntroduceeSession
+				.getInitial(m.getGroupId(), sessionId, owner, alice,
+						remote);
+	}
+*/
+
+	private <S extends Session> S handleMessage(Transaction txn, Message m,
+			BdfList body, MessageType type, S session, ProtocolEngine<S> engine)
+			throws DbException, FormatException {
+		switch (type) {
+
+			case MAILBOX_REQUEST: {
+				RequestMessage request =
+						messageParser.parseRequestMessage(m, body);
+				return engine.onRequestMessage(txn, session, request);
+			}
+			case MAILBOX_ACCEPT: {
+				MailboxAcceptMessage acceptMessage =
+						messageParser.parseMailboxAcceptMessage(m, body);
+				return engine
+						.onMailboxAcceptMessage(txn, session, acceptMessage);
+			}
+			case INTRODUCEE_REQUEST: {
+				IntroduceeRequestMessage acceptMessage =
+						messageParser.parseIntroduceeRequestMessage(m, body);
+				return engine.onIntroduceeRequestMessage(txn, session,
+						acceptMessage);
+			}
+			case INTRODUCEE_ACCEPT: {
+				IntroduceeAcceptMessage acceptMessage =
+						messageParser.parseIntroduceeAcceptMessage(m, body);
+				return engine
+						.onIntroduceeAcceptMessage(txn, session, acceptMessage);
+			}
+			case MAILBOX_AUTH:
+				MailboxAuthMessage authMessage =
+						messageParser.parseMailboxAuthMessage(m, body);
+				return engine.onAuthMessage(txn, session, authMessage);
+			case ABORT: {
+				AbortMessage abort = messageParser.parseAbortMessage(m, body);
+				return engine.onAbortMessage(txn, session, abort);
+			}
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Override
+	public Collection<ContactType> getApplicableContactTypes() {
+		return Arrays.asList(values());
+	}
+
+	@Override
+	public void addingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				break;
+			case MAILBOX_OWNER:
+				break;
+			case CONTACT:
+				contactAdded(txn, c);
+				break;
+			default:
+				return;
+		}
+	}
+
+	@Override
+	public void contactAdded(Transaction txn, Contact c) throws DbException {
+		// Create a group to share with the contact
+		Group g = getContactGroup(c);
+		db.addGroup(txn, g);
+		// Apply the client's visibility to the contact group
+		Group.Visibility client = clientVersioningManager
+				.getClientVisibility(txn, c.getId(), CLIENT_ID, MAJOR_VERSION);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
+		// Attach the contact ID to the group
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
+		try {
+			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+		Collection<Contact> pm = db.getContactsByType(txn, PRIVATE_MAILBOX);
+		if (pm.isEmpty()) return;
+		Collection<MailboxInfo> mailboxes = db.getContactMailboxes(txn);
+		for (MailboxInfo mailboxInfo : mailboxes) {
+			if (mailboxInfo.getContactId().equals(c.getId())) return;
+		}
+		ioExecutor.execute(
+				() -> {
+					try {
+						makeIntroduction((PrivateMailbox) pm.iterator().next(),
+								c,
+								clock.currentTimeMillis());
+					} catch (DbException e) {
+						LOG.warning(
+								"Mailbox introduction failed: " + e.toString());
+					}
+				});
+	}
+
+	@Override
+	public void privateMailboxAdded(Transaction txn,
+			PrivateMailbox privateMailbox) throws DbException {
+		LOG.info("Private mailbox added");
+	}
+
+	@Nullable
+	private StoredSession getSession(Transaction txn,
+			@Nullable SessionId sessionId) throws DbException, FormatException {
+		if (sessionId == null) return null;
+		BdfDictionary query = sessionParser.getSessionQuery(sessionId);
+		Map<MessageId, BdfDictionary> results = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId(), query);
+		if (results.size() > 1) throw new DbException();
+		if (results.isEmpty()) return null;
+		return new StoredSession(results.keySet().iterator().next(),
+				results.values().iterator().next());
+	}
+
+
+	@Override
+	public void makeIntroduction(PrivateMailbox privateMailbox, Contact contact,
+			long timestamp) throws DbException {
+		Transaction txn = db.startTransaction(false);
+		try {
+			// Look up the session, if there is one
+			Author introducer = identityManager.getLocalAuthor(txn);
+			SessionId sessionId =
+					crypto.getSessionId(introducer, privateMailbox.getAuthor(),
+							contact.getAuthor(), true);
+			StoredSession ss = getSession(txn, sessionId);
+			// Create or parse the session
+			OwnerSession session;
+			MessageId storageId;
+			if (ss == null) {
+				// This is the first request - create a new session
+				GroupId groupId1 = getContactGroup(privateMailbox).getId();
+				GroupId groupId2 = getContactGroup(contact).getId();
+				// use fixed deterministic roles for the introducees
+				session = new OwnerSession(sessionId, groupId1,
+						privateMailbox.getAuthor(), groupId2,
+						contact.getAuthor(), 0);
+				storageId = createStorageId(txn);
+			} else {
+				// An earlier request exists, so we already have a session
+				session = sessionParser.parseOwnerSession(ss.bdfSession);
+				storageId = ss.storageId;
+			}
+			// Handle the request action
+			session = ownerProtocolEngine
+					.onRequestAction(txn, session, timestamp);
+			// Store the updated session
+			storeSession(txn, storageId, session);
+			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	private void storeSession(Transaction txn, MessageId storageId,
+			Session session) throws DbException {
+		BdfDictionary d;
+		if (session.getRole() == OWNER) {
+			d = sessionEncoder.encodeIntroducerSession((OwnerSession) session);
+		} else if (session.getRole() == MAILBOX ||
+				session.getRole() == INTRODUCEE) {
+			d = sessionEncoder
+					.encodeIntroduceeSession(
+							(AbstractIntroduceeSession) session);
+		} else {
+			throw new AssertionError();
+		}
+		try {
+			clientHelper.mergeMessageMetadata(txn, storageId, d);
+		} catch (FormatException e) {
+			throw new AssertionError();
+		}
+	}
+
+	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
+			throws DbException, FormatException {
+		BdfDictionary meta =
+				clientHelper.getGroupMetadataAsDictionary(txn, contactGroupId);
+		return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
+	}
+
+	@Override
+	public Group getContactGroup(Contact c) {
+		return contactGroupFactory
+				.createContactGroup(CLIENT_ID, MAJOR_VERSION, c);
+	}
+
+	private MessageId createStorageId(Transaction txn) throws DbException {
+		Message m = clientHelper
+				.createMessageForStoringMetadata(localGroup.getId());
+		db.addLocalMessage(txn, m, new Metadata(), false);
+		return m.getId();
+	}
+
+	@Override
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		LOG.info("contact removed");
+	}
+
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Group.Visibility v) throws DbException {
+		if (!getApplicableContactTypes().contains(c.getType())) return;
+		// Apply the client's visibility to the contact group
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
+	private static class StoredSession {
+
+		private final MessageId storageId;
+		private final BdfDictionary bdfSession;
+
+		private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
+			this.storageId = storageId;
+			this.bdfSession = bdfSession;
+		}
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
new file mode 100644
index 000000000..5bf216054
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
@@ -0,0 +1,96 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.data.MetadataEncoder;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.sync.ValidationManager;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.briar.api.introduction.IntroductionManager;
+import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static org.briarproject.briar.api.mailbox.MailboxIntroductionManager.CLIENT_ID;
+import static org.briarproject.briar.api.mailbox.MailboxIntroductionManager.MAJOR_VERSION;
+import static org.briarproject.briar.api.mailbox.MailboxIntroductionManager.MINOR_VERSION;
+
+@Module
+public class MailboxIntroductionModule {
+
+	public static class EagerSingletons {
+		@Inject
+		MailboxIntroductionValidator mailboxIntroductionValidator;
+		@Inject
+		MailboxIntroductionManager mailboxIntroductionManager;
+	}
+
+
+	@Provides
+	@Singleton
+	MailboxIntroductionValidator provideMailboxValidator(
+			ValidationManager validationManager,
+			MailboxMessageEncoder messageEncoder,
+			MetadataEncoder metadataEncoder,
+			ClientHelper clientHelper, Clock clock) {
+		MailboxIntroductionValidator mailboxIntroductionValidator =
+				new MailboxIntroductionValidator(messageEncoder, clientHelper,
+						metadataEncoder, clock);
+		validationManager
+				.registerMessageValidator(CLIENT_ID,
+						IntroductionManager.MAJOR_VERSION,
+						mailboxIntroductionValidator);
+		return mailboxIntroductionValidator;
+	}
+
+	@Provides
+	@Singleton
+	MailboxIntroductionManager provideMailboxIntroductionManager(
+			LifecycleManager lifecycleManager, ContactManager contactManager,
+			ValidationManager validationManager,
+			ClientVersioningManager clientVersioningManager,
+			MailboxIntroductionManagerImpl mailboxIntroductionManager) {
+		lifecycleManager.registerClient(mailboxIntroductionManager);
+		contactManager.registerContactHook(mailboxIntroductionManager);
+		validationManager.registerIncomingMessageHook(CLIENT_ID,
+				MAJOR_VERSION, mailboxIntroductionManager);
+		clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
+				MINOR_VERSION, mailboxIntroductionManager);
+		return mailboxIntroductionManager;
+	}
+
+	@Provides
+	MailboxMessageParser provideMailboxMessageParser(
+			MailboxMessageParserImpl messageParser) {
+		return messageParser;
+	}
+
+	@Provides
+	MailboxMessageEncoder provideMailboxMessageEncoder(
+			MailboxMessageEncoderImpl messageEncoder) {
+		return messageEncoder;
+	}
+
+	@Provides
+	MailboxSessionParser provideMailboxSessionParser(
+			MailboxSessionParserImpl sessionParser) {
+		return sessionParser;
+	}
+
+	@Provides
+	MailboxSessionEncoder provideMailboxSessionEncoder(
+			MailboxSessionEncoderImpl sessionEncoder) {
+		return sessionEncoder;
+	}
+
+	@Provides
+	MailboxIntroductionCrypto provideMailboxIntroductionCrypto(
+			MailboxIntroductionCryptoImpl mailboxIntroductionCrypto) {
+		return mailboxIntroductionCrypto;
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
new file mode 100644
index 000000000..8725fc9f2
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
@@ -0,0 +1,248 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.UniqueId;
+import org.briarproject.bramble.api.client.BdfMessageContext;
+import org.briarproject.bramble.api.client.BdfMessageValidator;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.data.MetadataEncoder;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Collections;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
+import static org.briarproject.bramble.util.ValidationUtils.checkLength;
+import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_REQUEST;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_AUTH;
+
+
+@Immutable
+@NotNullByDefault
+class MailboxIntroductionValidator extends BdfMessageValidator {
+
+	private final MailboxMessageEncoder messageEncoder;
+
+	MailboxIntroductionValidator(MailboxMessageEncoder messageEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
+		this.messageEncoder = messageEncoder;
+	}
+
+	@Override
+	protected BdfMessageContext validateMessage(Message m, Group g,
+			BdfList body) throws FormatException {
+		MessageType type = MessageType.fromValue(body.getLong(0).intValue());
+
+		switch (type) {
+			case MAILBOX_REQUEST:
+				return validateRequestMessage(m, body);
+			case MAILBOX_ACCEPT:
+				return validateAcceptMessage(m, body);
+			case INTRODUCEE_REQUEST:
+				return validateIntroduceeRequestMessage(m, body);
+			case MAILBOX_AUTH:
+				return validateAuthMessage(m, body);
+			case INTRODUCEE_ACCEPT:
+				return validateIntroduceeAcceptMessage(m, body);
+			case DECLINE:
+			case ABORT:
+				return validateOtherMessage(type, m, body);
+			default:
+				throw new FormatException();
+		}
+	}
+
+	private BdfMessageContext validateRequestMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 4);
+		byte[] previousMessageId = body.getOptionalRaw(1);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+		BdfList authorList = body.getList(2);
+		clientHelper.parseAndValidateAuthor(authorList);
+		long messageCounter = body.getLong(3);
+		BdfDictionary meta = messageEncoder
+				.encodeRequestMetadata(m.getTimestamp(), messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateAcceptMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 6);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		long timestamp = body.getLong(4);
+		if (timestamp < 0) throw new FormatException();
+
+		long messageCounter = body.getLong(5);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(MAILBOX_ACCEPT, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateIntroduceeRequestMessage(Message m,
+			BdfList body)
+			throws FormatException {
+		checkSize(body, 7);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		BdfList authorList = body.getList(2);
+		clientHelper.parseAndValidateAuthor(authorList);
+
+		byte[] previousMessageId = body.getOptionalRaw(3);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		byte[] ephemeralPublicKey = body.getRaw(4);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		long timestamp = body.getLong(5);
+		if (timestamp < 0) throw new FormatException();
+
+		long messageCounter = body.getLong(6);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(INTRODUCEE_REQUEST, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateIntroduceeAcceptMessage(Message m,
+			BdfList body)
+			throws FormatException {
+		checkSize(body, 8);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		byte[] mac = body.getRaw(4);
+		checkLength(mac, MAC_BYTES);
+
+		byte[] signature = body.getRaw(5);
+		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
+
+		long timestamp = body.getLong(6);
+		if (timestamp < 0) throw new FormatException();
+
+		long messageCounter = body.getLong(7);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(INTRODUCEE_ACCEPT, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateAuthMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 7);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		BdfDictionary transportProperties = body.getDictionary(3);
+		if (transportProperties.size() < 1) throw new FormatException();
+		clientHelper
+				.parseAndValidateTransportPropertiesMap(transportProperties);
+
+		byte[] mac = body.getRaw(4);
+		checkLength(mac, MAC_BYTES);
+
+		byte[] signature = body.getRaw(5);
+		checkLength(signature, 1, MAX_SIGNATURE_BYTES);
+
+		long messageCounter = body.getLong(6);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(MAILBOX_AUTH, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		MessageId dependency = new MessageId(previousMessageId);
+		return new BdfMessageContext(meta,
+				Collections.singletonList(dependency));
+	}
+
+	private BdfMessageContext validateOtherMessage(MessageType type, Message m,
+			BdfList body) throws FormatException {
+		checkSize(body, 3);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, sessionId, m.getTimestamp(), false, 0);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java
new file mode 100644
index 000000000..76ab219ed
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoder.java
@@ -0,0 +1,66 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface MailboxMessageEncoder {
+
+	BdfDictionary encodeRequestMetadata(long timestamp, long messageCounter);
+
+	BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local,
+			long messsageCounter);
+
+	void addSessionId(BdfDictionary meta, SessionId sessionId);
+
+	void setAvailableToAnswer(BdfDictionary meta, boolean available);
+
+	Message encodeRequestMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, Author introduceeAuthor,
+			long messageCounter);
+
+	Message encodeIntroduceeAcceptMessage(GroupId contactGroupId,
+			long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			byte[] ephemeralPublicKey, byte[] mac, byte[] signature,
+			long acceptTimestamp,
+			long messageCounter);
+
+	Message encodeMailboxAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, MessageType type, byte[] ephemeralPublicKey,
+			long acceptTimestamp,
+			long messageCounter);
+
+	Message encodeIntroduceeRequestMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, MessageType type, Author author,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			long messageCounter);
+
+	Message encodeMailboxAuthMessage(GroupId contactGroupId,
+			long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature,
+			long messageCounter);
+
+	Message encodeDeclineMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId);
+
+	Message encodeAbortMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId);
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java
new file mode 100644
index 000000000..6fe872150
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageEncoderImpl.java
@@ -0,0 +1,172 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageFactory;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_COUNTER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_LOCAL;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_SESSION_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.briar.mailbox.MessageType.ABORT;
+import static org.briarproject.briar.mailbox.MessageType.DECLINE;
+import static org.briarproject.briar.mailbox.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_AUTH;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
+
+@NotNullByDefault
+class MailboxMessageEncoderImpl implements MailboxMessageEncoder {
+
+	private final ClientHelper clientHelper;
+	private final MessageFactory messageFactory;
+
+	@Inject
+	MailboxMessageEncoderImpl(ClientHelper clientHelper,
+			MessageFactory messageFactory) {
+		this.clientHelper = clientHelper;
+		this.messageFactory = messageFactory;
+	}
+
+	@Override
+	public BdfDictionary encodeRequestMetadata(long timestamp,
+			long messageCounter) {
+		BdfDictionary meta =
+				encodeMetadata(MAILBOX_REQUEST, null, timestamp, false,
+						messageCounter);
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return meta;
+	}
+
+	@Override
+	public BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local,
+			long messageCounter) {
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue());
+		if (sessionId != null) meta.put(MSG_KEY_SESSION_ID, sessionId);
+		else if (type != MAILBOX_REQUEST) throw new IllegalArgumentException();
+		meta.put(MSG_KEY_TIMESTAMP, timestamp);
+		meta.put(MSG_KEY_LOCAL, local);
+		meta.put(MSG_KEY_COUNTER, messageCounter);
+		return meta;
+	}
+
+	@Override
+	public void addSessionId(BdfDictionary meta, SessionId sessionId) {
+		meta.put(MSG_KEY_SESSION_ID, sessionId);
+	}
+
+	@Override
+	public void setAvailableToAnswer(BdfDictionary meta, boolean available) {
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
+	}
+
+	@Override
+	public Message encodeRequestMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, Author introduceeAuthor,
+			long messageCounter) {
+		BdfList body = BdfList.of(MAILBOX_REQUEST.getValue(), previousMessageId,
+				clientHelper.toList(introduceeAuthor), messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeMailboxAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, MessageType type,
+			byte[] ephemeralPublicKey,
+			long acceptTimestamp, long messageCounter) {
+		BdfList body = BdfList.of(type.getValue(), sessionId,
+				previousMessageId, ephemeralPublicKey, acceptTimestamp,
+				messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeIntroduceeRequestMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, MessageType type, Author author,
+			byte[] ephemeralPublicKey,
+			long acceptTimestamp, long messageCounter) {
+		BdfList body = BdfList.of(type.getValue(), sessionId,
+				clientHelper.toList(author),
+				previousMessageId, ephemeralPublicKey, acceptTimestamp,
+				messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeIntroduceeAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey, byte[] mac,
+			byte[] signature, long acceptTimestamp, long messageCounter) {
+		BdfList body = BdfList.of(INTRODUCEE_ACCEPT.getValue(), sessionId,
+				previousMessageId, ephemeralPublicKey, mac, signature,
+				acceptTimestamp,
+				messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeMailboxAuthMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac,
+			byte[] signature, long messageCounter) {
+		BdfList body = BdfList.of(MAILBOX_AUTH.getValue(), sessionId,
+				previousMessageId, clientHelper.toDictionary(transportProperties), mac, signature,
+				messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeDeclineMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId) {
+		return encodeMessage(DECLINE, contactGroupId, sessionId, timestamp,
+				previousMessageId);
+	}
+
+	@Override
+	public Message encodeAbortMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId) {
+		return encodeMessage(ABORT, contactGroupId, sessionId, timestamp,
+				previousMessageId);
+	}
+
+	private Message encodeMessage(MessageType type, GroupId contactGroupId,
+			SessionId sessionId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		BdfList body =
+				BdfList.of(type.getValue(), sessionId, previousMessageId);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	private Message createMessage(GroupId contactGroupId, long timestamp,
+			BdfList body) {
+		try {
+			return messageFactory.createMessage(contactGroupId, timestamp,
+					clientHelper.toByteArray(body));
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java
new file mode 100644
index 000000000..4c928a78d
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParser.java
@@ -0,0 +1,40 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.briar.api.client.SessionId;
+
+@NotNullByDefault
+interface MailboxMessageParser {
+
+	BdfDictionary getRequestsAvailableToAnswerQuery(SessionId sessionId);
+
+	MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
+
+	//MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
+
+	RequestMessage parseRequestMessage(Message m, BdfList body)
+			throws FormatException;
+
+	MailboxAcceptMessage parseMailboxAcceptMessage(Message m, BdfList body)
+			throws FormatException;
+
+	IntroduceeRequestMessage parseIntroduceeRequestMessage(Message m,
+			BdfList body) throws FormatException;
+
+	IntroduceeAcceptMessage parseIntroduceeAcceptMessage(Message m,
+			BdfList body) throws FormatException;
+
+	DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException;
+
+	MailboxAuthMessage parseMailboxAuthMessage(Message m, BdfList body)
+			throws FormatException;
+
+	AbortMessage parseAbortMessage(Message m, BdfList body)
+			throws FormatException;
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java
new file mode 100644
index 000000000..a040aa3d7
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxMessageParserImpl.java
@@ -0,0 +1,156 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_COUNTER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_LOCAL;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_SESSION_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
+
+@NotNullByDefault
+class MailboxMessageParserImpl implements MailboxMessageParser {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxMessageParserImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getRequestsAvailableToAnswerQuery(
+			SessionId sessionId) {
+		return BdfDictionary.of(new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
+				new BdfEntry(MSG_KEY_MESSAGE_TYPE, MAILBOX_REQUEST.getValue()),
+				new BdfEntry(MSG_KEY_SESSION_ID, sessionId));
+	}
+
+	@Override
+	public MessageMetadata parseMetadata(BdfDictionary d)
+			throws FormatException {
+		MessageType type = MessageType
+				.fromValue(d.getLong(MSG_KEY_MESSAGE_TYPE).intValue());
+		byte[] sessionIdBytes = d.getOptionalRaw(MSG_KEY_SESSION_ID);
+		SessionId sessionId =
+				sessionIdBytes == null ? null : new SessionId(sessionIdBytes);
+		long timestamp = d.getLong(MSG_KEY_TIMESTAMP);
+		boolean local = d.getBoolean(MSG_KEY_LOCAL);
+		long counter = d.getLong(MSG_KEY_COUNTER);
+		boolean available = d.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return new MessageMetadata(type, sessionId, timestamp, local, counter,
+				available);
+	}
+
+	@Override
+	public RequestMessage parseRequestMessage(Message m, BdfList body)
+			throws FormatException {
+		byte[] previousMsgBytes = body.getOptionalRaw(1);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		Author author = clientHelper.parseAndValidateAuthor(body.getList(2));
+		return new RequestMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, author);
+	}
+
+	@Override
+	public MailboxAcceptMessage parseMailboxAcceptMessage(Message m,
+			BdfList body) throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		long acceptTimestamp = body.getLong(4);
+		return new MailboxAcceptMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId,
+				ephemeralPublicKey, acceptTimestamp);
+	}
+
+	@Override
+	public IntroduceeRequestMessage parseIntroduceeRequestMessage(Message m,
+			BdfList body) throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		Author author = clientHelper.parseAndValidateAuthor(body.getList(2));
+		byte[] previousMsgBytes = body.getOptionalRaw(3);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		byte[] ephemeralPublicKey = body.getRaw(4);
+		long acceptTimestamp = body.getLong(5);
+		return new IntroduceeRequestMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId, author,
+				ephemeralPublicKey, acceptTimestamp);
+	}
+
+	@Override
+	public IntroduceeAcceptMessage parseIntroduceeAcceptMessage(Message m,
+			BdfList body) throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		byte[] mac = body.getRaw(4);
+		byte[] signature = body.getRaw(5);
+		long acceptTimestamp = body.getLong(6);
+		return new IntroduceeAcceptMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId,
+				ephemeralPublicKey, mac, signature, acceptTimestamp);
+	}
+
+	@Override
+	public DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		return new DeclineMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, sessionId);
+	}
+
+	@Override
+	public MailboxAuthMessage parseMailboxAuthMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getRaw(2);
+		MessageId previousMessageId = new MessageId(previousMsgBytes);
+		Map<TransportId, TransportProperties> transportProperties = clientHelper
+				.parseAndValidateTransportPropertiesMap(body.getDictionary(3));
+		byte[] mac = body.getRaw(4);
+		byte[] signature = body.getRaw(5);
+		return new MailboxAuthMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(),
+				previousMessageId, sessionId, transportProperties, mac,
+				signature);
+	}
+
+	@Override
+	public AbortMessage parseAbortMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		return new AbortMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, sessionId);
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
new file mode 100644
index 000000000..ea128eb7d
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
@@ -0,0 +1,278 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.data.BdfDictionary;
+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.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionSucceededEvent;
+
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.briar.mailbox.MailboxState.AWAIT_REMOTE_RESPONSE;
+import static org.briarproject.briar.mailbox.MailboxState.CONTACT_ADDED;
+
+class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
+
+	private final static Logger LOG =
+			Logger.getLogger(MailboxProtocolEngine.class.getName());
+
+	@Inject
+	MailboxProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager,
+			MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto,
+			KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		super(db, clientHelper, contactManager, contactGroupFactory,
+				identityManager, messageParser, messageEncoder,
+				clock, crypto, keyManager, transportPropertyManager);
+	}
+
+	@Override
+	public MailboxSession onRequestAction(Transaction txn,
+			MailboxSession session, long timestamp) throws DbException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onAcceptAction(Transaction txn,
+			MailboxSession session, long timestamp) throws DbException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onDeclineAction(Transaction txn,
+			MailboxSession session, long timestamp) throws DbException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onRequestMessage(Transaction txn,
+			MailboxSession session, RequestMessage m)
+			throws DbException, FormatException {
+		switch (session.getState()) {
+			case START:
+				return onRemoteRequest(txn, session, m);
+			case AWAIT_LOCAL_RESPONSE:
+			case LOCAL_DECLINED:
+			case LOCAL_ACCEPTED:
+			case AWAIT_REMOTE_RESPONSE:
+			case CONTACT_ADDED:
+				return abort(txn, session);
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private MailboxSession onRemoteRequest(Transaction txn, MailboxSession s,
+			RequestMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+
+		// Add SessionId to message metadata
+		addSessionId(txn, m.getMessageId(), s.getSessionId());
+
+		// Broadcast IntroductionRequestReceivedEvent
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		txn.attach(new MailboxIntroductionRequestReceivedEvent(
+				localAuthor.getId()));
+
+		// Create ephemeral key pair and get local transport properties
+		KeyPair keyPair = crypto.generateKeyPair();
+		byte[] publicKey = keyPair.getPublic().getEncoded();
+		byte[] privateKey = keyPair.getPrivate().getEncoded();
+		long localTimestamp = clock.currentTimeMillis();
+		// Send ephemeral public key and timestamp back
+		Message reply =
+				sendMailboxAcceptMessage(txn, s, localTimestamp, publicKey,
+						localTimestamp, s.getMessageCounter());
+
+		//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
+		// Move to the AWAIT_REMOTE_RESPONSE state
+		return MailboxSession
+				.addLocalAccept(s, AWAIT_REMOTE_RESPONSE, reply,
+						publicKey,
+						privateKey, localTimestamp);
+	}
+
+	private MailboxSession abort(Transaction txn, MailboxSession s)
+			throws DbException {
+		/*
+		// Mark the request message unavailable to answer
+		markRequestsUnavailableToAnswer(txn, s);
+
+		// Send an ABORT message
+		Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s));
+
+		// Broadcast abort event for testing
+		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
+
+		// Reset the session back to initial state
+		return IntroduceeSession.clear(s, START, sent.getId(),
+				sent.getTimestamp(), s.getLastRemoteMessageId());
+				*/
+		return null;
+	}
+
+	@Override
+	public MailboxSession onMailboxAcceptMessage(Transaction txn,
+			MailboxSession session, MailboxAcceptMessage m)
+			throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onIntroduceeRequestMessage(Transaction txn,
+			MailboxSession session, IntroduceeRequestMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public MailboxSession onContactAcceptMessage(Transaction txn,
+			MailboxSession session, IntroduceeRequestMessage m)
+			throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onDeclineMessage(Transaction txn,
+			MailboxSession session, DeclineMessage m)
+			throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onAuthMessage(Transaction txn, MailboxSession session,
+			MailboxAuthMessage m) throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onAbortMessage(Transaction txn,
+			MailboxSession session, AbortMessage m)
+			throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public MailboxSession onIntroduceeAcceptMessage(Transaction txn,
+			MailboxSession s, IntroduceeAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getLastLocalMessageId(),
+				m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Broadcast IntroductionRequestReceivedEvent
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		try {
+			SecretKey secretKey =
+					crypto.deriveMasterKey(s.local.ephemeralPublicKey,
+							s.local.ephemeralPrivateKey,
+							m.getEphemeralPublicKey(), true);
+			SecretKey aliceMacKey = crypto.deriveMacKey(secretKey, true);
+			SecretKey bobMacKey = crypto.deriveMacKey(secretKey, false);
+
+			s = MailboxSession.addIntroduceeAccept(s, m.getMessageId(),
+					m.getEphemeralPublicKey(), secretKey, aliceMacKey,
+					bobMacKey,
+					m.getAcceptTimestamp());
+			crypto.verifyAuthMac(m.getMac(), s, localAuthor.getId());
+			crypto.verifySignature(m.getSignature(), s);
+			long timestamp = Math.min(s.getLocal().acceptTimestamp,
+					s.getRemote().acceptTimestamp);
+			if (timestamp == -1) throw new AssertionError();
+			byte[] mac = crypto.authMac(aliceMacKey, s, localAuthor.getId());
+			byte[] signature =
+					crypto.sign(aliceMacKey, localAuthor.getPrivateKey());
+
+			contactManager
+					.addContact(txn, s.getRemote().author, localAuthor.getId(),
+							false, true);
+			// Only add transport properties and keys when the contact was added
+			// This will be changed once we have a way to reset state for peers
+			// that were contacts already at some point in the past.
+			Contact c = contactManager
+					.getContact(txn, s.getRemote().author.getId(),
+							localAuthor.getId());
+			// add the keys to the new contact
+			//noinspection ConstantConditions
+			Map<TransportId, KeySetId> keys = keyManager
+					.addContact(txn, c.getId(), secretKey,
+							timestamp, s.getLocal().alice, true);
+			// add signed transport properties for the contact
+			//noinspection ConstantConditions
+			transportPropertyManager.addRemoteProperties(txn, c.getId(),
+					transportPropertyManager.getLocalAnonymizedProperties(txn));
+			// Broadcast MailboxIntroductionSucceededEvent, because contact got added
+			MailboxIntroductionSucceededEvent e =
+					new MailboxIntroductionSucceededEvent(c);
+			txn.attach(e);
+			Map<TransportId, TransportProperties> transportProperties =
+					transportPropertyManager.getLocalProperties(txn);
+			Message reply =
+					sendMailboxAuthMessage(txn, s, clock.currentTimeMillis(),
+							transportProperties, mac, signature,
+							s.getMessageCounter());
+			//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
+			return MailboxSession
+					.clear(s, CONTACT_ADDED, reply.getId(),
+							s.getLocalTimestamp(), m.getMessageId());
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+
+	}
+
+
+	private boolean isInvalidDependency(AbstractIntroduceeSession s,
+			@Nullable MessageId dependency) {
+		return isInvalidDependency(s.getLastRemoteMessageId(), dependency);
+	}
+
+	private long getLocalTimestamp(AbstractIntroduceeSession s) {
+		return getLocalTimestamp(s.getLocalTimestamp(),
+				s.getRequestTimestamp());
+	}
+
+	private void addSessionId(Transaction txn, MessageId m, SessionId sessionId)
+			throws DbException {
+		BdfDictionary meta = new BdfDictionary();
+		messageEncoder.addSessionId(meta, sessionId);
+		try {
+			clientHelper.mergeMessageMetadata(txn, m, meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
new file mode 100644
index 000000000..a9a75c8b6
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
@@ -0,0 +1,139 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.Role;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
+import static org.briarproject.briar.mailbox.MailboxState.START;
+
+@Immutable
+@NotNullByDefault
+class MailboxSession extends AbstractIntroduceeSession<MailboxState> {
+
+
+	MailboxSession(SessionId sessionId, MailboxState state,
+			long requestTimestamp, GroupId contactGroupId, Author introducer,
+			Local local, Remote remote, @Nullable byte[] masterKey,
+			@Nullable Map<TransportId, KeySetId> transportKeys,
+			long sessionCounter) {
+		super(sessionId, state, requestTimestamp, contactGroupId, introducer,
+				local, remote, masterKey, transportKeys, sessionCounter);
+	}
+
+	static MailboxSession getInitial(GroupId contactGroupId,
+			SessionId sessionId, Author introducer, boolean localIsAlice,
+			Author remoteAuthor) {
+		Local local = new Local(localIsAlice, null, -1, null, null, -1, null);
+		Remote remote =
+				new Remote(!localIsAlice, remoteAuthor, null, null, null, -1,
+						null);
+		return new MailboxSession(sessionId, START, -1,
+				contactGroupId,
+				introducer, local, remote, null, null, 0);
+	}
+
+	static MailboxSession addLocalAccept(MailboxSession s,
+			MailboxState state, Message m, byte[] ephemeralPublicKey,
+			byte[] ephemeralPrivateKey, long acceptTimestamp) {
+		Local local = new Local(s.local.alice, m.getId(), m.getTimestamp(),
+				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp, null);
+		return new MailboxSession(s.getSessionId(), state, m.getTimestamp(),
+				s.contactGroupId, s.introducer, local, s.remote, s.masterKey,
+				s.transportKeys, s.getMessageCounter());
+	}
+
+	static MailboxSession clear(MailboxSession s, MailboxState state,
+			@Nullable MessageId lastLocalMessageId, long localTimestamp,
+			@Nullable MessageId lastRemoteMessageId) {
+		Local local =
+				new Local(s.local.alice, lastLocalMessageId, localTimestamp,
+						null, null, -1, null);
+		Remote remote =
+				new Remote(s.remote.alice, s.remote.author, lastRemoteMessageId,
+						null, null, -1, null);
+		return new MailboxSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, null, null, s.getMessageCounter());
+	}
+
+	static MailboxSession addIntroduceeAccept(MailboxSession s,
+			MessageId lastMessageId,
+			byte[] ephemeralPublicKey, SecretKey masterKey,
+			SecretKey aliceMacKey,
+			SecretKey bobMacKey, long acceptTimestamp) {
+		Local local = new Local(s.local.alice, s.local.lastMessageId,
+				s.local.lastMessageTimestamp, s.local.ephemeralPublicKey,
+				s.local.ephemeralPrivateKey, s.local.acceptTimestamp,
+				aliceMacKey.getBytes());
+		Remote remote =
+				new Remote(false, s.remote.author, lastMessageId,
+						ephemeralPublicKey, null, acceptTimestamp,
+						bobMacKey.getBytes());
+		return new MailboxSession(s.getSessionId(), s.getState(),
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, masterKey.getBytes(), null, s.getMessageCounter());
+	}
+
+	@Override
+	Role getRole() {
+		return MAILBOX;
+	}
+
+	@Override
+	public GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	@Override
+	public long getLocalTimestamp() {
+		return local.lastMessageTimestamp;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastLocalMessageId() {
+		return local.lastMessageId;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return remote.lastMessageId;
+	}
+
+	Author getIntroducer() {
+		return introducer;
+	}
+
+	public Local getLocal() {
+		return local;
+	}
+
+	public Remote getRemote() {
+		return remote;
+	}
+
+	@Nullable
+	byte[] getMasterKey() {
+		return masterKey;
+	}
+
+	@Nullable
+	Map<TransportId, KeySetId> getTransportKeys() {
+		return transportKeys;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoder.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoder.java
new file mode 100644
index 000000000..a20028f41
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoder.java
@@ -0,0 +1,18 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface MailboxSessionEncoder {
+
+	BdfDictionary getIntroduceeSessionsByIntroducerQuery(Author introducer);
+
+	BdfDictionary getIntroducerSessionsQuery();
+
+	BdfDictionary encodeIntroducerSession(OwnerSession s);
+
+	BdfDictionary encodeIntroduceeSession(AbstractIntroduceeSession s);
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoderImpl.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoderImpl.java
new file mode 100644
index 000000000..af188a754
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionEncoderImpl.java
@@ -0,0 +1,157 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.transport.KeySetId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Common;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Local;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Remote;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_ALICE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_COUNTER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_INTRODUCEE_B;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LOCAL;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_MAC_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_REMOTE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+
+@Immutable
+@NotNullByDefault
+class MailboxSessionEncoderImpl implements
+		MailboxSessionEncoder {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxSessionEncoderImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getIntroduceeSessionsByIntroducerQuery(
+			Author introducer) {
+		return BdfDictionary.of(
+				new BdfEntry(SESSION_KEY_ROLE, INTRODUCEE.getValue()),
+				new BdfEntry(SESSION_KEY_INTRODUCER,
+						clientHelper.toList(introducer))
+		);
+	}
+
+	@Override
+	public BdfDictionary getIntroducerSessionsQuery() {
+		return BdfDictionary.of(
+				new BdfEntry(SESSION_KEY_ROLE, INTRODUCER.getValue())
+		);
+	}
+
+	@Override
+	public BdfDictionary encodeIntroducerSession(OwnerSession s) {
+		BdfDictionary d = encodeSession(s);
+		d.put(SESSION_KEY_INTRODUCEE_A, encodeIntroducee(s.getMailbox()));
+		d.put(SESSION_KEY_INTRODUCEE_B, encodeIntroducee(s.getIntroducee()));
+		return d;
+	}
+
+	private BdfDictionary encodeIntroducee(Introducee i) {
+		BdfDictionary d = new BdfDictionary();
+		putNullable(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, i.lastLocalMessageId);
+		putNullable(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID,
+				i.lastRemoteMessageId);
+		d.put(SESSION_KEY_LOCAL_TIMESTAMP, i.localTimestamp);
+		d.put(SESSION_KEY_GROUP_ID, i.groupId);
+		d.put(SESSION_KEY_AUTHOR, clientHelper.toList(i.author));
+		return d;
+	}
+
+	@Override
+	public BdfDictionary encodeIntroduceeSession(AbstractIntroduceeSession s) {
+		BdfDictionary d = encodeSession(s);
+		d.put(SESSION_KEY_INTRODUCER, clientHelper.toList(s.getIntroducer()));
+		d.put(SESSION_KEY_LOCAL, encodeLocal(s.getLocal()));
+		d.put(SESSION_KEY_REMOTE, encodeRemote(s.getRemote()));
+		putNullable(d, SESSION_KEY_MASTER_KEY, s.getMasterKey());
+		putNullable(d, SESSION_KEY_TRANSPORT_KEYS,
+				encodeTransportKeys(s.getTransportKeys()));
+		return d;
+	}
+
+	private BdfDictionary encodeCommon(Common s) {
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_ALICE, s.alice);
+		putNullable(d, SESSION_KEY_EPHEMERAL_PUBLIC_KEY, s.ephemeralPublicKey);
+		d.put(SESSION_KEY_ACCEPT_TIMESTAMP, s.acceptTimestamp);
+		putNullable(d, SESSION_KEY_MAC_KEY, s.macKey);
+		return d;
+	}
+
+	private BdfDictionary encodeLocal(Local s) {
+		BdfDictionary d = encodeCommon(s);
+		d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.lastMessageTimestamp);
+		putNullable(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, s.lastMessageId);
+		putNullable(d, SESSION_KEY_EPHEMERAL_PRIVATE_KEY,
+				s.ephemeralPrivateKey);
+		return d;
+	}
+
+	private BdfDictionary encodeRemote(Remote s) {
+		BdfDictionary d = encodeCommon(s);
+		d.put(SESSION_KEY_REMOTE_AUTHOR, clientHelper.toList(s.author));
+		putNullable(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID, s.lastMessageId);
+		return d;
+	}
+
+	private BdfDictionary encodeSession(Session s) {
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_SESSION_ID, s.getSessionId());
+		d.put(SESSION_KEY_ROLE, s.getRole().getValue());
+		d.put(SESSION_KEY_STATE, s.getState().getValue());
+		d.put(SESSION_KEY_REQUEST_TIMESTAMP, s.getRequestTimestamp());
+		d.put(SESSION_KEY_COUNTER, s.getMessageCounter());
+		return d;
+	}
+
+	@Nullable
+	private BdfDictionary encodeTransportKeys(
+			@Nullable Map<TransportId, KeySetId> keys) {
+		if (keys == null) return null;
+		BdfDictionary d = new BdfDictionary();
+		for (Map.Entry<TransportId, KeySetId> e : keys.entrySet()) {
+			d.put(e.getKey().getString(), e.getValue().getInt());
+		}
+		return d;
+	}
+
+	private void putNullable(BdfDictionary d, String key, @Nullable Object o) {
+		d.put(key, o == null ? NULL_VALUE : o);
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java
new file mode 100644
index 000000000..b3b439dd8
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParser.java
@@ -0,0 +1,28 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.Role;
+
+@NotNullByDefault
+interface MailboxSessionParser {
+
+	BdfDictionary getSessionQuery(SessionId s);
+
+	Role getRole(BdfDictionary d) throws FormatException;
+
+	long getMessageCounter(BdfDictionary d) throws FormatException;
+
+	OwnerSession parseOwnerSession(BdfDictionary d)
+			throws FormatException;
+
+	MailboxSession parseMailboxSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException;
+
+	IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException;
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java
new file mode 100644
index 000000000..e74ee5238
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxSessionParserImpl.java
@@ -0,0 +1,230 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.Role;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.briar.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
+import static org.briarproject.briar.api.mailbox.Role.OWNER;
+import static org.briarproject.briar.api.mailbox.Role.fromValue;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Local;
+import static org.briarproject.briar.mailbox.AbstractIntroduceeSession.Remote;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_ALICE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_COUNTER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_INTRODUCEE_B;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LOCAL;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_MAC_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_REMOTE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
+
+@Immutable
+@NotNullByDefault
+class MailboxSessionParserImpl implements MailboxSessionParser {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxSessionParserImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getSessionQuery(SessionId s) {
+		return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
+	}
+
+	@Override
+	public Role getRole(BdfDictionary d) throws FormatException {
+		return fromValue(d.getLong(SESSION_KEY_ROLE).intValue());
+	}
+
+	@Override
+	public long getMessageCounter(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_COUNTER);
+	}
+
+	@Override
+	public OwnerSession parseOwnerSession(BdfDictionary d)
+			throws FormatException {
+		if (getRole(d) != OWNER) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		long sessionCounter = getSessionCounter(d);
+		OwnerState state = OwnerState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		Introducee introduceeA = parseIntroducee(sessionId,
+				d.getDictionary(SESSION_KEY_INTRODUCEE_A));
+		Introducee introduceeB = parseIntroducee(sessionId,
+				d.getDictionary(SESSION_KEY_INTRODUCEE_B));
+		return new OwnerSession(sessionId, state, requestTimestamp, introduceeA,
+				introduceeB, sessionCounter);
+	}
+
+	private Introducee parseIntroducee(SessionId sessionId, BdfDictionary d)
+			throws FormatException {
+		MessageId lastLocalMessageId =
+				getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
+		MessageId lastRemoteMessageId =
+				getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
+		long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
+		GroupId groupId = getGroupId(d, SESSION_KEY_GROUP_ID);
+		Author author = getAuthor(d, SESSION_KEY_AUTHOR);
+		return new Introducee(sessionId, groupId, author, localTimestamp,
+				lastLocalMessageId, lastRemoteMessageId);
+	}
+
+	@Override
+	public MailboxSession parseMailboxSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException {
+		if (getRole(d) != MAILBOX) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		long sessionCounter = getSessionCounter(d);
+		MailboxState state = MailboxState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		Author introducer = getAuthor(d, SESSION_KEY_INTRODUCER);
+		Local local = parseLocal(d.getDictionary(SESSION_KEY_LOCAL));
+		Remote remote = parseRemote(d.getDictionary(SESSION_KEY_REMOTE));
+		byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY);
+		Map<TransportId, KeySetId> transportKeys = parseTransportKeys(
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS));
+		return new MailboxSession(sessionId, state, requestTimestamp,
+				introducerGroupId, introducer, local, remote, masterKey,
+				transportKeys, sessionCounter);
+	}
+
+	@Override
+	public IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException {
+		if (getRole(d) != INTRODUCEE) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		long sessionCounter = getSessionCounter(d);
+		IntroduceeState state = IntroduceeState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		Author introducer = getAuthor(d, SESSION_KEY_INTRODUCER);
+		Local local = parseLocal(d.getDictionary(SESSION_KEY_LOCAL));
+		Remote remote = parseRemote(d.getDictionary(SESSION_KEY_REMOTE));
+		byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY);
+		Map<TransportId, KeySetId> transportKeys = parseTransportKeys(
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS));
+		return new IntroduceeSession(sessionId, state, requestTimestamp,
+				introducerGroupId, introducer, local, remote, masterKey,
+				transportKeys, sessionCounter);
+	}
+
+	private Local parseLocal(BdfDictionary d) throws FormatException {
+		boolean alice = d.getBoolean(SESSION_KEY_ALICE);
+		MessageId lastLocalMessageId =
+				getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
+		long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
+		byte[] ephemeralPublicKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PUBLIC_KEY);
+		BdfDictionary tpDict =
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_PROPERTIES);
+		byte[] ephemeralPrivateKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PRIVATE_KEY);
+		Map<TransportId, TransportProperties> transportProperties =
+				tpDict == null ? null : clientHelper
+						.parseAndValidateTransportPropertiesMap(tpDict);
+		long acceptTimestamp = d.getLong(SESSION_KEY_ACCEPT_TIMESTAMP);
+		byte[] macKey = d.getOptionalRaw(SESSION_KEY_MAC_KEY);
+		return new Local(alice, lastLocalMessageId, localTimestamp,
+				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp,
+				macKey);
+	}
+
+	private Remote parseRemote(BdfDictionary d) throws FormatException {
+		boolean alice = d.getBoolean(SESSION_KEY_ALICE);
+		Author remoteAuthor = getAuthor(d, SESSION_KEY_REMOTE_AUTHOR);
+		MessageId lastRemoteMessageId =
+				getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
+		byte[] ephemeralPublicKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PUBLIC_KEY);
+		BdfDictionary tpDict =
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_PROPERTIES);
+		Map<TransportId, TransportProperties> transportProperties =
+				tpDict == null ? null : clientHelper
+						.parseAndValidateTransportPropertiesMap(tpDict);
+		long acceptTimestamp = d.getLong(SESSION_KEY_ACCEPT_TIMESTAMP);
+		byte[] macKey = d.getOptionalRaw(SESSION_KEY_MAC_KEY);
+		return new Remote(alice, remoteAuthor, lastRemoteMessageId,
+				ephemeralPublicKey, transportProperties, acceptTimestamp,
+				macKey);
+	}
+
+	private int getState(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_STATE).intValue();
+	}
+
+	private long getSessionCounter(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_COUNTER);
+	}
+
+	private SessionId getSessionId(BdfDictionary d) throws FormatException {
+		byte[] b = d.getRaw(SESSION_KEY_SESSION_ID);
+		return new SessionId(b);
+	}
+
+	@Nullable
+	private MessageId getMessageId(BdfDictionary d, String key)
+			throws FormatException {
+		byte[] b = d.getOptionalRaw(key);
+		return b == null ? null : new MessageId(b);
+	}
+
+	private GroupId getGroupId(BdfDictionary d, String key)
+			throws FormatException {
+		return new GroupId(d.getRaw(key));
+	}
+
+	private Author getAuthor(BdfDictionary d, String key)
+			throws FormatException {
+		return clientHelper.parseAndValidateAuthor(d.getList(key));
+	}
+
+	@Nullable
+	private Map<TransportId, KeySetId> parseTransportKeys(
+			@Nullable BdfDictionary d) throws FormatException {
+		if (d == null) return null;
+		Map<TransportId, KeySetId> map = new HashMap<>(d.size());
+		for (String key : d.keySet()) {
+			map.put(new TransportId(key),
+					new KeySetId(d.getLong(key).intValue()));
+		}
+		return map;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
new file mode 100644
index 000000000..ecffd8110
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
@@ -0,0 +1,37 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum MailboxState implements State {
+
+	START(0),
+	AWAIT_LOCAL_RESPONSE(1),
+	LOCAL_DECLINED(2),
+	LOCAL_ACCEPTED(3),
+	AWAIT_REMOTE_RESPONSE(4),
+	REMOTE_DECLINED(5),
+	REMOTE_ACCEPTED(6),
+	CONTACT_ADDED(7);
+
+	private final int value;
+
+	MailboxState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static MailboxState fromValue(int value) throws FormatException {
+		for (MailboxState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageMetadata.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageMetadata.java
new file mode 100644
index 000000000..9c0c50617
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageMetadata.java
@@ -0,0 +1,55 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MessageMetadata {
+
+	private final MessageType type;
+	@Nullable
+	private final SessionId sessionId;
+	private final long timestamp, counter;
+	private final boolean local;
+	private final boolean available;
+
+	MessageMetadata(MessageType type, @Nullable SessionId sessionId,
+			long timestamp, boolean local, long counter, boolean available) {
+		this.type = type;
+		this.sessionId = sessionId;
+		this.timestamp = timestamp;
+		this.local = local;
+		this.counter = counter;
+		this.available = available;
+	}
+
+	MessageType getMessageType() {
+		return type;
+	}
+
+	@Nullable
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	boolean isLocal() {
+		return local;
+	}
+
+	long getCounter() {
+		return counter;
+	}
+
+	boolean isAvailableToAnswer() {
+		return available;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java
new file mode 100644
index 000000000..0b35f7721
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java
@@ -0,0 +1,30 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum MessageType {
+
+	MAILBOX_REQUEST(0), MAILBOX_ACCEPT(1), INTRODUCEE_REQUEST(2),
+	INTRODUCEE_ACCEPT(3), CONTACT_INFO(4), MAILBOX_AUTH(5), DECLINE(5), ABORT(6);
+
+	private final int value;
+
+	MessageType(int value) {
+		this.value = value;
+	}
+
+	int getValue() {
+		return value;
+	}
+
+	static MessageType fromValue(int value) throws FormatException {
+		for (MessageType m : values()) if (m.value == value) return m;
+		throw new FormatException();
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
new file mode 100644
index 000000000..bc05091b6
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
@@ -0,0 +1,218 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactManager;
+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.IdentityManager;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionResponseReceivedEvent;
+import org.briarproject.briar.api.mailbox.event.MailboxIntroductionSucceededEvent;
+
+import javax.inject.Inject;
+
+import static org.briarproject.briar.mailbox.OwnerState.ADDED;
+import static org.briarproject.briar.mailbox.OwnerState.AWAIT_AUTH_M;
+import static org.briarproject.briar.mailbox.OwnerState.AWAIT_RESPONSE_B;
+import static org.briarproject.briar.mailbox.OwnerState.AWAIT_RESPONSE_M;
+
+class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
+
+	@Inject
+	OwnerProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager,
+			MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		super(db, clientHelper, contactManager, contactGroupFactory,
+				identityManager, messageParser, messageEncoder,
+				clock, crypto, keyManager, transportPropertyManager);
+	}
+
+	@Override
+	public OwnerSession onRequestAction(Transaction txn, OwnerSession s,
+			long timestamp) throws DbException {
+		switch (s.getState()) {
+			case START:
+				return onLocalRequest(txn, s, timestamp);
+			case AWAIT_RESPONSE_M:
+			case M_DECLINED:
+			case AWAIT_RESPONSE_B:
+			case B_DECLINED:
+			case AWAIT_AUTH_M:
+			case ADDED:
+				throw new ProtocolStateException(); // Invalid in these states
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Override
+	public OwnerSession onAcceptAction(Transaction txn, OwnerSession session,
+			long timestamp) throws DbException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public OwnerSession onDeclineAction(Transaction txn, OwnerSession session,
+			long timestamp) throws DbException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public OwnerSession onRequestMessage(Transaction txn, OwnerSession session,
+			RequestMessage m) throws DbException, FormatException {
+		//		return abort(txn, session);
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public OwnerSession onMailboxAcceptMessage(Transaction txn, OwnerSession s,
+			MailboxAcceptMessage m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case START:
+			case AWAIT_RESPONSE_M:
+				return onMailboxAccept(txn, s, m);
+			case M_DECLINED:
+			case AWAIT_RESPONSE_B:
+			case B_DECLINED:
+			case AWAIT_AUTH_M:
+			case ADDED:
+				throw new ProtocolStateException(); // Invalid in these states
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Override
+	public OwnerSession onIntroduceeRequestMessage(Transaction txn,
+			OwnerSession session, IntroduceeRequestMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public OwnerSession onContactAcceptMessage(Transaction txn,
+			OwnerSession session, IntroduceeRequestMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	private OwnerSession onMailboxAccept(Transaction txn, OwnerSession s,
+			MailboxAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getMailbox().lastRemoteMessageId,
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendIntroduceeRequestMessage(txn, s.getIntroducee(),
+				clock.currentTimeMillis(), s.getMailbox().author,
+				m.getEphemeralPublicKey(),
+				m.getAcceptTimestamp(),
+				s.getMessageCounter());
+		broadcastMailboxIntroductionResponseReceived(txn, s.getMailbox().author,
+				s.getIntroducee().author);
+		return new OwnerSession(s.getSessionId(), AWAIT_RESPONSE_B,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), m.getMessageId()),
+				new Introducee(s.getIntroducee(), forward),
+				s.getMessageCounter());
+	}
+
+	@Override
+	public OwnerSession onDeclineMessage(Transaction txn, OwnerSession session,
+			DeclineMessage m) throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public OwnerSession onAuthMessage(Transaction txn, OwnerSession s,
+			MailboxAuthMessage m) throws DbException, FormatException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getMailbox().getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendMailboxAuthMessage(txn, s.getIntroducee(),
+				clock.currentTimeMillis(), m.getTransportProperties(),
+				m.getMac(), m.getSignature(), s.getMessageCounter());
+		Contact c = contactManager
+				.getContact(txn, s.getIntroducee().author.getId(),
+						identityManager.getLocalAuthor().getId());
+		db.setMailboxForContact(txn, c.getId(), null, c.getId());
+		txn.attach(new MailboxIntroductionSucceededEvent(c));
+		return new OwnerSession(s.getSessionId(), ADDED,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), m.getMessageId()),
+				new Introducee(s.getIntroducee(), forward),
+				s.getMessageCounter());
+	}
+
+	@Override
+	public OwnerSession onAbortMessage(Transaction txn, OwnerSession session,
+			AbortMessage m) throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public OwnerSession onIntroduceeAcceptMessage(Transaction txn,
+			OwnerSession s, IntroduceeAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getIntroducee().getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendIntroduceeResponseMessage(txn, s.getMailbox(),
+				s.getMailbox().lastRemoteMessageId,
+				clock.currentTimeMillis(),
+				m.getEphemeralPublicKey(),
+				m.getMac(), m.getSignature(),
+				m.getAcceptTimestamp(),
+				s.getMessageCounter());
+		return new OwnerSession(s.getSessionId(), AWAIT_AUTH_M,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), forward),
+				new Introducee(s.getIntroducee(), m.getMessageId()),
+				s.getMessageCounter());
+	}
+
+	private OwnerSession onLocalRequest(Transaction txn, OwnerSession s,
+			long timestamp) throws DbException {
+		// Send REQUEST messages
+		long maxIntroduceeTimestamp =
+				Math.max(getLocalTimestamp(s, s.getMailbox()),
+						getLocalTimestamp(s, s.getIntroducee()));
+		long localTimestamp = Math.max(timestamp, maxIntroduceeTimestamp);
+		Message sentMailbox =
+				sendMailboxRequestMessage(txn, s.getMailbox(), localTimestamp,
+						s.getIntroducee().author, s.getMessageCounter());
+		// Move to the AWAIT_RESPONSES state
+		Introducee mailbox = new Introducee(s.getMailbox(), sentMailbox);
+		Introducee b = new Introducee(s.getIntroducee().sessionId,
+				s.getIntroducee().groupId, s.getIntroducee().author);
+		return new OwnerSession(s.getSessionId(), AWAIT_RESPONSE_M,
+				localTimestamp, mailbox, b, s.getMessageCounter());
+	}
+
+	private long getLocalTimestamp(OwnerSession s, PeerSession p) {
+		return getLocalTimestamp(p.getLocalTimestamp(),
+				s.getRequestTimestamp());
+	}
+
+	private OwnerSession abort(Transaction txn, Session s) {
+		return null;
+	}
+
+	void broadcastMailboxIntroductionResponseReceived(Transaction txn,
+			Author from, Author to) {
+		MailboxIntroductionResponseReceivedEvent e =
+				new MailboxIntroductionResponseReceivedEvent(from, to);
+		txn.attach(e);
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
new file mode 100644
index 000000000..d07b5ac22
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
@@ -0,0 +1,52 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.Role;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.api.mailbox.Role.OWNER;
+
+@Immutable
+@NotNullByDefault
+class OwnerSession extends Session<OwnerState> {
+
+	private final Introducee mailbox, introducee;
+
+	OwnerSession(SessionId sessionId, OwnerState state,
+			long requestTimestamp, Introducee mailbox,
+			Introducee introducee, long sessionCounter) {
+		super(sessionId, state, requestTimestamp, sessionCounter);
+		this.mailbox = mailbox;
+		this.introducee = introducee;
+	}
+
+	OwnerSession(SessionId sessionId, GroupId groupIdA, Author authorA,
+			GroupId groupIdB, Author authorB, long sessionCounter) {
+		this(sessionId, OwnerState.START, -1,
+				new Introducee(sessionId, groupIdA, authorA),
+				new Introducee(sessionId, groupIdB, authorB), sessionCounter);
+	}
+
+	public static OwnerSession finished(OwnerSession s) {
+		return null;
+	}
+
+	@Override
+	Role getRole() {
+		return OWNER;
+	}
+
+	Introducee getMailbox() {
+		return mailbox;
+	}
+
+	Introducee getIntroducee() {
+		return introducee;
+	}
+
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java
new file mode 100644
index 000000000..ba0531f2e
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java
@@ -0,0 +1,36 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum OwnerState implements State {
+
+	START(0),
+	AWAIT_RESPONSE_M(1),
+	M_DECLINED(2),
+	AWAIT_RESPONSE_B(3),
+	B_DECLINED(4),
+	AWAIT_AUTH_M(5),
+	ADDED(6);
+
+	private final int value;
+
+	OwnerState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static OwnerState fromValue(int value) throws FormatException {
+		for (OwnerState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java
new file mode 100644
index 000000000..69b944943
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java
@@ -0,0 +1,25 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface PeerSession {
+
+	SessionId getSessionId();
+
+	GroupId getContactGroupId();
+
+	long getLocalTimestamp();
+
+	@Nullable
+	MessageId getLastLocalMessageId();
+
+	@Nullable
+	MessageId getLastRemoteMessageId();
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
new file mode 100644
index 000000000..2ab36fcc1
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
@@ -0,0 +1,46 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface ProtocolEngine<S extends Session> {
+
+	S onRequestAction(Transaction txn, S session, long timestamp)
+			throws DbException;
+
+	S onAcceptAction(Transaction txn, S session, long timestamp)
+			throws DbException;
+
+	S onDeclineAction(Transaction txn, S session, long timestamp)
+			throws DbException;
+
+	S onRequestMessage(Transaction txn, S session, RequestMessage m)
+			throws DbException, FormatException;
+
+	S onMailboxAcceptMessage(Transaction txn, S session,
+			MailboxAcceptMessage m)
+			throws DbException, FormatException;
+
+	S onIntroduceeRequestMessage(Transaction txn,
+			S session, IntroduceeRequestMessage m)
+			throws DbException, FormatException;
+
+	S onContactAcceptMessage(Transaction txn, S session,
+			IntroduceeRequestMessage m)
+			throws DbException, FormatException;
+
+	S onDeclineMessage(Transaction txn, S session, DeclineMessage m)
+			throws DbException, FormatException;
+
+	S onAuthMessage(Transaction txn, S session, MailboxAuthMessage m)
+			throws DbException, FormatException;
+
+	S onAbortMessage(Transaction txn, S session, AbortMessage m)
+			throws DbException, FormatException;
+
+	S onIntroduceeAcceptMessage(Transaction txn, S session,
+			IntroduceeAcceptMessage acceptMessage) throws DbException;
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java
new file mode 100644
index 000000000..2e25c9094
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java
@@ -0,0 +1,28 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class RequestMessage extends AbstractMailboxIntroductionMessage {
+
+
+	private final Author author;
+
+	protected RequestMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			Author author) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.author = author;
+	}
+
+	public Author getAuthor() {
+		return author;
+	}
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/Session.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/Session.java
new file mode 100644
index 000000000..ab65947ed
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/Session.java
@@ -0,0 +1,44 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.mailbox.Role;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class Session<S extends State> {
+
+	private final SessionId sessionId;
+	private final S state;
+	private final long requestTimestamp;
+	private long messageCounter;
+
+	Session(SessionId sessionId, S state, long requestTimestamp,
+			long messageCounter) {
+		this.sessionId = sessionId;
+		this.state = state;
+		this.requestTimestamp = requestTimestamp;
+		this.messageCounter = messageCounter;
+	}
+
+	abstract Role getRole();
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	S getState() {
+		return state;
+	}
+
+	long getRequestTimestamp() {
+		return requestTimestamp;
+	}
+
+	synchronized long getMessageCounter() {
+		return messageCounter;
+	}
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/briar/mailbox/State.java b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/State.java
new file mode 100644
index 000000000..66a2e80d1
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/briar/mailbox/State.java
@@ -0,0 +1,7 @@
+package org.briarproject.briar.mailbox;
+
+interface State {
+
+	int getValue();
+
+}
diff --git a/mailbox-core/witness.gradle b/mailbox-core/witness.gradle
new file mode 100644
index 000000000..ace73b9df
--- /dev/null
+++ b/mailbox-core/witness.gradle
@@ -0,0 +1,4 @@
+dependencyVerification {
+    verify = [
+    ]
+}
diff --git a/settings.gradle b/settings.gradle
index 1f8110466..1c273d335 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,3 +6,4 @@ include ':briar-api'
 include ':briar-core'
 include ':briar-android'
 include ':mailbox-android'
+include ':mailbox-core'
-- 
GitLab