From cacb04eca0c3344797ea8dc6c57ae52a262fa551 Mon Sep 17 00:00:00 2001
From: goapunk <goapunk@riseup.net>
Date: Thu, 20 Sep 2018 13:12:56 +0200
Subject: [PATCH] fix

---
 .../bramble/api/contact/ContactManager.java   |   8 +
 .../bramble/contact/ContactManagerImpl.java   |  32 ++
 .../mailbox/MailboxIntroductionManager.java   |  12 +-
 .../briarproject/briar/api/mailbox/Role.java  |  29 ++
 .../briar/mailbox/AbstractProtocolEngine.java | 284 +++++++-------
 .../briar/mailbox/IntroduceeSession.java      | 361 +++++++-----------
 .../briar/mailbox/IntroductionConstants.java  |   1 +
 .../mailbox/MailboxIntroductionCrypto.java    | 101 +++++
 .../MailboxIntroductionCryptoImpl.java        | 237 ++++++++++++
 .../MailboxIntroductionManagerImpl.java       | 147 ++++++-
 .../mailbox/MailboxIntroductionModule.java    |  48 +++
 .../mailbox/MailboxIntroductionValidator.java | 172 +++++++++
 .../briar/mailbox/MailboxProtocolEngine.java  |   4 -
 .../briar/mailbox/MailboxSession.java         | 161 ++++++++
 .../briar/mailbox/MessageEncoder.java         |  39 +-
 .../briar/mailbox/MessageEncoderImpl.java     | 248 ++++++------
 .../briar/mailbox/MessageParser.java          |  17 +-
 .../briar/mailbox/MessageParserImpl.java      |  67 ++--
 .../briar/mailbox/OwnerProtocolEngine.java    |  10 +-
 .../briar/mailbox/OwnerSession.java           |  62 +--
 .../briarproject/briar/mailbox/Session.java   |   2 +-
 .../briar/mailbox/SessionEncoder.java         |  18 +
 .../briar/mailbox/SessionEncoderImpl.java     | 155 ++++++++
 .../briar/mailbox/SessionParser.java          |  23 ++
 .../briar/mailbox/SessionParserImpl.java      | 210 ++++++++++
 .../IntroductionIntegrationTestComponent.java |   2 +
 .../MailboxIntroductionIntegrationTest.java   | 352 +++++++++++++++++
 ...xIntroductionIntegrationTestComponent.java |  78 ++++
 .../briar/test/BriarIntegrationTest.java      |  99 ++++-
 .../test/BriarIntegrationTestComponent.java   |   7 +
 30 files changed, 2351 insertions(+), 635 deletions(-)
 create mode 100644 briar-api/src/main/java/org/briarproject/briar/api/mailbox/Role.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
 delete mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoder.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoderImpl.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParser.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParserImpl.java
 create mode 100644 briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTest.java
 create mode 100644 briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTestComponent.java

diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java
index d8693d79f..e2b3299e1 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java
@@ -49,6 +49,10 @@ public interface ContactManager {
 			long timestamp, boolean alice, boolean verified, boolean active)
 			throws DbException;
 
+	ContactId addPrivateMailbox(Author remote, AuthorId local, SecretKey master,
+			long timestamp, boolean alice)
+			throws DbException;
+
 	/**
 	 * Add a private Mailbox
 	 */
