From 7ddef7293c68f7b59f43b26d74a9ea6d2cf99e66 Mon Sep 17 00:00:00 2001
From: goapunk <goapunk@riseup.net>
Date: Mon, 9 Jul 2018 15:39:41 +0200
Subject: [PATCH] wip bramble sharing client

---
 .../mailbox/MailboxIntroductionManager.java   |  30 +++
 .../briar/BriarCoreEagerSingletons.java       |   3 +
 .../briarproject/briar/BriarCoreModule.java   |   3 +
 .../briar/mailbox/AbortMessage.java           |  28 ++
 .../AbstractMailboxIntroductionMessage.java   |  48 ++++
 .../briar/mailbox/AbstractProtocolEngine.java | 180 +++++++++++++
 .../briar/mailbox/ContactAcceptMessage.java   |  53 ++++
 .../briar/mailbox/ContactRequestMessage.java  |  42 +++
 .../briar/mailbox/DeclineMessage.java         |  28 ++
 .../briar/mailbox/Introducee.java             |  77 ++++++
 .../briar/mailbox/IntroduceeSession.java      | 246 ++++++++++++++++++
 .../briar/mailbox/IntroduceeState.java        |  35 +++
 .../briar/mailbox/MailboxAcceptMessage.java   |  53 ++++
 .../briar/mailbox/MailboxAuthMessage.java     |  65 +++++
 .../MailboxIntroductionManagerImpl.java       |  91 +++++++
 .../mailbox/MailboxIntroductionModule.java    |  42 +++
 .../briar/mailbox/MailboxProtocolEngine.java  |   4 +
 .../briar/mailbox/MailboxState.java           |  37 +++
 .../briar/mailbox/MessageEncoder.java         |  56 ++++
 .../briar/mailbox/MessageEncoderImpl.java     | 185 +++++++++++++
 .../briar/mailbox/MessageParser.java          |  44 ++++
 .../briar/mailbox/MessageParserImpl.java      | 151 +++++++++++
 .../briar/mailbox/MessageType.java            |  30 +++
 .../briar/mailbox/OwnerProtocolEngine.java    |   4 +
 .../briar/mailbox/OwnerSession.java           |  49 ++++
 .../briar/mailbox/OwnerState.java             |  36 +++
 .../briar/mailbox/PeerSession.java            |  25 ++
 .../briar/mailbox/ProtocolEngine.java         |  37 +++
 .../briar/mailbox/RequestMessage.java         |  19 ++
 .../briarproject/briar/mailbox/Session.java   |  44 ++++
 .../org/briarproject/briar/mailbox/State.java |   7 +
 31 files changed, 1752 insertions(+)
 create mode 100644 briar-api/src/main/java/org/briarproject/briar/api/mailbox/MailboxIntroductionManager.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
 create 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/MailboxState.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoderImpl.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParser.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParserImpl.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/Session.java
 create mode 100644 briar-core/src/main/java/org/briarproject/briar/mailbox/State.java

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
new file mode 100644
index 000000000..3edb9ab0b
--- /dev/null
+++ b/briar-api/src/main/java/org/briarproject/briar/api/mailbox/MailboxIntroductionManager.java
@@ -0,0 +1,30 @@
+package org.briarproject.briar.api.mailbox;
+
+import org.briarproject.bramble.api.contact.Contact;
+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;
+
+public interface MailboxIntroductionManager {
+	/**
+	 * The unique ID of the introduction client.
+	 */
+	ClientId CLIENT_ID =
+			new ClientId("org.briarproject.briar.mailbox.introduction");
+
+	/**
+	 * The current major version of the introduction client.
+	 */
+	int MAJOR_VERSION = 1;
+
+	/**
+	 * The current minor version of the introduction client.
+	 */
+	int MINOR_VERSION = 0;
+
+	void contactAdded(Transaction txn, Contact contact) throws DbException;
+
+	void privateMailboxAdded(Transaction txn, PrivateMailbox privateMailbox) throws DbException;
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/BriarCoreEagerSingletons.java b/briar-core/src/main/java/org/briarproject/briar/BriarCoreEagerSingletons.java
index 7380afc3c..2c205e969 100644
--- a/briar-core/src/main/java/org/briarproject/briar/BriarCoreEagerSingletons.java
+++ b/briar-core/src/main/java/org/briarproject/briar/BriarCoreEagerSingletons.java
@@ -4,6 +4,7 @@ import org.briarproject.briar.blog.BlogModule;
 import org.briarproject.briar.feed.FeedModule;
 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;
@@ -27,4 +28,6 @@ public interface BriarCoreEagerSingletons {
 
 	void inject(SharingModule.EagerSingletons init);
 
+	void inject(MailboxIntroductionModule.EagerSingletons init);
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/BriarCoreModule.java b/briar-core/src/main/java/org/briarproject/briar/BriarCoreModule.java
index ec7e1944f..eb6e3dc7a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/BriarCoreModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/BriarCoreModule.java
@@ -6,6 +6,7 @@ import org.briarproject.briar.feed.DnsModule;
 import org.briarproject.briar.feed.FeedModule;
 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;
@@ -25,6 +26,7 @@ import dagger.Module;
 		MessagingModule.class,
 		PrivateGroupModule.class,
 		SharingModule.class,
+		MailboxIntroductionModule.class,
 		TestModule.class
 })
 public class BriarCoreModule {
@@ -38,5 +40,6 @@ public class BriarCoreModule {
 		c.inject(new PrivateGroupModule.EagerSingletons());
 		c.inject(new SharingModule.EagerSingletons());
 		c.inject(new IntroductionModule.EagerSingletons());
+		c.inject(new MailboxIntroductionModule.EagerSingletons());
 	}
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java
new file mode 100644
index 000000000..413705a17
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbortMessage.java
@@ -0,0 +1,28 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class AbortMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+
+	protected AbortMessage(MessageId messageId, GroupId groupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId, messageCounter);
+		this.sessionId = sessionId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java
new file mode 100644
index 000000000..fd8419d72
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractMailboxIntroductionMessage.java
@@ -0,0 +1,48 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class AbstractMailboxIntroductionMessage {
+
+	private final MessageId messageId;
+	private final GroupId groupId;
+	private final long timestamp;
+	@Nullable
+	private final MessageId previousMessageId;
+	private final long messageCounter;
+
+	AbstractMailboxIntroductionMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			long messageCounter) {
+		this.messageId = messageId;
+		this.groupId = groupId;
+		this.timestamp = timestamp;
+		this.previousMessageId = previousMessageId;
+		this.messageCounter = messageCounter;
+	}
+
+	MessageId getMessageId() {
+		return messageId;
+	}
+
+	GroupId getGroupId() {
+		return groupId;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	@Nullable
+	MessageId getPreviousMessageId() {
+		return previousMessageId;
+	}
+
+}
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
new file mode 100644
index 000000000..dbd246453
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/AbstractProtocolEngine.java
@@ -0,0 +1,180 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.briar.api.client.MessageTracker;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+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, Author author, @Nullable String message)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeRequestMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), author, message);
+		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
+		);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java
new file mode 100644
index 000000000..82781be3d
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/ContactAcceptMessage.java
@@ -0,0 +1,53 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class ContactAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected ContactAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp, byte[] mac, byte[] signature,
+			long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId, messageCounter);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java
new file mode 100644
index 000000000..ecfa53859
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/ContactRequestMessage.java
@@ -0,0 +1,42 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class ContactRequestMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+
+	protected ContactRequestMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp,
+			long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId, messageCounter);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java
new file mode 100644
index 000000000..df841bdcd
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/DeclineMessage.java
@@ -0,0 +1,28 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class DeclineMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+
+	protected DeclineMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId, messageCounter);
+		this.sessionId = sessionId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java
new file mode 100644
index 000000000..d4459605f
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/Introducee.java
@@ -0,0 +1,77 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+final class Introducee implements PeerSession {
+	final SessionId sessionId;
+	final GroupId groupId;
+	final Author author;
+	final long localTimestamp;
+	@Nullable
+	final MessageId lastLocalMessageId, lastRemoteMessageId;
+
+	Introducee(SessionId sessionId, GroupId groupId, Author author,
+			long localTimestamp,
+			@Nullable MessageId lastLocalMessageId,
+			@Nullable MessageId lastRemoteMessageId) {
+		this.sessionId = sessionId;
+		this.groupId = groupId;
+		this.localTimestamp = localTimestamp;
+		this.author = author;
+		this.lastLocalMessageId = lastLocalMessageId;
+		this.lastRemoteMessageId = lastRemoteMessageId;
+	}
+
+	Introducee(Introducee i, Message sent) {
+		this(i.sessionId, i.groupId, i.author, sent.getTimestamp(),
+				sent.getId(), i.lastRemoteMessageId);
+	}
+
+	Introducee(Introducee i, MessageId remoteMessageId) {
+		this(i.sessionId, i.groupId, i.author, i.localTimestamp,
+				i.lastLocalMessageId, remoteMessageId);
+	}
+
+	Introducee(SessionId sessionId, GroupId groupId,
+			Author author) {
+		this(sessionId, groupId, author, -1, null, null);
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	@Override
+	public GroupId getContactGroupId() {
+		return groupId;
+	}
+
+	@Override
+	public long getLocalTimestamp() {
+		return localTimestamp;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastLocalMessageId() {
+		return lastLocalMessageId;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return lastRemoteMessageId;
+	}
+
+}
+
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
new file mode 100644
index 000000000..93fcf7130
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeSession.java
@@ -0,0 +1,246 @@
+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<org.briarproject.briar.mailbox.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);
+		}
+
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java
new file mode 100644
index 000000000..1f0aebd2c
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/IntroduceeState.java
@@ -0,0 +1,35 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum IntroduceeState implements State {
+
+	START(0),
+	AWAIT_LOCAL_RESPONSE(1),
+	LOCAL_DECLINED(2),
+	LOCAL_ACCEPTED(3),
+	AWAIT_REMOTE_RESPONSE(4),
+	MAILBOX_ADDED(5);
+
+	private final int value;
+
+	IntroduceeState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static IntroduceeState fromValue(int value) throws FormatException {
+		for (IntroduceeState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
new file mode 100644
index 000000000..7284e0273
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAcceptMessage.java
@@ -0,0 +1,53 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MailboxAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+	private final Map<TransportId, TransportProperties> transportProperties;
+
+	protected MailboxAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties,
+			long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId, messageCounter);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+		this.transportProperties = transportProperties;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	public Map<TransportId, TransportProperties> getTransportProperties() {
+		return transportProperties;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
new file mode 100644
index 000000000..917a841ad
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxAuthMessage.java
@@ -0,0 +1,65 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MailboxAuthMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+	private final Map<TransportId, TransportProperties> transportProperties;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected MailboxAuthMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey,
+			long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature,
+			long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId, messageCounter);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+		this.transportProperties = transportProperties;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+
+	public Map<TransportId, TransportProperties> getTransportProperties() {
+		return transportProperties;
+	}
+}
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
new file mode 100644
index 000000000..d68950aa4
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionManagerImpl.java
@@ -0,0 +1,91 @@
+package org.briarproject.briar.mailbox;
+
+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.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.Transaction;
+import org.briarproject.bramble.api.sync.Client;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
+import org.briarproject.briar.client.BdfIncomingMessageHook;
+
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.contact.ContactManager.ContactHook;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
+
+class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
+		implements MailboxIntroductionManager, Client, ClientVersioningHook,
+		ContactHook {
+
+	private static final Logger LOG =
+			Logger.getLogger(MailboxIntroductionManagerImpl.class.getName());
+
+	@Inject
+	MailboxIntroductionManagerImpl(
+			DatabaseComponent db,
+			ClientHelper clientHelper,
+			MetadataParser metadataParser) {
+		super(db, clientHelper, metadataParser);
+	}
+
+	@Override
+	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
+			BdfDictionary meta) throws DbException, FormatException {
+		return false;
+	}
+
+	@Override
+	public void addingContact(Transaction txn, Contact c) throws DbException {
+		switch (c.getType()) {
+			case PRIVATE_MAILBOX:
+				privateMailboxAdded(txn, (PrivateMailbox) c);
+				break;
+			case CONTACT:
+				contactAdded(txn, c);
+				break;
+			default:
+				return;
+		}
+
+	}
+
+	@Override
+	public void contactAdded(Transaction txn, Contact contact)
+			throws DbException {
+		LOG.info("Contact added");
+	}
+
+	@Override
+	public void privateMailboxAdded(Transaction txn,
+			PrivateMailbox privateMailbox)
+			throws DbException {
+		LOG.info("Private mailbox added");
+	}
+
+	@Override
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		LOG.info("contact removed");
+	}
+
+	@Override
+	public void createLocalState(Transaction txn) throws DbException {
+
+	}
+
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Group.Visibility v) throws DbException {
+
+	}
+
+}
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
new file mode 100644
index 000000000..967b3d6e8
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxIntroductionModule.java
@@ -0,0 +1,42 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.sync.ValidationManager;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.briar.api.mailbox.MailboxIntroductionManager;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static org.briarproject.briar.api.mailbox.MailboxIntroductionManager.CLIENT_ID;
+import static org.briarproject.briar.api.mailbox.MailboxIntroductionManager.MAJOR_VERSION;
+import static org.briarproject.briar.api.mailbox.MailboxIntroductionManager.MINOR_VERSION;
+
+@Module
+public class MailboxIntroductionModule {
+
+	public static class EagerSingletons {
+		@Inject
+		MailboxIntroductionManager mailboxIntroductionManager;
+	}
+
+	@Provides
+	@Singleton
+	MailboxIntroductionManager provideMailboxIntroductionManager(
+			LifecycleManager lifecycleManager, ContactManager contactManager,
+			ValidationManager validationManager,
+			ClientVersioningManager clientVersioningManager,
+			MailboxIntroductionManagerImpl mailboxIntroductionManager) {
+		lifecycleManager.registerClient(mailboxIntroductionManager);
+		contactManager.registerContactHook(mailboxIntroductionManager);
+		validationManager.registerIncomingMessageHook(CLIENT_ID,
+				MAJOR_VERSION, mailboxIntroductionManager);
+		clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
+				MINOR_VERSION, mailboxIntroductionManager);
+		return mailboxIntroductionManager;
+	}
+}
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
new file mode 100644
index 000000000..511250dbe
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxProtocolEngine.java
@@ -0,0 +1,4 @@
+package org.briarproject.briar.mailbox;
+
+class MailboxProtocolEngine extends  AbstractProtocolEngine {
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
new file mode 100644
index 000000000..6a44a0c4d
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MailboxState.java
@@ -0,0 +1,37 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum MailboxState implements State {
+
+	START(0),
+	AWAIT_LOCAL_RESPONSES(1),
+	LOCAL_DECLINED(2),
+	LOCAL_ACCEPTED(3),
+	AWAIT_REMOTE_RESPONSE(4),
+	REMOTE_DECLINED(5),
+	REMOTE_ACCEPTED(6),
+	CONTACT_ADDED(7);
+
+	private final int value;
+
+	MailboxState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static MailboxState fromValue(int value) throws FormatException {
+		for (MailboxState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java
new file mode 100644
index 000000000..2bf584c98
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoder.java
@@ -0,0 +1,56 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.introduction.MessageType;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface MessageEncoder {
+
+	BdfDictionary encodeRequestMetadata(long timestamp);
+
+	BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local,
+			boolean read, boolean visible);
+
+	void addSessionId(BdfDictionary meta, SessionId sessionId);
+
+	void setVisibleInUi(BdfDictionary meta, boolean visible);
+
+	void setAvailableToAnswer(BdfDictionary meta, boolean available);
+
+	Message encodeRequestMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, Author author,
+			@Nullable String message);
+
+	Message encodeAcceptMessage(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 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);
+
+}
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
new file mode 100644
index 000000000..a75d0ba27
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageEncoderImpl.java
@@ -0,0 +1,185 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageFactory;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.introduction.MessageType;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+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.ABORT;
+import static org.briarproject.briar.introduction.MessageType.ACCEPT;
+import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
+import static org.briarproject.briar.introduction.MessageType.AUTH;
+import static org.briarproject.briar.introduction.MessageType.DECLINE;
+import static org.briarproject.briar.introduction.MessageType.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(REQUEST, null, timestamp, false, false, false);
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return meta;
+	}
+
+	@Override
+	public BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local,
+			boolean read, boolean visible) {
+		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 != REQUEST)
+			throw new IllegalArgumentException();
+		meta.put(MSG_KEY_TIMESTAMP, timestamp);
+		meta.put(MSG_KEY_LOCAL, local);
+		meta.put(MSG_KEY_READ, read);
+		meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
+		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, Author author,
+			@Nullable String message) {
+		if (message != null && message.equals("")) {
+			throw new IllegalArgumentException();
+		}
+		BdfList body = BdfList.of(
+				REQUEST.getValue(),
+				previousMessageId,
+				clientHelper.toList(author),
+				message
+		);
+		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);
+		}
+	}
+
+}
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
new file mode 100644
index 000000000..5b6f729e8
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParser.java
@@ -0,0 +1,44 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.briar.api.client.SessionId;
+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 {
+
+	BdfDictionary getMessagesVisibleInUiQuery();
+
+	BdfDictionary getRequestsAvailableToAnswerQuery(SessionId sessionId);
+
+	MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
+
+	RequestMessage parseRequestMessage(Message m, BdfList body)
+			throws FormatException;
+
+	AcceptMessage parseAcceptMessage(Message m, BdfList body)
+			throws FormatException;
+
+	DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException;
+
+	AuthMessage parseAuthMessage(Message m, BdfList body)
+			throws FormatException;
+
+	ActivateMessage parseActivateMessage(Message m, BdfList body)
+			throws FormatException;
+
+	org.briarproject.briar.introduction.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
new file mode 100644
index 000000000..89284e26a
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageParserImpl.java
@@ -0,0 +1,151 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+import 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;
+
+@NotNullByDefault
+class MessageParserImpl implements
+		MessageParser {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MessageParserImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getMessagesVisibleInUiQuery() {
+		return BdfDictionary.of(new BdfEntry(MSG_KEY_VISIBLE_IN_UI, true));
+	}
+
+	@Override
+	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_SESSION_ID, sessionId)
+		);
+	}
+
+	@Override
+	public MessageMetadata parseMetadata(BdfDictionary d)
+			throws FormatException {
+		MessageType type = MessageType
+				.fromValue(d.getLong(MSG_KEY_MESSAGE_TYPE).intValue());
+		byte[] sessionIdBytes = d.getOptionalRaw(MSG_KEY_SESSION_ID);
+		SessionId sessionId =
+				sessionIdBytes == null ? null : new SessionId(sessionIdBytes);
+		long timestamp = d.getLong(MSG_KEY_TIMESTAMP);
+		boolean local = d.getBoolean(MSG_KEY_LOCAL);
+		boolean read = d.getBoolean(MSG_KEY_READ);
+		boolean visible = d.getBoolean(MSG_KEY_VISIBLE_IN_UI);
+		boolean available = d.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return new MessageMetadata(type, sessionId, timestamp, local, read,
+				visible, available);
+	}
+
+	@Override
+	public RequestMessage parseRequestMessage(Message m, BdfList body)
+			throws FormatException {
+		byte[] previousMsgBytes = body.getOptionalRaw(1);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		Author author = clientHelper.parseAndValidateAuthor(body.getList(2));
+		String message = body.getOptionalString(3);
+		return new RequestMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, author, message);
+	}
+
+	@Override
+	public AcceptMessage parseAcceptMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		long acceptTimestamp = body.getLong(4);
+		Map<TransportId, TransportProperties> transportProperties = clientHelper
+				.parseAndValidateTransportPropertiesMap(body.getDictionary(5));
+		return new AcceptMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, sessionId, ephemeralPublicKey,
+				acceptTimestamp, transportProperties);
+	}
+
+	@Override
+	public DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		return new DeclineMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, sessionId);
+	}
+
+	@Override
+	public AuthMessage parseAuthMessage(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(),
+				previousMessageId, sessionId, mac);
+	}
+
+	@Override
+	public org.briarproject.briar.introduction.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);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java
new file mode 100644
index 000000000..644de22c4
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/MessageType.java
@@ -0,0 +1,30 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum MessageType {
+
+	MAILBOX_REQUEST(0), MAILBOX_ACCEPT(1), CONTACT_REQUEST(2),
+	CONTACT_ACCEPT(3), CONTACT_INFO(4), MAILBOX_AUTH(5), DECLINE(5), ABORT(6);
+
+	private final int value;
+
+	MessageType(int value) {
+		this.value = value;
+	}
+
+	int getValue() {
+		return value;
+	}
+
+	static MessageType fromValue(int value) throws FormatException {
+		for (MessageType m : values()) if (m.value == value) return m;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
new file mode 100644
index 000000000..384f5b032
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerProtocolEngine.java
@@ -0,0 +1,4 @@
+package org.briarproject.briar.mailbox;
+
+class OwnerProtocolEngine extends AbstractProtocolEngine {
+}
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
new file mode 100644
index 000000000..b6d4525d4
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerSession.java
@@ -0,0 +1,49 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction.Role;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
+
+@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;
+	}
+
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java
new file mode 100644
index 000000000..a8ac3d6ea
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/OwnerState.java
@@ -0,0 +1,36 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum OwnerState implements State {
+
+	START(0),
+	AWAIT_RESPONSE_M(1),
+	M_DECLINE(2),
+	AWAIT_RESPONSE_B(3),
+	B_DECLINED(4),
+	AWAIT_AUTH_M(5),
+	ADDED(6);
+
+	private final int value;
+
+	OwnerState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static OwnerState fromValue(int value) throws FormatException {
+		for (OwnerState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java
new file mode 100644
index 000000000..69b944943
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/PeerSession.java
@@ -0,0 +1,25 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface PeerSession {
+
+	SessionId getSessionId();
+
+	GroupId getContactGroupId();
+
+	long getLocalTimestamp();
+
+	@Nullable
+	MessageId getLastLocalMessageId();
+
+	@Nullable
+	MessageId getLastRemoteMessageId();
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
new file mode 100644
index 000000000..9845d4a68
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/ProtocolEngine.java
@@ -0,0 +1,37 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface ProtocolEngine<S extends Session> {
+
+	S onRequestAction(Transaction txn, S session, @Nullable String message,
+			long timestamp) throws DbException;
+
+	S onAcceptAction(Transaction txn, S session, long timestamp)
+			throws DbException;
+
+	S onDeclineAction(Transaction txn, S session, long timestamp)
+			throws DbException;
+
+	S onRequestMessage(Transaction txn, S session, RequestMessage m)
+			throws DbException, FormatException;
+
+	S onAcceptMessage(Transaction txn, S session, MailboxAcceptMessage m)
+			throws DbException, FormatException;
+
+	S onDeclineMessage(Transaction txn, S session, DeclineMessage m)
+			throws DbException, FormatException;
+
+	S onAuthMessage(Transaction txn, S session, MailboxAuthMessage m)
+			throws DbException, FormatException;
+
+	S onAbortMessage(Transaction txn, S session, AbortMessage m)
+			throws DbException, FormatException;
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java
new file mode 100644
index 000000000..5c060c9e3
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/RequestMessage.java
@@ -0,0 +1,19 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class RequestMessage extends AbstractMailboxIntroductionMessage {
+
+	protected RequestMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			long messageCounter) {
+		super(messageId, groupId, timestamp, previousMessageId, messageCounter);
+	}
+}
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
new file mode 100644
index 000000000..83da3b464
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/Session.java
@@ -0,0 +1,44 @@
+package org.briarproject.briar.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction.Role;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class Session<S extends State> {
+
+	private final SessionId sessionId;
+	private final S state;
+	private final long requestTimestamp;
+	private final long sessionCounter;
+
+	Session(SessionId sessionId, S state, long requestTimestamp,
+			long sessionCounter) {
+		this.sessionId = sessionId;
+		this.state = state;
+		this.requestTimestamp = requestTimestamp;
+		this.sessionCounter = sessionCounter;
+	}
+
+	abstract Role getRole();
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	S getState() {
+		return state;
+	}
+
+	long getRequestTimestamp() {
+		return requestTimestamp;
+	}
+
+	long getSessionCounter() {
+		return sessionCounter;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/mailbox/State.java b/briar-core/src/main/java/org/briarproject/briar/mailbox/State.java
new file mode 100644
index 000000000..66a2e80d1
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/mailbox/State.java
@@ -0,0 +1,7 @@
+package org.briarproject.briar.mailbox;
+
+interface State {
+
+	int getValue();
+
+}
-- 
GitLab