@@ -58,6 +62,10 @@ public interface ContactManager {
 	ContactId addMailboxOwner(Transaction txn, Author remote, AuthorId local,
 			SecretKey master, long timestamp, boolean alice) throws DbException;
 
+	ContactId addMailboxOwner(Author remote, AuthorId local,
+			SecretKey secretKey, long currentTimeMillis, boolean alice)
+			throws DbException;
+
 	/**
 	 * Returns the contact with the given ID.
 	 */
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
index 976f270f8..f6b8fb515 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
@@ -91,6 +91,22 @@ class ContactManagerImpl implements ContactManager {
 		return c;
 	}
 
+	@Override
+	public ContactId addPrivateMailbox(Author remote, AuthorId local,
+			SecretKey master,
+			long timestamp, boolean alice)
+			throws DbException {
+		ContactId c;
+		Transaction txn = db.startTransaction(false);
+		try {
+			c = addPrivateMailbox(txn, remote, local, master, timestamp, alice);
+			db.commitTransaction(txn);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return c;
+	}
+
 	@Override
 	public ContactId addPrivateMailbox(Transaction txn, Author remote,
 			AuthorId local, SecretKey master, long timestamp, boolean alice)
@@ -106,6 +122,22 @@ class ContactManagerImpl implements ContactManager {
 		return c;
 	}
 
+
+	@Override
+	public ContactId addMailboxOwner(Author remote, AuthorId local,
+			SecretKey master, long timestamp, boolean alice)
+			throws DbException {
+		ContactId c;
+		Transaction txn = db.startTransaction(false);
+		try {
+			c = addMailboxOwner(txn, remote, local, master, timestamp, alice);
+			db.commitTransaction(txn);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return c;
+	}
+
 	@Override
 	public ContactId addMailboxOwner(Transaction txn, Author remote,
 			AuthorId local, SecretKey master, long timestamp, boolean alice)
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/mailbox/MailboxIntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/mailbox/MailboxIntroductionManager.java
index 3edb9ab0b..709be9340 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/mailbox/MailboxIntroductionManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/mailbox/MailboxIntroductionManager.java
@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.contact.PrivateMailbox;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.sync.Group;
 
 public interface MailboxIntroductionManager {
 	/**
@@ -25,6 +26,15 @@ public interface MailboxIntroductionManager {
 
 	void contactAdded(Transaction txn, Contact contact) throws DbException;
 
-	void privateMailboxAdded(Transaction txn, PrivateMailbox privateMailbox) throws DbException;
+	void privateMailboxAdded(Transaction txn, PrivateMailbox privateMailbox)
+			throws DbException;
 
+	/**
+	 * Sends two initial introduction messages.
+	 */
+	void makeIntroduction(PrivateMailbox privateMailbox, Contact contact, long timestamp)
+			throws DbException;
+
+
+	Group getContactGroup(Contact c);
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/mailbox/Role.java b/briar-api/src/main/java/org/briarproject/briar/api/mailbox/Role.java
new file mode 100644
index 000000000..4ed40c1d0
--- /dev/null
+++ b/briar-api/src/main/java/org/briarproject/briar/api/mailbox/Role.java
@@ -0,0 +1,29 @@
+package org.briarproject.briar.api.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public enum Role {
+
+	OWNER(0), MAILBOX(1), INTRODUCEE(2);
+
+	private final int value;
+
+	Role(int value) {
+		this.value = value;
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public static Role fromValue(int value) throws FormatException {
+		for (Role r : values()) if (r.value == value) return r;
+		throw new FormatException();
+	}
+
+}
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 07231a624..ad3cd17b2 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
@@ -25,153 +25,157 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 import static org.briarproject.briar.mailbox.MessageType.ABORT;
-import static org.briarproject.briar.mailbox.MessageType.CONTACT_ACCEPT;
 import static org.briarproject.briar.mailbox.MessageType.CONTACT_REQUEST;
 import static org.briarproject.briar.mailbox.MessageType.DECLINE;
 import static org.briarproject.briar.mailbox.MessageType.MAILBOX_ACCEPT;
-import static org.briarproject.briar.mailbox.MessageType.MAILBOX_INFO;
 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 MessageTracker messageTracker;
-    protected final IdentityManager identityManager;
-    protected final MessageParser messageParser;
-    protected final MessageEncoder messageEncoder;
-    protected final Clock clock;
-
-    AbstractProtocolEngine(
-            DatabaseComponent db,
-            ClientHelper clientHelper,
-            ContactManager contactManager,
-            ContactGroupFactory contactGroupFactory,
-            MessageTracker messageTracker,
-            IdentityManager identityManager,
-            MessageParser messageParser,
-            MessageEncoder messageEncoder,
-            Clock clock) {
-        this.db = db;
-        this.clientHelper = clientHelper;
-        this.contactManager = contactManager;
-        this.contactGroupFactory = contactGroupFactory;
-        this.messageTracker = messageTracker;
-        this.identityManager = identityManager;
-        this.messageParser = messageParser;
-        this.messageEncoder = messageEncoder;
-        this.clock = clock;
-    }
-
-    Message sendMailboxRequestMessage(Transaction txn, PeerSession s, long timestamp) throws DbException {
-        Message m = messageEncoder
-                .encodeRequestMessage(s.getContactGroupId(), timestamp, s.getLastLocalMessageId());
-        sendMessage(txn, MAILBOX_REQUEST, s.getSessionId(), m);
-        return m;
-    }
-
-    Message sendMailboxAcceptMessage(Transaction txn, PeerSession s,
-                                     long timestamp,
-                                     byte[] ephemeralPublicKey, long acceptTimestamp,
-                                     Map<TransportId, TransportProperties> transportProperties,
-                                     boolean visible) throws DbException {
-        Message m = messageEncoder
-                .encodeAcceptMessage(s.getContactGroupId(), timestamp,
-                        s.getLastLocalMessageId(), s.getSessionId(),
-                        ephemeralPublicKey, acceptTimestamp,
-                        transportProperties);
-        sendMessage(txn, MAILBOX_ACCEPT, s.getSessionId(), m);
-        return m;
-    }
-
-    Message sendContactRequestMessage(Transaction txn, PeerSession s,
-                                      long timestamp, Author author, @Nullable String message)
-            throws DbException {
-        Message m = messageEncoder
-                .encodeRequestMessage(s.getContactGroupId(), timestamp,
-                        s.getLastLocalMessageId(), author, message);
-        sendMessage(txn, CONTACT_REQUEST, s.getSessionId(), m);
-        return m;
-    }
-
-    Message sendContactAcceptMessage(Transaction txn, PeerSession s,
-                                     long timestamp,
-                                     byte[] ephemeralPublicKey, long acceptTimestamp,
-                                     Map<TransportId, TransportProperties> transportProperties,
-                                     boolean visible) throws DbException {
-        Message m = messageEncoder
-                .encodeAcceptMessage(s.getContactGroupId(), timestamp,
-                        s.getLastLocalMessageId(), s.getSessionId(),
-                        ephemeralPublicKey, acceptTimestamp,
-                        transportProperties);
-        sendMessage(txn, CONTACT_ACCEPT, s.getSessionId(), m);
-        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
-                .encodeAcceptMessage(s.getContactGroupId(), timestamp,
-                        s.getLastLocalMessageId(), s.getSessionId(),
-                        ephemeralPublicKey, acceptTimestamp,
-                        transportProperties);
-        sendMessage(txn, MAILBOX_INFO, 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);
-        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);
-        return m;
-    }
-
-    private void sendMessage(Transaction txn, MessageType type,
-                             SessionId sessionId, Message m)
-            throws DbException {
-        BdfDictionary meta = messageEncoder
-                .encodeMetadata(type, sessionId, m.getTimestamp(), true, true);
-        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
-        );
-    }
+		implements ProtocolEngine<S> {
+
+	protected final DatabaseComponent db;
+	protected final ClientHelper clientHelper;
+	protected final ContactManager contactManager;
+	protected final ContactGroupFactory contactGroupFactory;
+	protected final MessageTracker messageTracker;
+	protected final IdentityManager identityManager;
+	protected final MessageParser messageParser;
+	protected final MessageEncoder messageEncoder;
+	protected final Clock clock;
+
+	AbstractProtocolEngine(
+			DatabaseComponent db,
+			ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			MessageTracker messageTracker,
+			IdentityManager identityManager,
+			MessageParser messageParser,
+			MessageEncoder messageEncoder,
+			Clock clock) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+		this.contactManager = contactManager;
+		this.contactGroupFactory = contactGroupFactory;
+		this.messageTracker = messageTracker;
+		this.identityManager = identityManager;
+		this.messageParser = messageParser;
+		this.messageEncoder = messageEncoder;
+		this.clock = clock;
+	}
+
+	Message sendMailboxRequestMessage(Transaction txn, PeerSession s,
+			long timestamp) throws DbException {
+		Message m = messageEncoder
+				.encodeRequestMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId());
+		sendMessage(txn, MAILBOX_REQUEST, s.getSessionId(), m);
+		return m;
+	}
+
+	Message sendMailboxAcceptMessage(Transaction txn, PeerSession s,
+			long timestamp,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties,
+			boolean visible) throws DbException {
+		Message m = messageEncoder
+				.encodeMailboxAcceptMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId(),
+						ephemeralPublicKey, acceptTimestamp,
+						transportProperties);
+		sendMessage(txn, MAILBOX_ACCEPT, s.getSessionId(), m);
+		return m;
+	}
+
+	Message sendContactRequestMessage(Transaction txn, PeerSession s,
+			long timestamp, Author author, @Nullable String message)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeRequestMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId());
+		sendMessage(txn, CONTACT_REQUEST, s.getSessionId(), m);
+		return m;
+	}
+
+	/*
+	Message sendContactAcceptMessage(Transaction txn, PeerSession s,
+			long timestamp,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties,
+			boolean visible) throws DbException {
+		Message m = messageEncoder
+				.encodeAcceptMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId(),
+						ephemeralPublicKey, acceptTimestamp,
+						transportProperties);
+		sendMessage(txn, CONTACT_ACCEPT, s.getSessionId(), m);
+		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);
+		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);
+		return m;
+	}
+
+	private void sendMessage(Transaction txn, MessageType type,
+			SessionId sessionId, Message m)
+			throws DbException {
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, sessionId, m.getTimestamp(), true);
+		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/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
index 7f8b5b60a..0ae5cf1f1 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,244 +1,157 @@
 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.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.bramble.api.transport.KeySetId;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction.Role;
 
 import java.util.Map;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.briar.api.introduction.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 Session<IntroduceeState> implements PeerSession {
-
-    private final GroupId contactGroupId;
-    private final Author introducer;
-    private final Local local;
-    private final Remote remote;
-    @Nullable
-    private final byte[] masterKey;
-    @Nullable
-    private final Map<TransportId, KeySetId> transportKeys;
-
-    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, sessionCounter);
-        this.contactGroupId = contactGroupId;
-        this.introducer = introducer;
-        this.local = local;
-        this.remote = remote;
-        this.masterKey = masterKey;
-        this.transportKeys = transportKeys;
-    }
-
-    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 addRemoteRequest(
-            IntroduceeSession s,
-            IntroduceeState state,
-            ContactRequestMessage m, byte[] ephemeralPublicKey,
-            byte[] ephemeralPrivateKey,
-            long acceptTimestamp, boolean alice) {
-        Local local = new Local(alice, m.getMessageId(),
-                m.getTimestamp(), ephemeralPublicKey, ephemeralPrivateKey,
-                acceptTimestamp, null);
-        Remote remote =
-                new Remote(!alice, s.remote.author, m.getMessageId(),
-                        m.getEphemeralPublicKey(), null, m.getAcceptTimestamp(),
-                        null);
-        return new IntroduceeSession(s.getSessionId(), state, m.getTimestamp(),
-                s.contactGroupId, s.introducer, local, remote, s.masterKey,
-                s.transportKeys, s.getSessionCounter());
-    }
-
-    static IntroduceeSession addLocalAuth(
-            IntroduceeSession 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 IntroduceeSession(s.getSessionId(), state,
-                s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
-                remote, masterKey.getBytes(), s.transportKeys,
-                s.getSessionCounter());
-    }
-
-    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.getSessionCounter());
-    }
-
-    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.getSessionCounter());
-    }
-
-    @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;
-    }
-
-    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;
-        }
-
-        private 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;
-        }
-
-        private Remote(Remote s, @Nullable MessageId lastMessageId) {
-            this(s.alice, s.author, lastMessageId, s.ephemeralPublicKey,
-                    s.transportProperties, s.acceptTimestamp, s.macKey);
-        }
-
-    }
+abstract class IntroduceeSession extends Session<IntroduceeState>
+		implements PeerSession {
+
+	final GroupId contactGroupId;
+	final Author introducer;
+	final Local local;
+	final Remote remote;
+	@Nullable
+	final byte[] masterKey;
+	@Nullable
+	final Map<TransportId, KeySetId> transportKeys;
+
+	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, 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/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java
index 940295983..fa6bc0624 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroductionConstants.java
@@ -15,6 +15,7 @@ interface IntroductionConstants {
 
 	// 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";
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
new file mode 100644
index 000000000..e4eb75246
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCrypto.java
@@ -0,0 +1,101 @@
+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);
+
+	/**
+	 * 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);
+
+	/**
+	 * Generates an agreement key pair.
+	 */
+	KeyPair generateKeyPair();
+
+	/**
+	 * Derives a session master key for Alice or Bob.
+	 *
+	 * @return The secret master key
+	 */
+	SecretKey deriveMasterKey(IntroduceeSession s)
+			throws GeneralSecurityException;
+
+	/**
+	 * Derives a MAC key from the session's master key for Alice or Bob.
+	 *
+	 * @param masterKey The key returned by {@link #deriveMasterKey(IntroduceeSession)}
+	 * @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, IntroduceeSession s,
+			AuthorId localAuthorId);
+
+	/**
+	 * Verifies a received MAC
+	 *
+	 * @param mac The MAC to verify
+	 * as returned by {@link #deriveMasterKey(IntroduceeSession)}
+	 * @throws GeneralSecurityException if the verification fails
+	 */
+	void verifyAuthMac(byte[] mac, IntroduceeSession 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, IntroduceeSession s)
+			throws GeneralSecurityException;
+
+	/**
+	 * Generates a MAC using the local MAC key.
+	 */
+	byte[] activateMac(IntroduceeSession s);
+
+	/**
+	 * Verifies a MAC from an ACTIVATE message.
+	 *
+	 * @throws GeneralSecurityException if the verification fails
+	 */
+	void verifyActivateMac(byte[] mac, IntroduceeSession s)
+			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
new file mode 100644
index 000000000..8e117c980
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionCryptoImpl.java
@@ -0,0 +1,237 @@
+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.IntroduceeSession.Common;
+import static org.briarproject.briar.mailbox.IntroduceeSession.Local;
+import static org.briarproject.briar.mailbox.IntroduceeSession.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 = isAlice(local.getId(), remote.getId());
+		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
+	public boolean isAlice(AuthorId local, AuthorId remote) {
+		return local.compareTo(remote) < 0;
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public SecretKey deriveMasterKey(IntroduceeSession s)
+			throws GeneralSecurityException {
+		return deriveMasterKey(
+				s.getLocal().ephemeralPublicKey,
+				s.getLocal().ephemeralPrivateKey,
+				s.getRemote().ephemeralPublicKey,
+				s.getLocal().alice
+		);
+	}
+
+	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, IntroduceeSession 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, IntroduceeSession 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, IntroduceeSession 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(IntroduceeSession 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, IntroduceeSession 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/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
index d68950aa4..69258f5ad 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
@@ -2,26 +2,39 @@ 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.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.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.versioning.ClientVersioningManager;
+import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
 import org.briarproject.briar.client.BdfIncomingMessageHook;
 
+import java.util.Map;
 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.versioning.ClientVersioningManager.ClientVersioningHook;
+import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
+import static org.briarproject.briar.api.mailbox.Role.OWNER;
 
 class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 		implements MailboxIntroductionManager, Client, ClientVersioningHook,
@@ -29,13 +42,48 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 
 	private static final Logger LOG =
 			Logger.getLogger(MailboxIntroductionManagerImpl.class.getName());
+	private final ClientVersioningManager clientVersioningManager;
+	private final ContactGroupFactory contactGroupFactory;
+	private final ContactManager contactManager;
+	private final MessageParser messageParser;
+	//	private final SessionEncoder sessionEncoder;
+	private final SessionParser sessionParser;
+	private final OwnerProtocolEngine ownerProtocolEngine;
+	private final SessionEncoder sessionEncoder;
+	private final MailboxIntroductionCrypto crypto;
+	//private final MailboxProtocolEngine mailboxProtocolEngine;
+	private final IdentityManager identityManager;
+
+	private final Group localGroup;
 
 	@Inject
 	MailboxIntroductionManagerImpl(
 			DatabaseComponent db,
 			ClientHelper clientHelper,
-			MetadataParser metadataParser) {
+			ClientVersioningManager clientVersioningManager,
+			MetadataParser metadataParser,
+			ContactGroupFactory contactGroupFactory,
+			ContactManager contactManager,
+			MessageParser messageParser,
+			SessionParser sessionParser,
+			OwnerProtocolEngine ownerProtocolEngine,
+			SessionEncoder sessionEncoder,
+			MailboxIntroductionCrypto crypto,
+		//	MailboxProtocolEngine mailboxProtocolEngine,
+			IdentityManager identityManager) {
 		super(db, clientHelper, metadataParser);
+		this.clientVersioningManager = clientVersioningManager;
+		this.contactGroupFactory = contactGroupFactory;
+		this.contactManager = contactManager;
+		this.messageParser = messageParser;
+		this.sessionParser = sessionParser;
+		this.ownerProtocolEngine = ownerProtocolEngine;
+		this.sessionEncoder = sessionEncoder;
+		this.crypto = crypto;
+//		this.mailboxProtocolEngine = mailboxProtocolEngine;
+		this.identityManager = identityManager;
+		this.localGroup =
+				contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
 	}
 
 	@Override
@@ -72,6 +120,93 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 		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());
+			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) {
+			d = sessionEncoder
+					.encodeIntroduceeSession((IntroduceeSession) session);
+		} else {
+			throw new AssertionError();
+		}
+		try {
+			clientHelper.mergeMessageMetadata(txn, storageId, d);
+		} catch (FormatException e) {
+			throw new AssertionError();
+		}
+	}
+
+	@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");
@@ -88,4 +223,14 @@ class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
 
 	}
 
+	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/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
index 967b3d6e8..14420f789 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
@@ -1,9 +1,13 @@
 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;
@@ -20,10 +24,28 @@ import static org.briarproject.briar.api.mailbox.MailboxIntroductionManager.MINO
 public class MailboxIntroductionModule {
 
 	public static class EagerSingletons {
+		@Inject
+		MailboxIntroductionValidator mailboxIntroductionValidator;
 		@Inject
 		MailboxIntroductionManager mailboxIntroductionManager;
 	}
 
+	@Provides
+	@Singleton
+	MailboxIntroductionValidator provideValidator(
+			ValidationManager validationManager,
+			MessageEncoder messageEncoder, MetadataEncoder metadataEncoder,
+			ClientHelper clientHelper, Clock clock) {
+		MailboxIntroductionValidator mailboxIntroductionValidator =
+				new MailboxIntroductionValidator(messageEncoder, clientHelper,
+						metadataEncoder, clock);
+		validationManager
+				.registerMessageValidator(IntroductionManager.CLIENT_ID,
+						IntroductionManager.MAJOR_VERSION,
+						mailboxIntroductionValidator);
+		return mailboxIntroductionValidator;
+	}
+
 	@Provides
 	@Singleton
 	MailboxIntroductionManager provideMailboxIntroductionManager(
@@ -39,4 +61,30 @@ public class MailboxIntroductionModule {
 				MINOR_VERSION, mailboxIntroductionManager);
 		return mailboxIntroductionManager;
 	}
+
+	@Provides
+	MessageParser provideMessageParser(MessageParserImpl messageParser) {
+		return messageParser;
+	}
+
+	@Provides
+	MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) {
+		return messageEncoder;
+	}
+
+	@Provides
+	SessionParser provideSessionParser(SessionParserImpl sessionParser) {
+		return sessionParser;
+	}
+
+	@Provides
+	SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) {
+		return sessionEncoder;
+	}
+
+	@Provides
+	MailboxIntroductionCrypto provideMailboxIntroductionCrypto(
+			MailboxIntroductionCryptoImpl mailboxIntroductionCrypto) {
+		return mailboxIntroductionCrypto;
+	}
 }
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
new file mode 100644
index 000000000..46cf6776f
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionValidator.java
@@ -0,0 +1,172 @@
+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.util.ValidationUtils.checkLength;
+import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
+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 MessageEncoder messageEncoder;
+
+	MailboxIntroductionValidator(
+			MessageEncoder 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 MAILBOX_AUTH:
+				return validateAuthMessage(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);
+
+		String msg = body.getOptionalString(3);
+		checkLength(msg, 1, MAX_REQUEST_MESSAGE_LENGTH);
+
+		BdfDictionary meta =
+				messageEncoder.encodeRequestMetadata(m.getTimestamp());
+		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();
+
+		BdfDictionary transportProperties = body.getDictionary(5);
+		if (transportProperties.size() < 1) throw new FormatException();
+		clientHelper
+				.parseAndValidateTransportPropertiesMap(transportProperties);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(MAILBOX_ACCEPT, sessionId, m.getTimestamp(),
+						false);
+		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);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		byte[] mac = body.getRaw(3);
+		checkLength(mac, MAC_BYTES);
+
+		byte[] signature = body.getRaw(4);
+		checkLength(signature, 1, MAX_SIGNATURE_BYTES);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(MAILBOX_AUTH, sessionId, m.getTimestamp(),
+						false);
+		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);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+}
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
deleted file mode 100644
index 511250dbe..000000000
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package org.briarproject.briar.mailbox;
-
-class MailboxProtocolEngine extends  AbstractProtocolEngine {
-}
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
new file mode 100644
index 000000000..edf1f0957
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxSession.java
@@ -0,0 +1,161 @@
+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.mailbox.IntroduceeState.AWAIT_REMOTE_RESPONSE;
+import static org.briarproject.briar.mailbox.IntroduceeState.START;
+
+@Immutable
+@NotNullByDefault
+class MailboxSession extends IntroduceeSession {
+
+
+	MailboxSession(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 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 addRemoteRequest(
+			MailboxSession s,
+			IntroduceeState state,
+			ContactRequestMessage m, byte[] ephemeralPublicKey,
+			byte[] ephemeralPrivateKey,
+			long acceptTimestamp, boolean alice) {
+		Local local = new Local(alice, m.getMessageId(),
+				m.getTimestamp(), ephemeralPublicKey, ephemeralPrivateKey,
+				acceptTimestamp, null);
+		Remote remote =
+				new Remote(!alice, s.remote.author, m.getMessageId(),
+						m.getEphemeralPublicKey(), null, m.getAcceptTimestamp(),
+						null);
+		return new MailboxSession(s.getSessionId(), state, m.getTimestamp(),
+				s.contactGroupId, s.introducer, local, remote, s.masterKey,
+				s.transportKeys, s.getSessionCounter());
+	}
+
+	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.getSessionCounter());
+	}
+
+	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.getSessionCounter());
+	}
+
+	static MailboxSession clear(
+			MailboxSession 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 MailboxSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, null, null, s.getSessionCounter());
+	}
+
+	@Override
+	Role getRole() {
+		return Role.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/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java
index 28ac43793..cc44d68c2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java
@@ -16,36 +16,29 @@ import javax.annotation.Nullable;
 @NotNullByDefault
 interface MessageEncoder {
 
-    BdfDictionary encodeRequestMetadata(long timestamp);
+	BdfDictionary encodeRequestMetadata(long timestamp);
 
-    BdfDictionary encodeMetadata(MessageType type, @Nullable SessionId sessionId, long timestamp, boolean local);
+	BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local);
 
-    void addSessionId(BdfDictionary meta, SessionId sessionId);
+	void addSessionId(BdfDictionary meta, SessionId sessionId);
 
-    void setVisibleInUi(BdfDictionary meta, boolean visible);
+	void setVisibleInUi(BdfDictionary meta, boolean visible);
 
-    void setAvailableToAnswer(BdfDictionary meta, boolean available);
+	void setAvailableToAnswer(BdfDictionary meta, boolean available);
 
-    Message encodeRequestMessage(GroupId contactGroupId, long timestamp,
-                                 @Nullable MessageId previousMessageId);
+	Message encodeRequestMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId);
 
-    Message encodeAcceptMessage(GroupId contactGroupId, long timestamp,
-                                @Nullable MessageId previousMessageId, SessionId sessionId,
-                                byte[] ephemeralPublicKey, long acceptTimestamp,
-                                Map<TransportId, TransportProperties> transportProperties);
+	Message encodeMailboxAcceptMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties);
 
-    Message encodeDeclineMessage(GroupId contactGroupId, long timestamp,
-                                 @Nullable MessageId previousMessageId, SessionId sessionId);
+	Message encodeDeclineMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId);
 
-    Message encodeAuthMessage(GroupId contactGroupId, long timestamp,
-                              @Nullable MessageId previousMessageId, SessionId sessionId,
-                              byte[] mac, byte[] signature);
-
-    Message encodeActivateMessage(GroupId contactGroupId, long timestamp,
-                                  @Nullable MessageId previousMessageId, SessionId sessionId,
-                                  byte[] mac);
-
-    Message encodeAbortMessage(GroupId contactGroupId, long timestamp,
-                               @Nullable MessageId previousMessageId, SessionId sessionId);
+	Message encodeAbortMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId);
 
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoderImpl.java
index e71a481c8..dfb5dfd3c 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoderImpl.java
@@ -24,146 +24,122 @@ import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_MESSA
 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.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI;
+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.MAILBOX_REQUEST;
 
 @NotNullByDefault
 class MessageEncoderImpl implements
-        MessageEncoder {
-
-    private final ClientHelper clientHelper;
-    private final MessageFactory messageFactory;
-
-    @Inject
-    MessageEncoderImpl(ClientHelper clientHelper,
-                       MessageFactory messageFactory) {
-        this.clientHelper = clientHelper;
-        this.messageFactory = messageFactory;
-    }
-
-    @Override
-    public BdfDictionary encodeRequestMetadata(long timestamp) {
-        BdfDictionary meta =
-                encodeMetadata(MAILBOX_REQUEST, null, timestamp, false);
-        meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, false);
-        return meta;
-    }
-
-    @Override
-    public BdfDictionary encodeMetadata(MessageType type,
-                                        @Nullable SessionId sessionId, long timestamp, boolean local) {
-        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);
-        return meta;
-    }
-
-    @Override
-    public void addSessionId(BdfDictionary meta, SessionId sessionId) {
-        meta.put(MSG_KEY_SESSION_ID, sessionId);
-    }
-
-    @Override
-    public void setVisibleInUi(BdfDictionary meta, boolean visible) {
-        meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
-    }
-
-    @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) {
-        BdfList body = BdfList.of(
-                MAILBOX_REQUEST.getValue(),
-                previousMessageId
-        );
-        return createMessage(contactGroupId, timestamp, body);
-    }
-
-    @Override
-    public Message encodeAcceptMessage(GroupId contactGroupId, long timestamp,
-                                       @Nullable MessageId previousMessageId, SessionId sessionId,
-                                       byte[] ephemeralPublicKey, long acceptTimestamp,
-                                       Map<TransportId, TransportProperties> transportProperties) {
-        BdfList body = BdfList.of(
-                ACCEPT.getValue(),
-                sessionId,
-                previousMessageId,
-                ephemeralPublicKey,
-                acceptTimestamp,
-                clientHelper.toDictionary(transportProperties)
-        );
-        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 encodeAuthMessage(GroupId contactGroupId, long timestamp,
-                                     @Nullable MessageId previousMessageId, SessionId sessionId,
-                                     byte[] mac, byte[] signature) {
-        BdfList body = BdfList.of(
-                AUTH.getValue(),
-                sessionId,
-                previousMessageId,
-                mac,
-                signature
-        );
-        return createMessage(contactGroupId, timestamp, body);
-    }
-
-    @Override
-    public Message encodeActivateMessage(GroupId contactGroupId, long timestamp,
-                                         @Nullable MessageId previousMessageId, SessionId sessionId,
-                                         byte[] mac) {
-        BdfList body = BdfList.of(
-                ACTIVATE.getValue(),
-                sessionId,
-                previousMessageId,
-                mac
-        );
-        return createMessage(contactGroupId, timestamp, body);
-    }
-
-    @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);
-        }
-    }
+		MessageEncoder {
+
+	private final ClientHelper clientHelper;
+	private final MessageFactory messageFactory;
+
+	@Inject
+	MessageEncoderImpl(ClientHelper clientHelper,
+			MessageFactory messageFactory) {
+		this.clientHelper = clientHelper;
+		this.messageFactory = messageFactory;
+	}
+
+	@Override
+	public BdfDictionary encodeRequestMetadata(long timestamp) {
+		BdfDictionary meta =
+				encodeMetadata(MAILBOX_REQUEST, null, timestamp, false);
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return meta;
+	}
+
+	@Override
+	public BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local) {
+		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);
+		return meta;
+	}
+
+	@Override
+	public void addSessionId(BdfDictionary meta, SessionId sessionId) {
+		meta.put(MSG_KEY_SESSION_ID, sessionId);
+	}
+
+	@Override
+	public void setVisibleInUi(BdfDictionary meta, boolean visible) {
+		meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
+	}
+
+	@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) {
+		BdfList body = BdfList.of(
+				MAILBOX_REQUEST.getValue(),
+				previousMessageId
+		);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeMailboxAcceptMessage(GroupId contactGroupId,
+			long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties) {
+		BdfList body = BdfList.of(
+				MAILBOX_ACCEPT.getValue(),
+				sessionId,
+				previousMessageId,
+				ephemeralPublicKey,
+				acceptTimestamp,
+				clientHelper.toDictionary(transportProperties)
+		);
+		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/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParser.java
index 5b6f729e8..640178c99 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParser.java
@@ -6,12 +6,6 @@ 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;
-import org.briarproject.briar.introduction.AcceptMessage;
-import org.briarproject.briar.introduction.ActivateMessage;
-import org.briarproject.briar.introduction.AuthMessage;
-import org.briarproject.briar.introduction.DeclineMessage;
-import org.briarproject.briar.introduction.MessageMetadata;
-import org.briarproject.briar.introduction.RequestMessage;
 
 @NotNullByDefault
 interface MessageParser {
@@ -20,24 +14,21 @@ interface MessageParser {
 
 	BdfDictionary getRequestsAvailableToAnswerQuery(SessionId sessionId);
 
-	MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
+	//MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
 
 	RequestMessage parseRequestMessage(Message m, BdfList body)
 			throws FormatException;
 
-	AcceptMessage parseAcceptMessage(Message m, BdfList body)
+	MailboxAcceptMessage parseMailboxAcceptMessage(Message m, BdfList body)
 			throws FormatException;
 
 	DeclineMessage parseDeclineMessage(Message m, BdfList body)
 			throws FormatException;
 
-	AuthMessage parseAuthMessage(Message m, BdfList body)
+	MailboxAuthMessage parseMailboxAuthMessage(Message m, BdfList body)
 			throws FormatException;
 
-	ActivateMessage parseActivateMessage(Message m, BdfList body)
-			throws FormatException;
-
-	org.briarproject.briar.introduction.AbortMessage parseAbortMessage(
+	AbortMessage parseAbortMessage(
 			Message m, BdfList body)
 			throws FormatException;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParserImpl.java
index 89284e26a..ec0e13baa 100644
--- a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParserImpl.java
@@ -5,33 +5,22 @@ 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 org.briarproject.briar.introduction.AcceptMessage;
-import org.briarproject.briar.introduction.ActivateMessage;
-import org.briarproject.briar.introduction.AuthMessage;
-import org.briarproject.briar.introduction.DeclineMessage;
-import org.briarproject.briar.introduction.MessageMetadata;
-import org.briarproject.briar.introduction.MessageType;
-import org.briarproject.briar.introduction.RequestMessage;
 
 import java.util.Map;
 
 import javax.inject.Inject;
 
-import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
-import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
-import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_LOCAL;
-import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
-import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_SESSION_ID;
-import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_TIMESTAMP;
-import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI;
-import static org.briarproject.briar.introduction.MessageType.REQUEST;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+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_VISIBLE_IN_UI;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_REQUEST;
 
 @NotNullByDefault
 class MessageParserImpl implements
@@ -50,14 +39,16 @@ class MessageParserImpl implements
 	}
 
 	@Override
-	public BdfDictionary getRequestsAvailableToAnswerQuery(SessionId sessionId) {
+	public BdfDictionary getRequestsAvailableToAnswerQuery(
+			SessionId sessionId) {
 		return BdfDictionary.of(
 				new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
-				new BdfEntry(MSG_KEY_MESSAGE_TYPE, REQUEST.getValue()),
+				new BdfEntry(MSG_KEY_MESSAGE_TYPE, MAILBOX_REQUEST.getValue()),
 				new BdfEntry(MSG_KEY_SESSION_ID, sessionId)
 		);
 	}
 
+	/*
 	@Override
 	public MessageMetadata parseMetadata(BdfDictionary d)
 			throws FormatException {
@@ -74,6 +65,7 @@ class MessageParserImpl implements
 		return new MessageMetadata(type, sessionId, timestamp, local, read,
 				visible, available);
 	}
+	*/
 
 	@Override
 	public RequestMessage parseRequestMessage(Message m, BdfList body)
@@ -81,14 +73,13 @@ class MessageParserImpl implements
 		byte[] previousMsgBytes = body.getOptionalRaw(1);
 		MessageId previousMessageId = (previousMsgBytes == null ? null :
 				new MessageId(previousMsgBytes));
-		Author author = clientHelper.parseAndValidateAuthor(body.getList(2));
-		String message = body.getOptionalString(3);
 		return new RequestMessage(m.getId(), m.getGroupId(),
-				m.getTimestamp(), previousMessageId, author, message);
+				m.getTimestamp(), previousMessageId, body.getLong(2));
 	}
 
 	@Override
-	public AcceptMessage parseAcceptMessage(Message m, BdfList body)
+	public MailboxAcceptMessage parseMailboxAcceptMessage(Message m,
+			BdfList body)
 			throws FormatException {
 		SessionId sessionId = new SessionId(body.getRaw(1));
 		byte[] previousMsgBytes = body.getOptionalRaw(2);
@@ -98,9 +89,10 @@ class MessageParserImpl implements
 		long acceptTimestamp = body.getLong(4);
 		Map<TransportId, TransportProperties> transportProperties = clientHelper
 				.parseAndValidateTransportPropertiesMap(body.getDictionary(5));
-		return new AcceptMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+		return new MailboxAcceptMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(),
 				previousMessageId, sessionId, ephemeralPublicKey,
-				acceptTimestamp, transportProperties);
+				acceptTimestamp, transportProperties, body.getLong(5));
 	}
 
 	@Override
@@ -111,41 +103,34 @@ class MessageParserImpl implements
 		MessageId previousMessageId = (previousMsgBytes == null ? null :
 				new MessageId(previousMsgBytes));
 		return new DeclineMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
-				previousMessageId, sessionId);
+				previousMessageId, sessionId, body.getLong(3));
 	}
 
 	@Override
-	public AuthMessage parseAuthMessage(Message m, BdfList body)
+	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 AuthMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
-				previousMessageId, sessionId, mac, signature);
-	}
-
-	@Override
-	public ActivateMessage parseActivateMessage(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);
-		return new ActivateMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+		return new MailboxAuthMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
 				previousMessageId, sessionId, mac);
+				*/
+		return null;
 	}
 
 	@Override
-	public org.briarproject.briar.introduction.AbortMessage parseAbortMessage(Message m, BdfList body)
+	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 org.briarproject.briar.introduction.AbortMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
-				previousMessageId, sessionId);
+		return new AbortMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, sessionId, body.getLong(3));
 	}
 
 }
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 a330103c7..3aa608b08 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
@@ -69,7 +69,8 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 	@Override
 	public OwnerSession onRequestMessage(Transaction txn, OwnerSession session,
 			RequestMessage m) throws DbException, FormatException {
-		return abort(txn, session);
+//		return abort(txn, session);
+		throw new UnsupportedOperationException();
 	}
 
 	@Override
@@ -90,6 +91,13 @@ class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
 		}
 	}
 
+	@Override
+	public OwnerSession onContactAcceptMessage(Transaction txn,
+			OwnerSession session, MailboxAcceptMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
 	private void onMailboxAccept(Transaction txn, OwnerSession s,
 			MailboxAcceptMessage m) {
 
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 c68382ac7..d63caea3d 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
@@ -4,45 +4,45 @@ 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.introduction.Role;
+import org.briarproject.briar.api.mailbox.Role;
 
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
+import static org.briarproject.briar.api.mailbox.Role.MAILBOX;
 
 @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);
-    }
-
-    @Override
-    Role getRole() {
-        return INTRODUCER;
-    }
-
-    Introducee getMailbox() {
-        return mailbox;
-    }
-
-    Introducee getIntroducee() {
-        return introducee;
-    }
+	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);
+	}
+
+	@Override
+	Role getRole() {
+		return MAILBOX;
+	}
+
+	Introducee getMailbox() {
+		return mailbox;
+	}
+
+	Introducee getIntroducee() {
+		return introducee;
+	}
 
 
 }
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 83da3b464..5f1305600 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
@@ -2,7 +2,7 @@ package org.briarproject.briar.mailbox;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction.Role;
+import org.briarproject.briar.api.mailbox.Role;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoder.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoder.java
new file mode 100644
index 000000000..c781f014a
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoder.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 SessionEncoder {
+
+	BdfDictionary getIntroduceeSessionsByIntroducerQuery(Author introducer);
+
+	BdfDictionary getIntroducerSessionsQuery();
+
+	BdfDictionary encodeIntroducerSession(OwnerSession s);
+
+	BdfDictionary encodeIntroduceeSession(IntroduceeSession s);
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoderImpl.java
new file mode 100644
index 000000000..413048223
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionEncoderImpl.java
@@ -0,0 +1,155 @@
+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.IntroduceeSession.Common;
+import static org.briarproject.briar.mailbox.IntroduceeSession.Local;
+import static org.briarproject.briar.mailbox.IntroduceeSession.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_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 SessionEncoderImpl implements
+		SessionEncoder {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	SessionEncoderImpl(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(IntroduceeSession 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());
+		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/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParser.java
new file mode 100644
index 000000000..a66a24f70
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParser.java
@@ -0,0 +1,23 @@
+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 SessionParser {
+
+	BdfDictionary getSessionQuery(SessionId s);
+
+	Role getRole(BdfDictionary d) throws FormatException;
+
+	OwnerSession parseOwnerSession(BdfDictionary d)
+			throws FormatException;
+
+	MailboxSession parseMailboxSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException;
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParserImpl.java
new file mode 100644
index 000000000..ebc687e95
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/SessionParserImpl.java
@@ -0,0 +1,210 @@
+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.OWNER;
+import static org.briarproject.briar.api.mailbox.Role.fromValue;
+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;
+import static org.briarproject.briar.mailbox.MailboxSession.Local;
+import static org.briarproject.briar.mailbox.MailboxSession.Remote;
+
+@Immutable
+@NotNullByDefault
+class SessionParserImpl implements SessionParser {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	SessionParserImpl(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 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) != 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 MailboxSession(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/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
index 108f48849..4c520efe3 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
@@ -21,6 +21,7 @@ import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.blog.BlogModule;
 import org.briarproject.briar.client.BriarClientModule;
 import org.briarproject.briar.forum.ForumModule;
+import org.briarproject.briar.mailbox.MailboxIntroductionModule;
 import org.briarproject.briar.messaging.MessagingModule;
 import org.briarproject.briar.privategroup.PrivateGroupModule;
 import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;
@@ -49,6 +50,7 @@ import dagger.Component;
 		GroupInvitationModule.class,
 		IdentityModule.class,
 		IntroductionModule.class,
+		MailboxIntroductionModule.class,
 		LifecycleModule.class,
 		MessagingModule.class,
 		PrivateGroupModule.class,
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
new file mode 100644
index 000000000..c8e83d3cd
--- /dev/null
+++ b/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTest.java
@@ -0,0 +1,352 @@
+package org.briarproject.briar.mailbox;
+
+import net.jodah.concurrentunit.Waiter;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.PrivateMailbox;
+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.db.DbException;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+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.test.TestDatabaseModule;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction.IntroductionMessage;
+import org.briarproject.briar.api.introduction.IntroductionRequest;
+import org.briarproject.briar.api.introduction.IntroductionResponse;
+import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
+import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent;
+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.test.BriarIntegrationTest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID;
+import static org.briarproject.bramble.test.TestUtils.getTransportProperties;
+import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
+import static org.briarproject.briar.mailbox.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.mailbox.IntroductionConstants.SESSION_KEY_AUTHOR;
+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_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.mailbox.MessageType.DECLINE;
+import static org.briarproject.briar.mailbox.MessageType.MAILBOX_ACCEPT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class MailboxIntroductionIntegrationTest
+		extends
+		BriarIntegrationTest<MailboxIntroductionIntegrationTestComponent> {
+
+	// objects accessed from background threads need to be volatile
+	private volatile MailboxIntroductionManager introductionManager0;
+	private volatile MailboxIntroductionManager introductionManager1;
+	private volatile MailboxIntroductionManager introductionManager2;
+	private volatile MailboxIntroductionManager introductionManagerMailbox;
+	private volatile Waiter eventWaiter;
+
+	private OwnerListener listener0;
+	private IntroduceeListener listener1;
+	private IntroduceeListener listener2;
+
+	/*
+	interface StateVisitor {
+		AcceptMessage visit(AcceptMessage response);
+	}*/
+
+	@Before
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+
+		introductionManager0 = c0.getMailboxIntroductionManager();
+		introductionManager1 = c1.getMailboxIntroductionManager();
+		introductionManager2 = c2.getMailboxIntroductionManager();
+		introductionManagerMailbox = cMailbox.getMailboxIntroductionManager();
+
+		// initialize waiter fresh for each test
+		eventWaiter = new Waiter();
+
+		addTransportProperties();
+	}
+
+	@Override
+	protected void createComponents() {
+		MailboxIntroductionIntegrationTestComponent component =
+				DaggerMailboxIntroductionIntegrationTestComponent.builder()
+						.build();
+		component.inject(this);
+
+		c0 = DaggerMailboxIntroductionIntegrationTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
+		injectEagerSingletons(c0);
+
+		c1 = DaggerMailboxIntroductionIntegrationTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
+		injectEagerSingletons(c1);
+
+		c2 = DaggerMailboxIntroductionIntegrationTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t2Dir)).build();
+		injectEagerSingletons(c2);
+
+		cMailbox = DaggerMailboxIntroductionIntegrationTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t3Dir)).build();
+		injectEagerSingletons(cMailbox);
+	}
+
+	@Test
+	public void testIntroductionSession() throws Exception {
+		addListeners(true, true);
+
+		// make introduction
+		long time = clock.currentTimeMillis();
+		Contact introducee = contact1From0;
+		PrivateMailbox mailbox = privateMailboxFrom0;
+		introductionManager0
+				.makeIntroduction(mailbox, introducee, time);
+
+		// sync first REQUEST message
+		sync0ToMailbox(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener1.requestReceived);
+		assertEquals(mailbox.getAuthor().getName(),
+				listener1.getRequest().getName());
+	}
+
+
+	private void addTransportProperties()
+			throws DbException, IOException, TimeoutException {
+		TransportPropertyManager tpm0 = c0.getTransportPropertyManager();
+		TransportPropertyManager tpm1 = c1.getTransportPropertyManager();
+		TransportPropertyManager tpm2 = c2.getTransportPropertyManager();
+
+		tpm0.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
+		sync0To1(1, true);
+		sync0To2(1, true);
+
+		tpm1.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
+		sync1To0(1, true);
+
+		tpm2.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
+		sync2To0(1, true);
+	}
+
+	private void assertMessagesAreAcked(
+			Collection<IntroductionMessage> messages) {
+		for (IntroductionMessage msg : messages) {
+			if (msg.isLocal()) assertTrue(msg.isSeen());
+		}
+	}
+
+	private void addListeners(boolean accept1, boolean accept2) {
+		// listen to events
+		listener0 = new OwnerListener();
+		c0.getEventBus().addListener(listener0);
+		listener1 = new IntroduceeListener(1, accept1);
+		c1.getEventBus().addListener(listener1);
+		listener2 = new IntroduceeListener(2, accept2);
+		c2.getEventBus().addListener(listener2);
+	}
+
+	@MethodsNotNullByDefault
+	@ParametersNotNullByDefault
+	private abstract class IntroductionListener implements EventListener {
+
+		protected volatile boolean aborted = false;
+		protected volatile Event latestEvent;
+
+		@SuppressWarnings("WeakerAccess")
+		IntroductionResponse getResponse() {
+			assertTrue(
+					latestEvent instanceof IntroductionResponseReceivedEvent);
+			return ((IntroductionResponseReceivedEvent) latestEvent)
+					.getIntroductionResponse();
+		}
+	}
+
+	@MethodsNotNullByDefault
+	@ParametersNotNullByDefault
+	private class IntroduceeListener extends IntroductionListener {
+
+		private volatile boolean requestReceived = false;
+		private volatile boolean succeeded = false;
+		private volatile boolean answerRequests = true;
+		private volatile SessionId sessionId;
+
+		private final int introducee;
+		private final boolean accept;
+
+		private IntroduceeListener(int introducee, boolean accept) {
+			this.introducee = introducee;
+			this.accept = accept;
+		}
+
+		@Override
+		public void eventOccurred(Event e) {
+			if (e instanceof IntroductionRequestReceivedEvent) {
+				latestEvent = e;
+				IntroductionRequestReceivedEvent introEvent =
+						((IntroductionRequestReceivedEvent) e);
+				requestReceived = true;
+				IntroductionRequest ir = introEvent.getIntroductionRequest();
+				ContactId contactId = introEvent.getContactId();
+				sessionId = ir.getSessionId();
+				long time = clock.currentTimeMillis();
+				try {
+					if (introducee == 1 && answerRequests) {
+					/*	introductionManager1
+								.respondToIntroduction(contactId, sessionId,
+										time, accept);*/
+					} else if (introducee == 2 && answerRequests) {
+/*						introductionManager2
+								.respondToIntroduction(contactId, sessionId,
+										time, accept);*/
+					}
+					//	} catch (DbException exception) {
+					//eventWaiter.rethrow(exception);
+				} finally {
+					eventWaiter.resume();
+				}
+			} else if (e instanceof IntroductionResponseReceivedEvent) {
+				// only broadcast for DECLINE messages in introducee role
+				latestEvent = e;
+				eventWaiter.resume();
+			} else if (e instanceof IntroductionSucceededEvent) {
+				latestEvent = e;
+				succeeded = true;
+				Contact contact = ((IntroductionSucceededEvent) e).getContact();
+				eventWaiter
+						.assertFalse(contact.getId().equals(contactId0From1));
+				eventWaiter.resume();
+			} else if (e instanceof IntroductionAbortedEvent) {
+				latestEvent = e;
+				aborted = true;
+				eventWaiter.resume();
+			}
+		}
+
+		private IntroductionRequest getRequest() {
+			assertTrue(
+					latestEvent instanceof IntroductionRequestReceivedEvent);
+			return ((IntroductionRequestReceivedEvent) latestEvent)
+					.getIntroductionRequest();
+		}
+	}
+
+	@NotNullByDefault
+	private class OwnerListener extends IntroductionListener {
+
+		private volatile boolean response1Received = false;
+		private volatile boolean response2Received = false;
+
+		@Override
+		public void eventOccurred(Event e) {
+			if (e instanceof IntroductionResponseReceivedEvent) {
+				latestEvent = e;
+				ContactId c =
+						((IntroductionResponseReceivedEvent) e)
+								.getContactId();
+				if (c.equals(contactId1From0)) {
+					response1Received = true;
+				} else if (c.equals(contactId2From0)) {
+					response2Received = true;
+				}
+				eventWaiter.resume();
+			} else if (e instanceof IntroductionAbortedEvent) {
+				latestEvent = e;
+				aborted = true;
+				eventWaiter.resume();
+			}
+		}
+
+	}
+
+	private void replacePreviousLocalMessageId(Author author,
+			BdfDictionary d, MessageId id) throws FormatException {
+		BdfDictionary i1 = d.getDictionary(SESSION_KEY_INTRODUCEE_A);
+		BdfDictionary i2 = d.getDictionary(SESSION_KEY_INTRODUCEE_B);
+		Author a1 = clientHelper
+				.parseAndValidateAuthor(i1.getList(SESSION_KEY_AUTHOR));
+		Author a2 = clientHelper
+				.parseAndValidateAuthor(i2.getList(SESSION_KEY_AUTHOR));
+
+		if (a1.equals(author)) {
+			i1.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id);
+			d.put(SESSION_KEY_INTRODUCEE_A, i1);
+		} else if (a2.equals(author)) {
+			i2.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id);
+			d.put(SESSION_KEY_INTRODUCEE_B, i2);
+		} else {
+			throw new AssertionError();
+		}
+	}
+
+	private AbstractMailboxIntroductionMessage getMessageFor(ClientHelper ch,
+			Contact contact, MessageType type)
+			throws FormatException, DbException {
+		Group g = introductionManager0.getContactGroup(contact);
+		BdfDictionary query = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_MESSAGE_TYPE, type.getValue())
+		);
+		Map<MessageId, BdfDictionary> map =
+				ch.getMessageMetadataAsDictionary(g.getId(), query);
+		assertEquals(1, map.size());
+		MessageId id = map.entrySet().iterator().next().getKey();
+		Message m = ch.getMessage(id);
+		BdfList body = ch.getMessageAsList(id);
+		if (type == MAILBOX_ACCEPT) {
+			//noinspection ConstantConditions
+			return c0.getMessageParser().parseMailboxAcceptMessage(m, body);
+		} else if (type == DECLINE) {
+			//noinspection ConstantConditions
+			return c0.getMessageParser().parseDeclineMessage(m, body);
+		} else throw new AssertionError("Not implemented");
+	}
+
+	private OwnerSession getOwnerSession()
+			throws DbException, FormatException {
+		Map<MessageId, BdfDictionary> dicts = c0.getClientHelper()
+				.getMessageMetadataAsDictionary(getLocalGroup().getId());
+		assertEquals(1, dicts.size());
+		BdfDictionary d = dicts.values().iterator().next();
+		return c0.getSessionParser().parseOwnerSession(d);
+	}
+
+	private MailboxSession getMailboxSession(
+			MailboxIntroductionIntegrationTestComponent c)
+			throws DbException, FormatException {
+		Map<MessageId, BdfDictionary> dicts = c.getClientHelper()
+				.getMessageMetadataAsDictionary(getLocalGroup().getId());
+		assertEquals(1, dicts.size());
+		BdfDictionary d = dicts.values().iterator().next();
+		Group introducerGroup =
+				introductionManager2.getContactGroup(contact0From2);
+		return c.getSessionParser()
+				.parseMailboxSession(introducerGroup.getId(), d);
+	}
+
+	private Group getLocalGroup() {
+		return contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
+	}
+
+}
diff --git a/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTestComponent.java
new file mode 100644
index 000000000..80abea604
--- /dev/null
+++ b/briar-core/src/test/java/org/briarproject/briar/mailbox/MailboxIntroductionIntegrationTestComponent.java
@@ -0,0 +1,78 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.client.ClientModule;
+import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
+import org.briarproject.bramble.crypto.CryptoModule;
+import org.briarproject.bramble.data.DataModule;
+import org.briarproject.bramble.db.DatabaseModule;
+import org.briarproject.bramble.event.EventModule;
+import org.briarproject.bramble.identity.IdentityModule;
+import org.briarproject.bramble.lifecycle.LifecycleModule;
+import org.briarproject.bramble.properties.PropertiesModule;
+import org.briarproject.bramble.record.RecordModule;
+import org.briarproject.bramble.sync.SyncModule;
+import org.briarproject.bramble.system.SystemModule;
+import org.briarproject.bramble.test.TestDatabaseModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
+import org.briarproject.bramble.test.TestSecureRandomModule;
+import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
+import org.briarproject.briar.blog.BlogModule;
+import org.briarproject.briar.client.BriarClientModule;
+import org.briarproject.briar.forum.ForumModule;
+import org.briarproject.briar.introduction.IntroductionModule;
+import org.briarproject.briar.messaging.MessagingModule;
+import org.briarproject.briar.privategroup.PrivateGroupModule;
+import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;
+import org.briarproject.briar.sharing.SharingModule;
+import org.briarproject.briar.test.BriarIntegrationTestComponent;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+@Singleton
+@Component(modules = {
+		TestDatabaseModule.class,
+		TestPluginConfigModule.class,
+		TestSecureRandomModule.class,
+		BlogModule.class,
+		BriarClientModule.class,
+		ClientModule.class,
+		ContactModule.class,
+		CryptoModule.class,
+		CryptoExecutorModule.class,
+		DataModule.class,
+		DatabaseModule.class,
+		EventModule.class,
+		ForumModule.class,
+		GroupInvitationModule.class,
+		IdentityModule.class,
+		IntroductionModule.class,
+		MailboxIntroductionModule.class,
+		LifecycleModule.class,
+		MessagingModule.class,
+		PrivateGroupModule.class,
+		PropertiesModule.class,
+		RecordModule.class,
+		SharingModule.class,
+		SyncModule.class,
+		SystemModule.class,
+		TransportModule.class,
+		VersioningModule.class
+})
+interface MailboxIntroductionIntegrationTestComponent
+		extends BriarIntegrationTestComponent {
+
+	void inject(MailboxIntroductionIntegrationTest init);
+
+	MessageEncoder getMessageEncoder();
+
+	MessageParser getMessageParser();
+
+	SessionParser getSessionParser();
+
+	MailboxIntroductionCrypto getMailboxIntroductionCrypto();
+
+}
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
index f10d94183..7c43ea31a 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
@@ -7,6 +7,7 @@ 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.PrivateMailbox;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DbException;
@@ -44,6 +45,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationFactory
 import org.briarproject.briar.blog.BlogModule;
 import org.briarproject.briar.forum.ForumModule;
 import org.briarproject.briar.introduction.IntroductionModule;
+import org.briarproject.briar.mailbox.MailboxIntroductionModule;
 import org.briarproject.briar.messaging.MessagingModule;
 import org.briarproject.briar.privategroup.PrivateGroupModule;
 import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;
@@ -80,19 +82,21 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 	@Nullable
 	protected ContactId contactId1From2, contactId2From1;
 	protected ContactId contactId0From1, contactId0From2, contactId1From0,
-			contactId2From0;
+			contactId2From0, privateMailboxIdFrom0, contactId0FromMailbox;
 	protected Contact contact0From1, contact0From2, contact1From0,
-			contact2From0;
-	protected LocalAuthor author0, author1, author2;
-	protected ContactManager contactManager0, contactManager1, contactManager2;
+			contact2From0, contact0FromMailbox;
+	protected PrivateMailbox privateMailboxFrom0;
+	protected LocalAuthor author0, author1, author2, authorMailbox;
+	protected ContactManager contactManager0, contactManager1, contactManager2,
+			contactManagerMailbox;
 	protected IdentityManager identityManager0, identityManager1,
-			identityManager2;
-	protected DatabaseComponent db0, db1, db2;
+			identityManager2, identityManagerMailbox;
+	protected DatabaseComponent db0, db1, db2, dbMailbox;
 	protected MessageTracker messageTracker0, messageTracker1, messageTracker2;
 
 	private LifecycleManager lifecycleManager0, lifecycleManager1,
-			lifecycleManager2;
-	private SyncSessionFactory sync0, sync1, sync2;
+			lifecycleManager2, lifecycleManagerMailbox;
+	private SyncSessionFactory sync0, sync1, sync2, syncMailbox;
 
 	@Inject
 	protected Clock clock;
@@ -123,17 +127,19 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 	private volatile Waiter validationWaiter;
 	private volatile Waiter deliveryWaiter;
 
-	protected final static int TIMEOUT = 15000;
-	protected C c0, c1, c2;
+	protected final static int TIMEOUT = 600000;
+	protected C c0, c1, c2, cMailbox;
 
 	private final File testDir = TestUtils.getTestDirectory();
 	private final String AUTHOR0 = "Author 0";
 	private final String AUTHOR1 = "Author 1";
 	private final String AUTHOR2 = "Author 2";
+	private final String AUTHOR_MAILBOX = "Author MB";
 
 	protected File t0Dir = new File(testDir, AUTHOR0);
 	protected File t1Dir = new File(testDir, AUTHOR1);
 	protected File t2Dir = new File(testDir, AUTHOR2);
+	protected File t3Dir = new File(testDir, AUTHOR_MAILBOX);
 
 	@Before
 	public void setUp() throws Exception {
@@ -143,18 +149,22 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		identityManager0 = c0.getIdentityManager();
 		identityManager1 = c1.getIdentityManager();
 		identityManager2 = c2.getIdentityManager();
+		identityManagerMailbox = cMailbox.getIdentityManager();
 		contactManager0 = c0.getContactManager();
 		contactManager1 = c1.getContactManager();
 		contactManager2 = c2.getContactManager();
+		contactManagerMailbox = cMailbox.getContactManager();
 		messageTracker0 = c0.getMessageTracker();
 		messageTracker1 = c1.getMessageTracker();
 		messageTracker2 = c2.getMessageTracker();
 		db0 = c0.getDatabaseComponent();
 		db1 = c1.getDatabaseComponent();
 		db2 = c2.getDatabaseComponent();
+		dbMailbox = cMailbox.getDatabaseComponent();
 		sync0 = c0.getSyncSessionFactory();
 		sync1 = c1.getSyncSessionFactory();
 		sync2 = c2.getSyncSessionFactory();
+		syncMailbox = cMailbox.getSyncSessionFactory();
 
 		// initialize waiters fresh for each test
 		validationWaiter = new Waiter();
@@ -177,6 +187,7 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		component.inject(new GroupInvitationModule.EagerSingletons());
 		component.inject(new IdentityModule.EagerSingletons());
 		component.inject(new IntroductionModule.EagerSingletons());
+		component.inject(new MailboxIntroductionModule.EagerSingletons());
 		component.inject(new LifecycleModule.EagerSingletons());
 		component.inject(new MessagingModule.EagerSingletons());
 		component.inject(new PrivateGroupModule.EagerSingletons());
@@ -193,12 +204,15 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		lifecycleManager0 = c0.getLifecycleManager();
 		lifecycleManager1 = c1.getLifecycleManager();
 		lifecycleManager2 = c2.getLifecycleManager();
+		lifecycleManagerMailbox = cMailbox.getLifecycleManager();
 		lifecycleManager0.startServices(getSecretKey());
 		lifecycleManager1.startServices(getSecretKey());
 		lifecycleManager2.startServices(getSecretKey());
+		lifecycleManagerMailbox.startServices(getSecretKey());
 		lifecycleManager0.waitForStartup();
 		lifecycleManager1.waitForStartup();
 		lifecycleManager2.waitForStartup();
+		lifecycleManagerMailbox.waitForStartup();
 	}
 
 	private void listenToEvents() {
@@ -208,6 +222,8 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		c1.getEventBus().addListener(listener1);
 		Listener listener2 = new Listener();
 		c2.getEventBus().addListener(listener2);
+		Listener listenerMailbox = new Listener();
+		cMailbox.getEventBus().addListener(listenerMailbox);
 	}
 
 	private class Listener implements EventListener {
@@ -237,6 +253,9 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		identityManager1.registerLocalAuthor(author1);
 		author2 = identityManager2.createLocalAuthor(AUTHOR2);
 		identityManager2.registerLocalAuthor(author2);
+		authorMailbox =
+				identityManagerMailbox.createLocalAuthor(AUTHOR_MAILBOX);
+		identityManagerMailbox.registerLocalAuthor(authorMailbox);
 	}
 
 	protected void addDefaultContacts() throws Exception {
@@ -256,14 +275,29 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 				.addContact(author0, author2.getId(), getSecretKey(),
 						clock.currentTimeMillis(), true, true, true);
 		contact0From2 = contactManager2.getContact(contactId0From2);
+		privateMailboxIdFrom0 = contactManager0
+				.addPrivateMailbox(authorMailbox, author0.getId(),
+						getSecretKey(), clock.currentTimeMillis(), true);
+		privateMailboxFrom0 =
+				(PrivateMailbox) contactManager0
+						.getContact(privateMailboxIdFrom0);
+		contactId0FromMailbox = contactManagerMailbox
+				.addMailboxOwner(author0, authorMailbox.getId(),
+						getSecretKey(),
+						clock.currentTimeMillis(), false);
+		contact0FromMailbox =
+				contactManagerMailbox.getContact(contactId0FromMailbox);
 
 		// Sync initial client versioning updates
 		sync0To1(1, true);
 		sync0To2(1, true);
+		sync0ToMailbox(1, true);
 		sync1To0(1, true);
 		sync2To0(1, true);
+		syncMailboxTo0(1, true);
 		sync0To1(1, true);
 		sync0To2(1, true);
+		sync0ToMailbox(1, true);
 	}
 
 	protected void addContacts1And2() throws Exception {
@@ -291,43 +325,65 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		lifecycleManager0.stopServices();
 		lifecycleManager1.stopServices();
 		lifecycleManager2.stopServices();
+		lifecycleManagerMailbox.stopServices();
 		lifecycleManager0.waitForShutdown();
 		lifecycleManager1.waitForShutdown();
 		lifecycleManager2.waitForShutdown();
+		lifecycleManagerMailbox.waitForShutdown();
 	}
 
 	protected void sync0To1(int num, boolean valid)
 			throws IOException, TimeoutException {
-		syncMessage(sync0, contactId0From1, sync1, contactId1From0, num, valid);
+		syncMessage(sync0, contactId0From1, sync1, contactId1From0, num,
+				valid);
 	}
 
 	protected void sync0To2(int num, boolean valid)
 			throws IOException, TimeoutException {
-		syncMessage(sync0, contactId0From2, sync2, contactId2From0, num, valid);
+		syncMessage(sync0, contactId0From2, sync2, contactId2From0, num,
+				valid);
+	}
+
+	protected void sync0ToMailbox(int num, boolean valid)
+			throws IOException, TimeoutException {
+		syncMessage(sync0, contactId0FromMailbox, syncMailbox,
+				privateMailboxIdFrom0, num,
+				valid);
 	}
 
 	protected void sync1To0(int num, boolean valid)
 			throws IOException, TimeoutException {
-		syncMessage(sync1, contactId1From0, sync0, contactId0From1, num, valid);
+		syncMessage(sync1, contactId1From0, sync0, contactId0From1, num,
+				valid);
 	}
 
 	protected void sync2To0(int num, boolean valid)
 			throws IOException, TimeoutException {
-		syncMessage(sync2, contactId2From0, sync0, contactId0From2, num, valid);
+		syncMessage(sync2, contactId2From0, sync0, contactId0From2, num,
+				valid);
+	}
+
+	protected void syncMailboxTo0(int num, boolean valid)
+			throws IOException, TimeoutException {
+		syncMessage(syncMailbox, privateMailboxIdFrom0, sync0,
+				contactId0FromMailbox,
+				num, valid);
 	}
 
 	protected void sync2To1(int num, boolean valid)
 			throws IOException, TimeoutException {
 		assertNotNull(contactId2From1);
 		assertNotNull(contactId1From2);
-		syncMessage(sync2, contactId2From1, sync1, contactId1From2, num, valid);
+		syncMessage(sync2, contactId2From1, sync1, contactId1From2, num,
+				valid);
 	}
 
 	protected void sync1To2(int num, boolean valid)
 			throws IOException, TimeoutException {
 		assertNotNull(contactId2From1);
 		assertNotNull(contactId1From2);
-		syncMessage(sync1, contactId1From2, sync2, contactId2From1, num, valid);
+		syncMessage(sync1, contactId1From2, sync2, contactId2From1, num,
+				valid);
 	}
 
 	private void syncMessage(SyncSessionFactory fromSync, ContactId fromId,
@@ -338,21 +394,25 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		String from = "0";
 		if (fromSync == sync1) from = "1";
 		else if (fromSync == sync2) from = "2";
+		else if (fromSync == syncMailbox) from = "Mailbox";
 		String to = "0";
 		if (toSync == sync1) to = "1";
 		else if (toSync == sync2) to = "2";
+		else if (toSync == syncMailbox) to = "Mailbox";
 		LOG.info("TEST: Sending message from " + from + " to " + to);
 
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		StreamWriter streamWriter = new TestStreamWriter(out);
 		// Create an outgoing sync session
-		SyncSession sessionFrom = fromSync.createSimplexOutgoingSession(toId,
-				MAX_LATENCY, streamWriter);
+		SyncSession sessionFrom =
+				fromSync.createSimplexOutgoingSession(toId,
+						MAX_LATENCY, streamWriter);
 		// Write whatever needs to be written
 		sessionFrom.run();
 		out.close();
 
-		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		ByteArrayInputStream in =
+				new ByteArrayInputStream(out.toByteArray());
 		// Create an incoming sync session
 		SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
 		// Read whatever needs to be read
@@ -371,6 +431,7 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		contactManager0.removeContact(contactId2From0);
 		contactManager1.removeContact(contactId0From1);
 		contactManager2.removeContact(contactId0From2);
+		contactManagerMailbox.removeContact(contactId0FromMailbox);
 		assertNotNull(contactId2From1);
 		contactManager1.removeContact(contactId2From1);
 		assertNotNull(contactId1From2);
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
index a3b74d4c2..493a4e76c 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
@@ -34,12 +34,14 @@ import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.forum.ForumManager;
 import org.briarproject.briar.api.forum.ForumSharingManager;
 import org.briarproject.briar.api.introduction.IntroductionManager;
+import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
 import org.briarproject.briar.api.privategroup.PrivateGroupManager;
 import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
 import org.briarproject.briar.blog.BlogModule;
 import org.briarproject.briar.client.BriarClientModule;
 import org.briarproject.briar.forum.ForumModule;
 import org.briarproject.briar.introduction.IntroductionModule;
+import org.briarproject.briar.mailbox.MailboxIntroductionModule;
 import org.briarproject.briar.messaging.MessagingModule;
 import org.briarproject.briar.privategroup.PrivateGroupModule;
 import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;
@@ -67,6 +69,7 @@ import dagger.Component;
 		GroupInvitationModule.class,
 		IdentityModule.class,
 		IntroductionModule.class,
+		MailboxIntroductionModule.class,
 		LifecycleModule.class,
 		MessagingModule.class,
 		PrivateGroupModule.class,
@@ -96,6 +99,8 @@ public interface BriarIntegrationTestComponent {
 
 	void inject(IntroductionModule.EagerSingletons init);
 
+	void inject(MailboxIntroductionModule.EagerSingletons init);
+
 	void inject(LifecycleModule.EagerSingletons init);
 
 	void inject(MessagingModule.EagerSingletons init);
@@ -140,6 +145,8 @@ public interface BriarIntegrationTestComponent {
 
 	IntroductionManager getIntroductionManager();
 
+	MailboxIntroductionManager getMailboxIntroductionManager();
+
 	MessageTracker getMessageTracker();
 
 	PrivateGroupManager getPrivateGroupManager();
-- 
GitLab