From 15ea91a9bd9b2d1b44aa9c110fba3e2149a1e15a Mon Sep 17 00:00:00 2001
From: Julian Dehm <goapunk@riseup.net>
Date: Sat, 29 Sep 2018 12:11:27 +0200
Subject: [PATCH] wipper

---
 .../api}/client/BdfIncomingMessageHook.java   |   2 +-
 .../api/client/ProtocolStateException.java    |   2 +-
 .../bramble}/api/client/SessionId.java        |   2 +-
 .../bramble/api/contact/ContactFactory.java   |   3 +
 .../bramble/api/contact/ContactMailbox.java   |  26 +
 .../bramble/api/contact/ContactManager.java   |  15 +-
 .../bramble/api/db/DatabaseConfig.java        |   1 +
 .../api/mailbox/IntroductionConstants.java    |  24 +
 .../mailbox/MailboxIntroductionManager.java   |  40 ++
 .../bramble/api/mailbox/Role.java             |  29 ++
 .../MailboxIntroductionAbortedEvent.java      |  22 +
 ...ilboxIntroductionRequestReceivedEvent.java |  24 +
 ...lboxIntroductionResponseReceivedEvent.java |  29 ++
 .../MailboxIntroductionSucceededEvent.java    |  22 +
 .../properties/TransportPropertyManager.java  |   4 +
 bramble-core/build.gradle                     |   1 +
 .../bramble/contact/ContactManagerImpl.java   |  65 ++-
 .../briarproject/bramble/db/JdbcDatabase.java |   2 +-
 .../mailbox/introduction/AbortMessage.java    |  27 +
 .../AbstractIntroduceeSession.java            | 156 ++++++
 .../AbstractMailboxIntroductionMessage.java   |  45 ++
 .../introduction/AbstractProtocolEngine.java  | 168 ++++++
 .../mailbox/introduction/DeclineMessage.java  |  28 +
 .../mailbox/introduction/Introducee.java      |  83 +++
 .../introduction/IntroduceeAcceptMessage.java |  52 ++
 .../IntroduceeProtocolEngine.java             | 230 +++++++++
 .../introduction/IntroduceeSession.java       | 147 ++++++
 .../mailbox/introduction/IntroduceeState.java |  33 ++
 .../introduction/IntroductionConstants.java   |  49 ++
 .../introduction/MailboxAcceptMessage.java    |  48 ++
 .../introduction/MailboxAuthMessage.java      |  51 ++
 .../MailboxIntroductionCrypto.java            |  95 ++++
 .../MailboxIntroductionCryptoImpl.java        | 190 +++++++
 .../MailboxIntroductionManagerImpl.java       | 486 ++++++++++++++++++
 .../MailboxIntroductionModule.java            |  95 ++++
 .../MailboxIntroductionValidator.java         | 216 ++++++++
 .../introduction/MailboxMessageEncoder.java   |  57 ++
 .../MailboxMessageEncoderImpl.java            | 160 ++++++
 .../introduction/MailboxMessageParser.java    |  33 ++
 .../MailboxMessageParserImpl.java             | 134 +++++
 .../introduction/MailboxProtocolEngine.java   | 263 ++++++++++
 .../mailbox/introduction/MailboxSession.java  | 136 +++++
 .../introduction/MailboxSessionEncoder.java   |  18 +
 .../MailboxSessionEncoderImpl.java            | 155 ++++++
 .../introduction/MailboxSessionParser.java    |  27 +
 .../MailboxSessionParserImpl.java             | 231 +++++++++
 .../mailbox/introduction/MailboxState.java    |  34 ++
 .../mailbox/introduction/MessageMetadata.java |  55 ++
 .../mailbox/introduction/MessageType.java     |  35 ++
 .../introduction/OwnerProtocolEngine.java     | 262 ++++++++++
 .../mailbox/introduction/OwnerSession.java    |  52 ++
 .../mailbox/introduction/OwnerState.java      |  36 ++
 .../mailbox/introduction/PeerSession.java     |  27 +
 .../mailbox/introduction/ProtocolEngine.java  |  30 ++
 .../mailbox/introduction/RequestMessage.java  |  28 +
 .../bramble/mailbox/introduction/Session.java |  44 ++
 .../bramble/mailbox/introduction/State.java   |   7 +
 .../TransportPropertyManagerImpl.java         |  15 +-
 .../integration/BrambleIntegrationTest.java   | 404 +++++++++++++++
 .../BrambleIntegrationTestComponent.java      | 102 ++++
 .../bramble/integration/TestStreamWriter.java |  25 +
 .../MailboxIntroductionIntegrationTest.java   | 303 +++++++++++
 ...xIntroductionIntegrationTestComponent.java |  62 +++
 .../briar/api/blog/BlogInvitationRequest.java |   2 +-
 .../api/blog/BlogInvitationResponse.java      |   2 +-
 .../api/forum/ForumInvitationRequest.java     |   2 +-
 .../api/forum/ForumInvitationResponse.java    |   2 +-
 .../api/introduction/IntroductionManager.java |   2 +-
 .../api/introduction/IntroductionMessage.java |   2 +-
 .../api/introduction/IntroductionRequest.java |   2 +-
 .../introduction/IntroductionResponse.java    |   2 +-
 .../event/IntroductionAbortedEvent.java       |   2 +-
 .../invitation/GroupInvitationManager.java    |   4 +-
 .../invitation/GroupInvitationRequest.java    |   2 +-
 .../invitation/GroupInvitationResponse.java   |   2 +-
 .../briar/api/sharing/InvitationMessage.java  |   2 +-
 .../briar/api/sharing/InvitationRequest.java  |   2 +-
 .../briar/api/sharing/InvitationResponse.java |   2 +-
 .../briar/api/sharing/SharingManager.java     |   2 +-
 .../briar/blog/BlogManagerImpl.java           |   2 +-
 .../briar/client/ConversationClientImpl.java  |   1 +
 .../briar/forum/ForumManagerImpl.java         |   2 +-
 .../briar/introduction/AbortMessage.java      |   2 +-
 .../introduction/AbstractProtocolEngine.java  |   2 +-
 .../briar/introduction/AcceptMessage.java     |   2 +-
 .../briar/introduction/ActivateMessage.java   |   2 +-
 .../briar/introduction/AuthMessage.java       |   2 +-
 .../briar/introduction/DeclineMessage.java    |   2 +-
 .../IntroduceeProtocolEngine.java             |   4 +-
 .../briar/introduction/IntroduceeSession.java |   2 +-
 .../IntroducerProtocolEngine.java             |   3 +-
 .../briar/introduction/IntroducerSession.java |   2 +-
 .../introduction/IntroductionCrypto.java      |   2 +-
 .../introduction/IntroductionCryptoImpl.java  |   2 +-
 .../introduction/IntroductionManagerImpl.java |   3 +-
 .../introduction/IntroductionValidator.java   |   2 +-
 .../briar/introduction/MessageEncoder.java    |   2 +-
 .../introduction/MessageEncoderImpl.java      |   2 +-
 .../briar/introduction/MessageMetadata.java   |   2 +-
 .../briar/introduction/MessageParser.java     |   2 +-
 .../briar/introduction/MessageParserImpl.java |   2 +-
 .../briar/introduction/PeerSession.java       |   2 +-
 .../briar/introduction/Session.java           |   2 +-
 .../briar/introduction/SessionParser.java     |   2 +-
 .../briar/introduction/SessionParserImpl.java |   2 +-
 .../briar/messaging/MessagingManagerImpl.java |   1 +
 .../privategroup/PrivateGroupManagerImpl.java |   4 +-
 .../invitation/CreatorProtocolEngine.java     |   4 +-
 .../GroupInvitationManagerImpl.java           |   4 +-
 .../invitation/InviteeProtocolEngine.java     |   4 +-
 .../invitation/PeerProtocolEngine.java        |   2 +-
 .../invitation/SessionParser.java             |   2 +-
 .../invitation/SessionParserImpl.java         |   2 +-
 .../sharing/BlogInvitationFactoryImpl.java    |   2 +-
 .../sharing/ForumInvitationFactoryImpl.java   |   2 +-
 .../briar/sharing/ProtocolEngineImpl.java     |   2 +-
 .../briar/sharing/SessionParser.java          |   2 +-
 .../briar/sharing/SessionParserImpl.java      |   2 +-
 .../briar/sharing/SharingManagerImpl.java     |   4 +-
 .../IntroductionCryptoIntegrationTest.java    |   2 +-
 .../introduction/IntroductionCryptoTest.java  |   2 +-
 .../IntroductionIntegrationTest.java          |   4 +-
 .../IntroductionValidatorTest.java            |   2 +-
 .../MessageEncoderParserIntegrationTest.java  |   2 +-
 .../SessionEncoderParserIntegrationTest.java  |   2 +-
 .../invitation/CreatorProtocolEngineTest.java |   2 +-
 .../GroupInvitationIntegrationTest.java       |   2 +-
 .../GroupInvitationManagerImplTest.java       |   2 +-
 .../invitation/InviteeProtocolEngineTest.java |   2 +-
 .../invitation/PeerProtocolEngineTest.java    |   2 +-
 .../sharing/BlogSharingManagerImplTest.java   |   2 +-
 131 files changed, 5320 insertions(+), 79 deletions(-)
 rename {briar-core/src/main/java/org/briarproject/briar => bramble-api/src/main/java/org/briarproject/bramble/api}/client/BdfIncomingMessageHook.java (98%)
 rename {briar-api/src/main/java/org/briarproject/briar => bramble-api/src/main/java/org/briarproject/bramble}/api/client/ProtocolStateException.java (89%)
 rename {briar-api/src/main/java/org/briarproject/briar => bramble-api/src/main/java/org/briarproject/bramble}/api/client/SessionId.java (91%)
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactMailbox.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/IntroductionConstants.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxIntroductionManager.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/Role.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionAbortedEvent.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionRequestReceivedEvent.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionResponseReceivedEvent.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionSucceededEvent.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbortMessage.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractIntroduceeSession.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractMailboxIntroductionMessage.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractProtocolEngine.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/DeclineMessage.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Introducee.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeAcceptMessage.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeProtocolEngine.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeSession.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeState.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroductionConstants.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAcceptMessage.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAuthMessage.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCrypto.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCryptoImpl.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionManagerImpl.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionModule.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionValidator.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoder.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoderImpl.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParser.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParserImpl.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxProtocolEngine.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSession.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoder.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoderImpl.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParser.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParserImpl.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxState.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageMetadata.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageType.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerProtocolEngine.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerSession.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerState.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/PeerSession.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/ProtocolEngine.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/RequestMessage.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Session.java
 create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/State.java
 create mode 100644 bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTest.java
 create mode 100644 bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTestComponent.java
 create mode 100644 bramble-core/src/test/java/org/briarproject/bramble/integration/TestStreamWriter.java
 create mode 100644 bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTest.java
 create mode 100644 bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTestComponent.java

diff --git a/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java b/bramble-api/src/main/java/org/briarproject/bramble/api/client/BdfIncomingMessageHook.java
similarity index 98%
rename from briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java
rename to bramble-api/src/main/java/org/briarproject/bramble/api/client/BdfIncomingMessageHook.java
index edc62948d..30de75695 100644
--- a/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/client/BdfIncomingMessageHook.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.client;
+package org.briarproject.bramble.api.client;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/ProtocolStateException.java b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ProtocolStateException.java
similarity index 89%
rename from briar-api/src/main/java/org/briarproject/briar/api/client/ProtocolStateException.java
rename to bramble-api/src/main/java/org/briarproject/bramble/api/client/ProtocolStateException.java
index a879e912d..3672e091f 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/client/ProtocolStateException.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ProtocolStateException.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.api.client;
+package org.briarproject.bramble.api.client;
 
 import org.briarproject.bramble.api.db.DbException;
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/SessionId.java b/bramble-api/src/main/java/org/briarproject/bramble/api/client/SessionId.java
similarity index 91%
rename from briar-api/src/main/java/org/briarproject/briar/api/client/SessionId.java
rename to bramble-api/src/main/java/org/briarproject/bramble/api/client/SessionId.java
index d569bdfec..e350327d8 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/client/SessionId.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/client/SessionId.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.api.client;
+package org.briarproject.bramble.api.client;
 
 import org.briarproject.bramble.api.UniqueId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactFactory.java
index b12c19c99..ab3eb6eae 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactFactory.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactFactory.java
@@ -19,6 +19,9 @@ public interface ContactFactory {
 			case MAILBOX_OWNER:
 				return new MailboxOwner(c, author, localAuthorId, verified,
 						active);
+			case CONTACT_MAILBOX:
+				return new ContactMailbox(c, author, localAuthorId, verified,
+						active);
 			default:
 				throw new IllegalArgumentException("Unknown contact type");
 		}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactMailbox.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactMailbox.java
new file mode 100644
index 000000000..6a044b94b
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactMailbox.java
@@ -0,0 +1,26 @@
+package org.briarproject.bramble.api.contact;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.contact.ContactType.CONTACT_MAILBOX;
+
+@Immutable
+@NotNullByDefault
+public class ContactMailbox extends Contact {
+
+	public ContactMailbox(ContactId id,
+			Author author,
+			AuthorId localAuthorId,
+			boolean verified, boolean active) {
+		super(id, author, localAuthorId, verified, active);
+	}
+
+	@Override
+	public ContactType getType() {
+		return CONTACT_MAILBOX;
+	}
+}
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..5b87dc303 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,14 @@ 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;
+
+	ContactId addContactMailbox(Transaction txn, Author remote,
+			AuthorId local, SecretKey master, long timestamp, boolean alice)
+			throws DbException;
+
 	/**
 	 * Returns the contact with the given ID.
 	 */
@@ -96,6 +108,8 @@ public interface ContactManager {
 	 */
 	PrivateMailbox getPrivateMailbox() throws DbException;
 
+	PrivateMailbox getPrivateMailbox(Transaction txn) throws DbException;
+
 	/**
 	 * Removes a contact and all associated state.
 	 */
@@ -124,7 +138,6 @@ public interface ContactManager {
 	boolean contactExists(AuthorId remoteAuthorId, AuthorId localAuthorId)
 			throws DbException;
 
-
 	interface ContactHook {
 
 		Collection<ContactType> getApplicableContactTypes();
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseConfig.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseConfig.java
index f096f1fec..0df7b80a7 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseConfig.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseConfig.java
@@ -12,4 +12,5 @@ public interface DatabaseConfig {
 	File getDatabaseKeyDirectory();
 
 	long getMaxSize();
+
 }
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/IntroductionConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/IntroductionConstants.java
new file mode 100644
index 000000000..651309882
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/IntroductionConstants.java
@@ -0,0 +1,24 @@
+package org.briarproject.bramble.api.mailbox;
+
+public interface IntroductionConstants {
+
+	String LABEL_SESSION_ID = "org.briarproject.briar.introduction/SESSION_ID";
+
+	String LABEL_MASTER_KEY = "org.briarproject.briar.introduction/MASTER_KEY";
+
+	String LABEL_ALICE_MAC_KEY =
+			"org.briarproject.briar.introduction/ALICE_MAC_KEY";
+
+	String LABEL_BOB_MAC_KEY =
+			"org.briarproject.briar.introduction/BOB_MAC_KEY";
+
+	String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC";
+
+	String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN";
+
+	String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE";
+
+	String LABEL_ACTIVATE_MAC =
+			"org.briarproject.briar.introduction/ACTIVATE_MAC";
+
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxIntroductionManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxIntroductionManager.java
new file mode 100644
index 000000000..ca912d551
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxIntroductionManager.java
@@ -0,0 +1,40 @@
+package org.briarproject.bramble.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;
+import org.briarproject.bramble.api.sync.Group;
+
+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;
+
+	/**
+	 * Sends two initial introduction messages.
+	 */
+	void makeIntroduction(PrivateMailbox privateMailbox, Contact contact, long timestamp)
+			throws DbException;
+
+
+	Group getContactGroup(Contact c);
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/Role.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/Role.java
new file mode 100644
index 000000000..df8655b9a
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/Role.java
@@ -0,0 +1,29 @@
+package org.briarproject.bramble.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/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionAbortedEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionAbortedEvent.java
new file mode 100644
index 000000000..38e36fa46
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionAbortedEvent.java
@@ -0,0 +1,22 @@
+package org.briarproject.bramble.api.mailbox.event;
+
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.client.SessionId;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class MailboxIntroductionAbortedEvent extends Event {
+
+	private SessionId sessionId;
+
+	public MailboxIntroductionAbortedEvent(SessionId sessionId) {
+		this.sessionId = sessionId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionRequestReceivedEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionRequestReceivedEvent.java
new file mode 100644
index 000000000..a690247f7
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionRequestReceivedEvent.java
@@ -0,0 +1,24 @@
+package org.briarproject.bramble.api.mailbox.event;
+
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class MailboxIntroductionRequestReceivedEvent extends Event {
+
+	private final AuthorId contactId;
+
+	public MailboxIntroductionRequestReceivedEvent(AuthorId contactId) {
+		this.contactId = contactId;
+
+	}
+
+	public AuthorId getAuthorId() {
+		return contactId;
+	}
+
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionResponseReceivedEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionResponseReceivedEvent.java
new file mode 100644
index 000000000..3f655215e
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionResponseReceivedEvent.java
@@ -0,0 +1,29 @@
+package org.briarproject.bramble.api.mailbox.event;
+
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class MailboxIntroductionResponseReceivedEvent extends Event {
+
+	private final Author from;
+	private final Author to;
+
+	public MailboxIntroductionResponseReceivedEvent(Author from, Author to) {
+		this.from = from;
+		this.to = to;
+	}
+
+
+	public Author getFrom() {
+		return from;
+	}
+
+	public Author getTo() {
+		return to;
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionSucceededEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionSucceededEvent.java
new file mode 100644
index 000000000..df6cb7b8b
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/event/MailboxIntroductionSucceededEvent.java
@@ -0,0 +1,22 @@
+package org.briarproject.bramble.api.mailbox.event;
+
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class MailboxIntroductionSucceededEvent extends Event {
+
+	private final Contact contact;
+
+	public MailboxIntroductionSucceededEvent(Contact contact) {
+		this.contact = contact;
+	}
+
+	public Contact getContact() {
+		return contact;
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
index 1c64f5b6d..164d1f5a2 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
@@ -35,6 +35,10 @@ public interface TransportPropertyManager {
 	void addRemoteProperties(Transaction txn, ContactId c,
 			Map<TransportId, TransportProperties> props) throws DbException;
 
+	Map<TransportId, TransportProperties> getLocalAnonymizedProperties(
+			Transaction txn)
+			throws DbException;
+
 	/**
 	 * Returns the local transport properties for all transports.
 	 */
diff --git a/bramble-core/build.gradle b/bramble-core/build.gradle
index eec6c4387..e0e54ac4f 100644
--- a/bramble-core/build.gradle
+++ b/bramble-core/build.gradle
@@ -22,6 +22,7 @@ dependencies {
 	testImplementation project(path: ':bramble-api', configuration: 'testOutput')
 	testImplementation 'org.hsqldb:hsqldb:2.3.5' // The last version that supports Java 1.6
 	testImplementation 'junit:junit:4.12'
+	testImplementation 'net.jodah:concurrentunit:0.4.2'
 	testImplementation "org.jmock:jmock:2.8.2"
 	testImplementation "org.jmock:jmock-junit4:2.8.2"
 	testImplementation "org.jmock:jmock-legacy:2.8.2"
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..c600e3a21 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
@@ -24,6 +24,7 @@ import javax.annotation.concurrent.ThreadSafe;
 import javax.inject.Inject;
 
 import static org.briarproject.bramble.api.contact.ContactType.CONTACT;
+import static org.briarproject.bramble.api.contact.ContactType.CONTACT_MAILBOX;
 import static org.briarproject.bramble.api.contact.ContactType.MAILBOX_OWNER;
 import static org.briarproject.bramble.api.contact.ContactType.PRIVATE_MAILBOX;
 
@@ -91,6 +92,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 +123,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)
@@ -121,6 +154,21 @@ class ContactManagerImpl implements ContactManager {
 		return c;
 	}
 
+	@Override
+	public ContactId addContactMailbox(Transaction txn, Author remote,
+			AuthorId local, SecretKey master, long timestamp, boolean alice)
+			throws DbException {
+		ContactId c = db.addContact(txn, remote, local, false, true,
+				CONTACT_MAILBOX);
+		//keyManager.addContact(txn, c, master, timestamp, alice, true);
+		Contact contact = db.getContact(txn, c);
+		for (ContactHook hook : hooks) {
+			if (hook.getApplicableContactTypes().contains(contact.getType()))
+				hook.addingContact(txn, contact);
+		}
+		return c;
+	}
+
 	@Override
 	public Contact getContact(ContactId c) throws DbException {
 		Contact contact;
@@ -181,7 +229,7 @@ class ContactManagerImpl implements ContactManager {
 		Collection<Contact> contacts;
 		Transaction txn = db.startTransaction(true);
 		try {
-			contacts = db.getContactsByType(txn, type);
+			contacts = getContactsByType(txn, type);
 			db.commitTransaction(txn);
 		} finally {
 			db.endTransaction(txn);
@@ -189,6 +237,11 @@ class ContactManagerImpl implements ContactManager {
 		return contacts;
 	}
 
+	private Collection<Contact> getContactsByType(Transaction txn,
+			ContactType type) throws DbException {
+		return db.getContactsByType(txn, type);
+	}
+
 	@Override
 	public PrivateMailbox getPrivateMailbox() throws DbException {
 		Collection<Contact> privateMailbox = getContactsByType(PRIVATE_MAILBOX);
@@ -197,6 +250,16 @@ class ContactManagerImpl implements ContactManager {
 		return (PrivateMailbox) privateMailbox.iterator().next();
 	}
 
+	@Override
+	public PrivateMailbox getPrivateMailbox(Transaction txn)
+			throws DbException {
+		Collection<Contact> privateMailbox =
+				getContactsByType(txn, PRIVATE_MAILBOX);
+		if (privateMailbox.isEmpty())
+			return null;
+		return (PrivateMailbox) privateMailbox.iterator().next();
+	}
+
 	@Override
 	public void removeContact(ContactId c) throws DbException {
 		Transaction txn = db.startTransaction(false);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index 132f3b8de..a185633c7 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -996,7 +996,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, contactId.getInt());
 			if (mailboxId == null) ps.setNull(2, INTEGER);
-			else ps.setInt(1, mailboxId.getInt());
+			else ps.setInt(2, mailboxId.getInt());
 			if (aliasId == null) ps.setNull(3, INTEGER);
 			else ps.setInt(3, aliasId.getInt());
 			int affected = ps.executeUpdate();
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbortMessage.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbortMessage.java
new file mode 100644
index 000000000..02bd625a5
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbortMessage.java
@@ -0,0 +1,27 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 AbortMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+
+	protected AbortMessage(MessageId messageId, GroupId groupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractIntroduceeSession.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractIntroduceeSession.java
new file mode 100644
index 000000000..438bc62cb
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractIntroduceeSession.java
@@ -0,0 +1,156 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class AbstractIntroduceeSession<S extends State> extends Session<S>
+		implements PeerSession {
+
+	final GroupId contactGroupId;
+	final Author introducer;
+	final Local local;
+	final Remote remote;
+	@Nullable
+	final byte[] masterKey;
+	@Nullable
+	final Map<TransportId, KeySetId> transportKeys;
+
+	AbstractIntroduceeSession(SessionId sessionId, S state,
+			long requestTimestamp, GroupId contactGroupId, Author introducer,
+			Local local, Remote remote, @Nullable byte[] masterKey,
+			@Nullable Map<TransportId, KeySetId> transportKeys,
+			long abortCounter) {
+		super(sessionId, state, requestTimestamp, abortCounter);
+		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;
+	}
+
+	Author getIntroducer() {
+		return introducer;
+	}
+
+	Local getLocal() {
+		return local;
+	}
+
+	Remote getRemote() {
+		return remote;
+	}
+
+	@Nullable
+	byte[] getMasterKey() {
+		return masterKey;
+	}
+
+	@Nullable
+	Map<TransportId, KeySetId> getTransportKeys() {
+		return transportKeys;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return remote.lastMessageId;
+	}
+
+	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(Local s, @Nullable MessageId lastMessageId,
+				long lastMessageTimestamp) {
+			this(s.alice, lastMessageId, lastMessageTimestamp,
+					s.ephemeralPublicKey, s.ephemeralPrivateKey,
+					s.acceptTimestamp, s.macKey);
+		}
+
+		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;
+		}
+	}
+
+	static class Remote extends Common {
+		final Author author;
+		@Nullable
+		final Map<TransportId, TransportProperties> transportProperties;
+
+		Remote(Remote s, @Nullable MessageId lastMessageId) {
+			this(s.alice, s.author, lastMessageId, s.ephemeralPublicKey,
+					s.transportProperties, s.acceptTimestamp, s.macKey);
+		}
+
+		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;
+		}
+
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractMailboxIntroductionMessage.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractMailboxIntroductionMessage.java
new file mode 100644
index 000000000..f498ce766
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractMailboxIntroductionMessage.java
@@ -0,0 +1,45 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class AbstractMailboxIntroductionMessage {
+
+	private final MessageId messageId;
+	private final GroupId groupId;
+	private final long timestamp;
+	@Nullable
+	private final MessageId previousMessageId;
+
+	AbstractMailboxIntroductionMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId) {
+		this.messageId = messageId;
+		this.groupId = groupId;
+		this.timestamp = timestamp;
+		this.previousMessageId = previousMessageId;
+	}
+
+	MessageId getMessageId() {
+		return messageId;
+	}
+
+	GroupId getGroupId() {
+		return groupId;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	@Nullable
+	MessageId getPreviousMessageId() {
+		return previousMessageId;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractProtocolEngine.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractProtocolEngine.java
new file mode 100644
index 000000000..8a998aa5e
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/AbstractProtocolEngine.java
@@ -0,0 +1,168 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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.client.SessionId;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.mailbox.introduction.MessageType.ABORT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.DECLINE;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_ACCEPT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_AUTH;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_REQUEST;
+
+@Immutable
+@NotNullByDefault
+abstract class AbstractProtocolEngine<S extends Session>
+		implements ProtocolEngine<S> {
+
+	protected final DatabaseComponent db;
+	protected final ClientHelper clientHelper;
+	protected final ContactManager contactManager;
+	protected final ContactGroupFactory contactGroupFactory;
+	protected final IdentityManager identityManager;
+	protected final MailboxMessageParser messageParser;
+	protected final MailboxMessageEncoder messageEncoder;
+	protected final Clock clock;
+	protected final MailboxIntroductionCrypto crypto;
+	protected final KeyManager keyManager;
+	protected final TransportPropertyManager transportPropertyManager;
+
+	AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager, MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+		this.contactManager = contactManager;
+		this.contactGroupFactory = contactGroupFactory;
+		this.identityManager = identityManager;
+		this.messageParser = messageParser;
+		this.messageEncoder = messageEncoder;
+		this.clock = clock;
+		this.crypto = crypto;
+		this.keyManager = keyManager;
+		this.transportPropertyManager = transportPropertyManager;
+	}
+
+	Message sendMailboxRequestMessage(Transaction txn, PeerSession s,
+			long timestamp, Author introduceeAuthor) throws DbException {
+		Message m = messageEncoder
+				.encodeRequestMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), introduceeAuthor,
+						s.getAbortCounter());
+		sendMessage(txn, MAILBOX_REQUEST, s.getSessionId(), m,
+				s.getAbortCounter());
+		return m;
+	}
+
+	Message sendMailboxAcceptMessage(Transaction txn, PeerSession s,
+			long timestamp, Author author, byte[] ephemeralPublicKey,
+			long acceptTimestamp) throws DbException {
+		Message m = messageEncoder
+				.encodeMailboxAcceptMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId(),
+						MAILBOX_ACCEPT, author, ephemeralPublicKey,
+						acceptTimestamp, s.getAbortCounter());
+		sendMessage(txn, MAILBOX_ACCEPT, s.getSessionId(), m,
+				s.getAbortCounter());
+		return m;
+	}
+
+	Message sendIntroduceeResponseMessage(Transaction txn, PeerSession s,
+			MessageId previousMessage, long timestamp,
+			byte[] ephemeralPublicKey, byte[] mac, byte[] signature,
+			long acceptTimestamp) throws DbException {
+		Message m = messageEncoder
+				.encodeIntroduceeAcceptMessage(s.getContactGroupId(), timestamp,
+						previousMessage, s.getSessionId(), ephemeralPublicKey,
+						mac, signature, acceptTimestamp, s.getAbortCounter());
+		sendMessage(txn, INTRODUCEE_ACCEPT, s.getSessionId(), m,
+				s.getAbortCounter());
+		return m;
+	}
+
+	Message sendMailboxAuthMessage(Transaction txn, PeerSession s,
+			long timestamp,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature) throws DbException {
+		Message m = messageEncoder
+				.encodeMailboxAuthMessage(s.getContactGroupId(), timestamp,
+						s.getLastRemoteMessageId(), s.getSessionId(),
+						transportProperties, mac, signature,
+						s.getAbortCounter());
+		sendMessage(txn, MAILBOX_AUTH, s.getSessionId(), m,
+				s.getAbortCounter());
+		return m;
+	}
+
+	Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeDeclineMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId(),
+						s.getAbortCounter());
+		sendMessage(txn, DECLINE, s.getSessionId(), m, s.getAbortCounter());
+		return m;
+	}
+
+	Message sendAbortMessage(Transaction txn, PeerSession s, long timestamp)
+			throws DbException {
+		Message m = messageEncoder
+				.encodeAbortMessage(s.getContactGroupId(), timestamp,
+						s.getLastLocalMessageId(), s.getSessionId(),
+						s.getAbortCounter());
+		sendMessage(txn, ABORT, s.getSessionId(), m, s.getAbortCounter());
+		return m;
+	}
+
+	private void sendMessage(Transaction txn, MessageType type,
+			SessionId sessionId, Message m, long abortCounter)
+			throws DbException {
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, sessionId, m.getTimestamp(), true,
+						abortCounter);
+		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/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/DeclineMessage.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/DeclineMessage.java
new file mode 100644
index 000000000..d7367c467
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/DeclineMessage.java
@@ -0,0 +1,28 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 DeclineMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+
+	protected DeclineMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Introducee.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Introducee.java
new file mode 100644
index 000000000..8494cfdcb
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Introducee.java
@@ -0,0 +1,83 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 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;
+	final long abortCounter;
+
+	Introducee(SessionId sessionId, GroupId groupId, Author author,
+			long localTimestamp, @Nullable MessageId lastLocalMessageId,
+			@Nullable MessageId lastRemoteMessageId, long abortCounter) {
+		this.sessionId = sessionId;
+		this.groupId = groupId;
+		this.localTimestamp = localTimestamp;
+		this.author = author;
+		this.lastLocalMessageId = lastLocalMessageId;
+		this.lastRemoteMessageId = lastRemoteMessageId;
+		this.abortCounter = abortCounter;
+	}
+
+	Introducee(Introducee i, Message sent, long abortCounter) {
+		this(i.sessionId, i.groupId, i.author, sent.getTimestamp(),
+				sent.getId(), i.lastRemoteMessageId, abortCounter);
+	}
+
+	Introducee(Introducee i, MessageId remoteMessageId, long abortCounter) {
+		this(i.sessionId, i.groupId, i.author, i.localTimestamp,
+				i.lastLocalMessageId, remoteMessageId, abortCounter);
+	}
+
+	Introducee(SessionId sessionId, GroupId groupId, Author author,
+			long abortCounter) {
+		this(sessionId, groupId, author, -1, null, null, abortCounter);
+	}
+
+	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;
+	}
+
+	@Override
+	public long getAbortCounter() {
+		return abortCounter;
+	}
+
+}
+
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeAcceptMessage.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeAcceptMessage.java
new file mode 100644
index 000000000..fc34dddc1
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeAcceptMessage.java
@@ -0,0 +1,52 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 IntroduceeAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected IntroduceeAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey, byte[] mac,
+			byte[] signature, long acceptTimestamp) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeProtocolEngine.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeProtocolEngine.java
new file mode 100644
index 000000000..83fe29e7c
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeProtocolEngine.java
@@ -0,0 +1,230 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionAbortedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionSucceededEvent;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.bramble.api.transport.KeySetId;
+
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.mailbox.introduction.IntroduceeState.AWAIT_AUTH;
+import static org.briarproject.bramble.mailbox.introduction.IntroduceeState.MAILBOX_ADDED;
+import static org.briarproject.bramble.mailbox.introduction.IntroduceeState.START;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+class IntroduceeProtocolEngine
+		extends AbstractProtocolEngine<IntroduceeSession> {
+
+	private final static Logger LOG =
+			Logger.getLogger(IntroduceeProtocolEngine.class.getName());
+
+	@Inject
+	IntroduceeProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager, MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		super(db, clientHelper, contactManager, contactGroupFactory,
+				identityManager, messageParser, messageEncoder, clock, crypto,
+				keyManager, transportPropertyManager);
+	}
+
+	@Override
+	public IntroduceeSession onRequestMessage(Transaction txn,
+			IntroduceeSession session, RequestMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public IntroduceeSession onMailboxAcceptMessage(Transaction txn,
+			IntroduceeSession s, MailboxAcceptMessage m)
+			throws DbException, FormatException {
+		switch (s.getState()) {
+			case START:
+				return onRemoteRequest(txn, s, m);
+			case LOCAL_DECLINED:
+			case MAILBOX_ADDED:
+				return abort(txn, s);
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Override
+	public IntroduceeSession onIntroduceeAcceptMessage(Transaction txn,
+			IntroduceeSession session, IntroduceeAcceptMessage acceptMessage) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public IntroduceeSession onDeclineMessage(Transaction txn,
+			IntroduceeSession s, DeclineMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public IntroduceeSession onAuthMessage(Transaction txn, IntroduceeSession s,
+			MailboxAuthMessage m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case AWAIT_AUTH:
+				return handleAuthMessage(txn, s, m);
+			case START:
+			case LOCAL_DECLINED:
+			case MAILBOX_ADDED:
+				return abort(txn, s);
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Override
+	public IntroduceeSession onAbortMessage(Transaction txn,
+			IntroduceeSession s, AbortMessage m) {
+		// Broadcast abort event for testing
+		txn.attach(new MailboxIntroductionAbortedEvent(s.getSessionId()));
+		// Reset the session back to initial state
+		return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(),
+				s.getLocalTimestamp(), m.getMessageId(),
+				s.getAbortCounter() + 1);
+	}
+
+	private IntroduceeSession handleAuthMessage(Transaction txn,
+			IntroduceeSession s, MailboxAuthMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		try {
+			crypto.verifyAuthMac(m.getMac(), s, localAuthor.getId());
+			crypto.verifySignature(m.getSignature(), s);
+			long timestamp = Math.min(s.getLocal().acceptTimestamp,
+					s.getRemote().acceptTimestamp);
+			if (timestamp == -1) throw new AssertionError();
+			contactManager.addContactMailbox(txn, s.getRemote().author,
+					localAuthor.getId(), new SecretKey(s.masterKey), timestamp,
+					false);
+			Contact c = contactManager
+					.getContact(txn, s.getRemote().author.getId(),
+							localAuthor.getId());
+			// add the keys to the new mailbox
+			//noinspection ConstantConditions
+			Map<TransportId, KeySetId> keys = keyManager
+					.addContact(txn, c.getId(), new SecretKey(s.masterKey),
+							timestamp, false, true);
+			// add signed transport properties for the mailbox
+			//noinspection ConstantConditions
+			transportPropertyManager.addRemoteProperties(txn, c.getId(),
+					m.getTransportProperties());
+			Contact owner = contactManager
+					.getContact(txn, s.getIntroducer().getId(),
+							localAuthor.getId());
+			// Mark the mailbox as usable for the owner
+			db.setMailboxForContact(txn, owner.getId(), c.getId(), null);
+			MailboxIntroductionSucceededEvent e =
+					new MailboxIntroductionSucceededEvent(c);
+			txn.attach(e);
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+		return IntroduceeSession
+				.clear(s, MAILBOX_ADDED, s.getLastLocalMessageId(),
+						clock.currentTimeMillis(), m.getMessageId(),
+						s.getAbortCounter());
+	}
+
+	private IntroduceeSession abort(Transaction txn, IntroduceeSession s)
+			throws DbException {
+		// Send an ABORT message
+		Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s));
+		// Broadcast abort event for testing
+		txn.attach(new MailboxIntroductionAbortedEvent(s.getSessionId()));
+		// Reset the session back to initial state
+		return IntroduceeSession
+				.clear(s, START, sent.getId(), sent.getTimestamp(),
+						s.getLastRemoteMessageId(), s.getAbortCounter() + 1);
+	}
+
+	private long getLocalTimestamp(AbstractIntroduceeSession s) {
+		return getLocalTimestamp(s.getLocalTimestamp(),
+				s.getRequestTimestamp());
+	}
+
+	private IntroduceeSession onRemoteRequest(Transaction txn,
+			IntroduceeSession s, MailboxAcceptMessage m)
+			throws DbException, FormatException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Broadcast IntroductionRequestReceivedEvent
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		txn.attach(new MailboxIntroductionRequestReceivedEvent(
+				localAuthor.getId()));
+		if (m.getAuthor() == null) throw new FormatException();
+		// Create ephemeral key pair and get local transport properties
+		KeyPair keyPair = crypto.generateKeyPair();
+		byte[] publicKey = keyPair.getPublic().getEncoded();
+		byte[] privateKey = keyPair.getPrivate().getEncoded();
+		long localTimestamp = clock.currentTimeMillis();
+		try {
+			SecretKey secretKey = crypto.deriveMasterKey(publicKey, privateKey,
+					m.getEphemeralPublicKey(), false);
+			SecretKey mailboxMacKey = crypto.deriveMacKey(secretKey, true);
+			SecretKey introduceeMacKey = crypto.deriveMacKey(secretKey, false);
+			// Add the keys to the session
+			s = IntroduceeSession
+					.addLocalAccept(s, s.getState(), m, publicKey, privateKey,
+							localTimestamp, secretKey.getBytes(),
+							mailboxMacKey.getBytes(),
+							introduceeMacKey.getBytes());
+			byte[] mac =
+					crypto.authMac(introduceeMacKey, s, localAuthor.getId());
+			byte[] signature =
+					crypto.sign(introduceeMacKey, localAuthor.getPrivateKey());
+			// Send ephemeral public key, mac and accept timestamp back
+			Message reply =
+					sendIntroduceeResponseMessage(txn, s, m.getMessageId(),
+							localTimestamp, publicKey, mac, signature,
+							localTimestamp);
+			//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
+			// Move to the AWAIT_REMOTE_RESPONSE state
+			return IntroduceeSession.addLocalAuth(s, AWAIT_AUTH, reply);
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+	}
+
+	private boolean isInvalidDependency(AbstractIntroduceeSession s,
+			@Nullable MessageId dependency) {
+		return isInvalidDependency(s.getLastRemoteMessageId(), dependency);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeSession.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeSession.java
new file mode 100644
index 000000000..9ffd984ed
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeSession.java
@@ -0,0 +1,147 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.mailbox.Role;
+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 java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.bramble.mailbox.introduction.IntroduceeState.AWAIT_AUTH;
+import static org.briarproject.bramble.mailbox.introduction.IntroduceeState.START;
+
+@Immutable
+@NotNullByDefault
+class IntroduceeSession extends AbstractIntroduceeSession<IntroduceeState> {
+
+	IntroduceeSession(SessionId sessionId, IntroduceeState state,
+			long requestTimestamp, GroupId contactGroupId, Author introducer,
+			Local local, Remote remote, @Nullable byte[] masterKey,
+			@Nullable Map<TransportId, KeySetId> transportKeys,
+			long sessionCounter) {
+		super(sessionId, state, requestTimestamp, contactGroupId, introducer,
+				local, remote, masterKey, transportKeys, sessionCounter);
+	}
+
+	static IntroduceeSession getInitial(GroupId contactGroupId,
+			SessionId sessionId, Author introducer, boolean localIsAlice,
+			Author remoteAuthor) {
+		Local local = new Local(localIsAlice, null, -1, null, null, -1, null);
+		Remote remote =
+				new Remote(!localIsAlice, remoteAuthor, null, null, null, -1,
+						null);
+		return new IntroduceeSession(sessionId, START, -1, contactGroupId,
+				introducer, local, remote, null, null, 0);
+	}
+
+	static IntroduceeSession addLocalAccept(IntroduceeSession s,
+			IntroduceeState state, MailboxAcceptMessage m,
+			byte[] ephemeralPublicKey, byte[] ephemeralPrivateKey,
+			long acceptTimestamp, byte[] masterKey, byte[] aliceMackey,
+			byte[] bobMacKey) {
+		Local local = new Local(false, s.local.lastMessageId,
+				s.local.lastMessageTimestamp, ephemeralPublicKey,
+				ephemeralPrivateKey, acceptTimestamp, bobMacKey);
+		Remote remote = new Remote(true, m.getAuthor(), m.getMessageId(),
+				m.getEphemeralPublicKey(), null, m.getAcceptTimestamp(),
+				aliceMackey);
+		return new IntroduceeSession(s.getSessionId(), state, m.getTimestamp(),
+				s.contactGroupId, s.introducer, local, remote, masterKey,
+				s.transportKeys, s.getAbortCounter());
+	}
+
+	static IntroduceeSession addLocalAuth(IntroduceeSession s,
+			IntroduceeState state, Message m) {
+		// add mac key and sent message
+		Local local = new Local(false, m.getId(), m.getTimestamp(),
+				s.local.ephemeralPublicKey, s.local.ephemeralPrivateKey,
+				s.local.acceptTimestamp, s.local.macKey);
+
+		return new IntroduceeSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				s.remote, s.masterKey, s.transportKeys, s.getAbortCounter());
+	}
+
+	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_AUTH,
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, null, transportKeys, s.getAbortCounter());
+	}
+
+	static IntroduceeSession clear(IntroduceeSession s, IntroduceeState state,
+			@Nullable MessageId lastLocalMessageId, long localTimestamp,
+			@Nullable MessageId lastRemoteMessageId, long abortCounter) {
+		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, abortCounter);
+	}
+
+	@Override
+	Role getRole() {
+		return INTRODUCEE;
+	}
+
+	@Override
+	public GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	@Override
+	public long getLocalTimestamp() {
+		return local.lastMessageTimestamp;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastLocalMessageId() {
+		return local.lastMessageId;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return remote.lastMessageId;
+	}
+
+	Author getIntroducer() {
+		return introducer;
+	}
+
+	public Local getLocal() {
+		return local;
+	}
+
+	public Remote getRemote() {
+		return remote;
+	}
+
+	@Nullable
+	byte[] getMasterKey() {
+		return masterKey;
+	}
+
+	@Nullable
+	Map<TransportId, KeySetId> getTransportKeys() {
+		return transportKeys;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeState.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeState.java
new file mode 100644
index 000000000..e0bad4558
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroduceeState.java
@@ -0,0 +1,33 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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),
+	LOCAL_DECLINED(1),
+	AWAIT_AUTH(2),
+	MAILBOX_ADDED(3);
+
+	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/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroductionConstants.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroductionConstants.java
new file mode 100644
index 000000000..adc5223cd
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/IntroductionConstants.java
@@ -0,0 +1,49 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+interface IntroductionConstants {
+
+	// Group metadata keys
+	String GROUP_KEY_CONTACT_ID = "contactId";
+
+	// Message metadata keys
+	String MSG_KEY_MESSAGE_TYPE = "messageType";
+	String MSG_KEY_SESSION_ID = "sessionId";
+	String MSG_KEY_TIMESTAMP = "timestamp";
+	String MSG_KEY_LOCAL = "local";
+	String MSG_KEY_COUNTER = "counter";
+	String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
+
+	// Session Keys
+	String SESSION_KEY_SESSION_ID = "sessionId";
+	String SESSION_KEY_COUNTER = "counter";
+	String SESSION_KEY_ROLE = "role";
+	String SESSION_KEY_STATE = "state";
+	String SESSION_KEY_REQUEST_TIMESTAMP = "requestTimestamp";
+	String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp";
+	String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
+	String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId";
+
+	// Session Keys Introducer
+	String SESSION_KEY_INTRODUCEE_A = "introduceeA";
+	String SESSION_KEY_INTRODUCEE_B = "introduceeB";
+	String SESSION_KEY_GROUP_ID = "groupId";
+	String SESSION_KEY_AUTHOR = "author";
+
+	// Session Keys Introducee
+	String SESSION_KEY_INTRODUCER = "introducer";
+	String SESSION_KEY_LOCAL = "local";
+	String SESSION_KEY_REMOTE = "remote";
+
+	String SESSION_KEY_MASTER_KEY = "masterKey";
+	String SESSION_KEY_TRANSPORT_KEYS = "transportKeys";
+
+	String SESSION_KEY_ALICE = "alice";
+	String SESSION_KEY_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey";
+	String SESSION_KEY_EPHEMERAL_PRIVATE_KEY = "ephemeralPrivateKey";
+	String SESSION_KEY_TRANSPORT_PROPERTIES = "transportProperties";
+	String SESSION_KEY_ACCEPT_TIMESTAMP = "acceptTimestamp";
+	String SESSION_KEY_MAC_KEY = "macKey";
+
+	String SESSION_KEY_REMOTE_AUTHOR = "remoteAuthor";
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAcceptMessage.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAcceptMessage.java
new file mode 100644
index 000000000..bec6267bd
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAcceptMessage.java
@@ -0,0 +1,48 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MailboxAcceptMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final Author author;
+	private final byte[] ephemeralPublicKey;
+	private final long acceptTimestamp;
+
+	protected MailboxAcceptMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, Author author, byte[] ephemeralPublicKey,
+			long acceptTimestamp) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.author = author;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.acceptTimestamp = acceptTimestamp;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public Author getAuthor() {
+		return author;
+	}
+
+	public byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	public long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAuthMessage.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAuthMessage.java
new file mode 100644
index 000000000..8f7871be9
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxAuthMessage.java
@@ -0,0 +1,51 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MailboxAuthMessage extends AbstractMailboxIntroductionMessage {
+
+	private final SessionId sessionId;
+	private final Map<TransportId, TransportProperties> transportProperties;
+	private final byte[] mac;
+	private final byte[] signature;
+
+	protected MailboxAuthMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.sessionId = sessionId;
+		this.transportProperties = transportProperties;
+		this.mac = mac;
+		this.signature = signature;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public byte[] getMac() {
+		return mac;
+	}
+
+	public byte[] getSignature() {
+		return signature;
+	}
+
+	public Map<TransportId, TransportProperties> getTransportProperties() {
+		return transportProperties;
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCrypto.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCrypto.java
new file mode 100644
index 000000000..46f8736cd
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCrypto.java
@@ -0,0 +1,95 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 java.security.GeneralSecurityException;
+
+public interface MailboxIntroductionCrypto {
+
+	/**
+	 * Returns the {@link SessionId} based on the introducer
+	 * and the two introducees.
+	 */
+	SessionId getSessionId(Author introducer, Author local, Author remote,
+			boolean isAlice);
+
+	/**
+	 * Generates an agreement key pair.
+	 */
+	KeyPair generateKeyPair();
+
+	/**
+	 * Derives a session master key for Alice or Bob.
+	 *
+	 * @return The secret master key
+	 */
+	@SuppressWarnings("ConstantConditions")
+	SecretKey deriveMasterKey(byte[] ephemeralPublicKey,
+			byte[] ephemeralPrivateKey, byte[] remoteEphemeralPublicKey,
+			boolean alice) throws GeneralSecurityException;
+
+	/**
+	 * Derives a MAC key from the session's master key for Alice or Bob.
+	 *
+	 * @param masterKey The key returned by {@link #deriveMasterKey(byte[], byte[], byte[], boolean)}
+	 * @param alice true for Alice's MAC key, false for Bob's
+	 * @return The MAC key
+	 */
+	SecretKey deriveMacKey(SecretKey masterKey, boolean alice);
+
+	/**
+	 * Generates a MAC that covers both introducee's ephemeral public keys,
+	 * transport properties, Author IDs and timestamps of the accept message.
+	 */
+	byte[] authMac(SecretKey macKey, AbstractIntroduceeSession s,
+			AuthorId localAuthorId);
+
+	/**
+	 * Verifies a received MAC
+	 *
+	 * @param mac The MAC to verify
+	 * as returned by {@link #deriveMasterKey#deriveMasterKey(byte[], byte[], byte[], boolean)}
+	 * @throws GeneralSecurityException if the verification fails
+	 */
+	void verifyAuthMac(byte[] mac, AbstractIntroduceeSession s,
+			AuthorId localAuthorId) throws GeneralSecurityException;
+
+	/**
+	 * Signs a nonce derived from the macKey
+	 * with the local introducee's identity private key.
+	 *
+	 * @param macKey The corresponding MAC key for the signer's role
+	 * @param privateKey The identity private key
+	 * (from {@link LocalAuthor#getPrivateKey()})
+	 * @return The signature as a byte array
+	 */
+	byte[] sign(SecretKey macKey, byte[] privateKey)
+			throws GeneralSecurityException;
+
+	/**
+	 * Verifies the signature on a nonce derived from the MAC key.
+	 *
+	 * @throws GeneralSecurityException if the signature is invalid
+	 */
+	void verifySignature(byte[] signature, AbstractIntroduceeSession s)
+			throws GeneralSecurityException;
+
+	/**
+	 * Generates a MAC using the local MAC key.
+	 */
+	byte[] activateMac(AbstractIntroduceeSession s);
+
+	/**
+	 * Verifies a MAC from an ACTIVATE message.
+	 *
+	 * @throws GeneralSecurityException if the verification fails
+	 */
+	void verifyActivateMac(byte[] mac, AbstractIntroduceeSession s)
+			throws GeneralSecurityException;
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCryptoImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCryptoImpl.java
new file mode 100644
index 000000000..79d7f6d07
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionCryptoImpl.java
@@ -0,0 +1,190 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.SessionId;
+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 java.security.GeneralSecurityException;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_ACTIVATE_MAC;
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_ALICE_MAC_KEY;
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_AUTH_MAC;
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_AUTH_NONCE;
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_AUTH_SIGN;
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_BOB_MAC_KEY;
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_MASTER_KEY;
+import static org.briarproject.bramble.api.mailbox.IntroductionConstants.LABEL_SESSION_ID;
+import static org.briarproject.bramble.api.mailbox.MailboxIntroductionManager.MAJOR_VERSION;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Common;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Local;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Remote;
+
+@Immutable
+@NotNullByDefault
+class MailboxIntroductionCryptoImpl implements MailboxIntroductionCrypto {
+
+	private final CryptoComponent crypto;
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxIntroductionCryptoImpl(CryptoComponent crypto,
+			ClientHelper clientHelper) {
+		this.crypto = crypto;
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public SessionId getSessionId(Author owner, Author local, Author remote,
+			boolean isAlice) {
+		byte[] hash = crypto.hash(LABEL_SESSION_ID, owner.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 SecretKey deriveMasterKey(byte[] publicKey, byte[] privateKey,
+			byte[] remotePublicKey, boolean alice)
+			throws GeneralSecurityException {
+		KeyParser kp = crypto.getAgreementKeyParser();
+		PublicKey remoteEphemeralPublicKey = kp.parsePublicKey(remotePublicKey);
+		PublicKey ephemeralPublicKey = kp.parsePublicKey(publicKey);
+		PrivateKey ephemeralPrivateKey = kp.parsePrivateKey(privateKey);
+		KeyPair keyPair = new KeyPair(ephemeralPublicKey, ephemeralPrivateKey);
+		return crypto
+				.deriveSharedSecret(LABEL_MASTER_KEY, remoteEphemeralPublicKey,
+						keyPair, new byte[] {MAJOR_VERSION},
+						alice ? publicKey : remotePublicKey,
+						alice ? remotePublicKey : publicKey);
+	}
+
+	@Override
+	public SecretKey deriveMacKey(SecretKey masterKey, boolean alice) {
+		return crypto.deriveKey(alice ? LABEL_ALICE_MAC_KEY : LABEL_BOB_MAC_KEY,
+				masterKey);
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public byte[] authMac(SecretKey macKey, AbstractIntroduceeSession s,
+			AuthorId localAuthorId) {
+		// the macKey is not yet available in the session at this point
+		return authMac(macKey, s.getIntroducer().getId(), localAuthorId,
+				s.getLocal(), s.getRemote());
+	}
+
+	byte[] authMac(SecretKey macKey, AuthorId introducerId,
+			AuthorId localAuthorId, Local local, Remote remote) {
+		byte[] inputs = getAuthMacInputs(introducerId, localAuthorId, local,
+				remote.author.getId(), remote);
+		return crypto.mac(LABEL_AUTH_MAC, macKey, inputs);
+	}
+
+	@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
+	@SuppressWarnings("ConstantConditions")
+	public void verifyAuthMac(byte[] mac, AbstractIntroduceeSession s,
+			AuthorId localAuthorId) throws GeneralSecurityException {
+		verifyAuthMac(mac, new SecretKey(s.getRemote().macKey),
+				s.getIntroducer().getId(), localAuthorId, s.getLocal(),
+				s.getRemote().author.getId(), s.getRemote());
+	}
+
+	void verifyAuthMac(byte[] mac, SecretKey macKey, AuthorId introducerId,
+			AuthorId localAuthorId, Common local, AuthorId remoteAuthorId,
+			Common remote) throws GeneralSecurityException {
+		// switch input for verification
+		byte[] inputs = getAuthMacInputs(introducerId, remoteAuthorId, remote,
+				localAuthorId, local);
+		if (!crypto.verifyMac(mac, LABEL_AUTH_MAC, macKey, inputs)) {
+			throw new GeneralSecurityException();
+		}
+	}
+
+	@Override
+	public byte[] sign(SecretKey macKey, byte[] privateKey)
+			throws GeneralSecurityException {
+		return crypto.sign(LABEL_AUTH_SIGN, getNonce(macKey), privateKey);
+	}
+
+	private byte[] getNonce(SecretKey macKey) {
+		return crypto.mac(LABEL_AUTH_NONCE, macKey);
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public void verifySignature(byte[] signature, AbstractIntroduceeSession s)
+			throws GeneralSecurityException {
+		SecretKey macKey = new SecretKey(s.getRemote().macKey);
+		verifySignature(macKey, s.getRemote().author.getPublicKey(), signature);
+	}
+
+	void verifySignature(SecretKey macKey, byte[] publicKey, byte[] signature)
+			throws GeneralSecurityException {
+		byte[] nonce = getNonce(macKey);
+		if (!crypto.verifySignature(signature, LABEL_AUTH_SIGN, nonce,
+				publicKey)) {
+			throw new GeneralSecurityException();
+		}
+	}
+
+	@Override
+	public byte[] activateMac(AbstractIntroduceeSession s) {
+		if (s.getLocal().macKey == null)
+			throw new AssertionError("Local MAC key is null");
+		return activateMac(new SecretKey(s.getLocal().macKey));
+	}
+
+	byte[] activateMac(SecretKey macKey) {
+		return crypto.mac(LABEL_ACTIVATE_MAC, macKey);
+	}
+
+	@Override
+	public void verifyActivateMac(byte[] mac, AbstractIntroduceeSession s)
+			throws GeneralSecurityException {
+		if (s.getRemote().macKey == null)
+			throw new AssertionError("Remote MAC key is null");
+		verifyActivateMac(mac, new SecretKey(s.getRemote().macKey));
+	}
+
+	void verifyActivateMac(byte[] mac, SecretKey macKey)
+			throws GeneralSecurityException {
+		if (!crypto.verifyMac(mac, LABEL_ACTIVATE_MAC, macKey)) {
+			throw new GeneralSecurityException();
+		}
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionManagerImpl.java
new file mode 100644
index 000000000..c10a7bfce
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionManagerImpl.java
@@ -0,0 +1,486 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.BdfIncomingMessageHook;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.contact.ContactType;
+import org.briarproject.bramble.api.contact.PrivateMailbox;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.data.MetadataParser;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.mailbox.MailboxIntroductionManager;
+import org.briarproject.bramble.api.mailbox.Role;
+import org.briarproject.bramble.api.sync.Client;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.contact.ContactManager.ContactHook;
+import static org.briarproject.bramble.api.contact.ContactType.values;
+import static org.briarproject.bramble.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.bramble.api.mailbox.Role.MAILBOX;
+import static org.briarproject.bramble.api.mailbox.Role.OWNER;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.GROUP_KEY_CONTACT_ID;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_ACCEPT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_REQUEST;
+
+class MailboxIntroductionManagerImpl extends BdfIncomingMessageHook
+		implements MailboxIntroductionManager, Client, ClientVersioningHook,
+		ContactHook {
+
+	private static final Logger LOG =
+			Logger.getLogger(MailboxIntroductionManagerImpl.class.getName());
+	private final Executor ioExecutor;
+	private final ClientVersioningManager clientVersioningManager;
+	private final ContactGroupFactory contactGroupFactory;
+	private final ContactManager contactManager;
+	private final MailboxMessageParser messageParser;
+	private final MailboxSessionEncoder sessionEncoder;
+	private final MailboxSessionParser sessionParser;
+	private final OwnerProtocolEngine ownerProtocolEngine;
+	private final IntroduceeProtocolEngine introduceeProtocolEngine;
+	private final MailboxIntroductionCrypto crypto;
+	private final MailboxProtocolEngine mailboxProtocolEngine;
+	private final IdentityManager identityManager;
+	private final Clock clock;
+	private final Group localGroup;
+
+	@Inject
+	MailboxIntroductionManagerImpl(@IoExecutor Executor ioExecutor,
+			DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
+			MetadataParser metadataParser,
+			ContactGroupFactory contactGroupFactory,
+			ContactManager contactManager, MailboxMessageParser messageParser,
+			MailboxSessionParser sessionParser,
+			OwnerProtocolEngine ownerProtocolEngine,
+			IntroduceeProtocolEngine introduceeProtocolEngine,
+			MailboxSessionEncoder sessionEncoder,
+			MailboxIntroductionCrypto crypto,
+			MailboxProtocolEngine mailboxProtocolEngine,
+			IdentityManager identityManager, Clock clock) {
+		super(db, clientHelper, metadataParser);
+		this.ioExecutor = ioExecutor;
+		this.clientVersioningManager = clientVersioningManager;
+		this.contactGroupFactory = contactGroupFactory;
+		this.contactManager = contactManager;
+		this.messageParser = messageParser;
+		this.sessionParser = sessionParser;
+		this.ownerProtocolEngine = ownerProtocolEngine;
+		this.introduceeProtocolEngine = introduceeProtocolEngine;
+		this.sessionEncoder = sessionEncoder;
+		this.crypto = crypto;
+		this.mailboxProtocolEngine = mailboxProtocolEngine;
+		this.identityManager = identityManager;
+		this.localGroup =
+				contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
+		this.clock = clock;
+	}
+
+	@Override
+	public void createLocalState(Transaction txn) throws DbException {
+		// Create a local group to store protocol sessions
+		if (db.containsGroup(txn, localGroup.getId())) return;
+		db.addGroup(txn, localGroup);
+		// Set up groups for communication with any pre-existing contacts
+		for (Contact c : db.getAllContacts(txn)) addingContact(txn, c);
+	}
+
+	@Override
+	public Collection<ContactType> getApplicableContactTypes() {
+		return Arrays.asList(values());
+	}
+
+	@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:
+				setupNewContact(txn, c);
+		}
+	}
+
+	private void setupNewContact(Transaction txn, Contact c)
+			throws DbException {
+		// Create a group to share with the contact
+		Group g = getContactGroup(c);
+		db.addGroup(txn, g);
+		// Apply the client's visibility to the contact group
+		Group.Visibility client = clientVersioningManager
+				.getClientVisibility(txn, c.getId(), CLIENT_ID, MAJOR_VERSION);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
+		// Attach the contact ID to the group
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
+		try {
+			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	@Override
+	public void contactAdded(Transaction txn, Contact c) throws DbException {
+		setupNewContact(txn, c);
+		PrivateMailbox privateMailbox = contactManager.getPrivateMailbox(txn);
+		if (privateMailbox == null) return;
+		ioExecutor.execute(() -> {
+			try {
+				makeIntroduction(privateMailbox, c, clock.currentTimeMillis());
+			} catch (DbException e) {
+				LOG.warning("Mailbox introduction failed: " + e.toString());
+			}
+		});
+	}
+
+	@Override
+	public void privateMailboxAdded(Transaction txn,
+			PrivateMailbox privateMailbox) throws DbException {
+		setupNewContact(txn, privateMailbox);
+		ioExecutor.execute(() -> {
+			try {
+				for (Contact c : db.getContacts(txn)) {
+					makeIntroduction(privateMailbox, c,
+							clock.currentTimeMillis());
+				}
+			} catch (DbException e) {
+				LOG.warning("Mailbox introduction failed: " + e.toString());
+			}
+		});
+	}
+
+	@Override
+	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
+			BdfDictionary bdfMeta) throws DbException, FormatException {
+		MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
+		// Look up the session, if there is one
+		SessionId sessionId = meta.getSessionId();
+		Session session = null;
+		if (sessionId == null) {
+			if (meta.getMessageType() != MAILBOX_REQUEST)
+				throw new AssertionError();
+			session = createMailboxSession(txn, m, body);
+			sessionId = session.getSessionId();
+		}
+		StoredSession ss = getSession(txn, sessionId);
+		// Handle the message
+		MessageId storageId;
+		if (ss == null) {
+			ProtocolEngine engine;
+			if (meta.getMessageType() == MAILBOX_REQUEST)
+				engine = mailboxProtocolEngine;
+			else if (meta.getMessageType() == MAILBOX_ACCEPT) {
+				session = createIntroduceeSession(txn, m, body);
+				engine = introduceeProtocolEngine;
+			} else throw new FormatException();
+			if (session == null) throw new AssertionError();
+			session = handleMessage(txn, m, body, meta, session, engine);
+			storageId = createStorageId(txn);
+		} else {
+			storageId = ss.storageId;
+			Role role = sessionParser.getRole(ss.bdfSession);
+			switch (role) {
+				case OWNER:
+					session = handleMessage(txn, m, body, meta,
+							sessionParser.parseOwnerSession(ss.bdfSession),
+							ownerProtocolEngine);
+					break;
+				case MAILBOX:
+					session = handleMessage(txn, m, body, meta, sessionParser
+									.parseMailboxSession(m.getGroupId(), ss.bdfSession),
+							mailboxProtocolEngine);
+					break;
+				case INTRODUCEE:
+					session = handleMessage(txn, m, body, meta, sessionParser
+							.parseIntroduceeSession(m.getGroupId(),
+									ss.bdfSession), introduceeProtocolEngine);
+					break;
+				default:
+					throw new AssertionError();
+			}
+		}
+		// Store the updated session
+		storeSession(txn, storageId, session);
+		return false;
+	}
+
+	private MailboxSession createMailboxSession(Transaction txn, Message m,
+			BdfList body) throws DbException, FormatException {
+		ContactId ownerId = getContactId(txn, m.getGroupId());
+		Author owner = db.getContact(txn, ownerId).getAuthor();
+		Author local = identityManager.getLocalAuthor(txn);
+		Author remote = messageParser.parseRequestMessage(m, body).getAuthor();
+		if (local.equals(remote)) throw new FormatException();
+		SessionId sessionId = crypto.getSessionId(owner, local, remote, true);
+		return MailboxSession
+				.getInitial(m.getGroupId(), sessionId, owner, true, remote);
+	}
+
+	private IntroduceeSession createIntroduceeSession(Transaction txn,
+			Message m, BdfList body) throws DbException, FormatException {
+		ContactId ownerId = getContactId(txn, m.getGroupId());
+		Author owner = db.getContact(txn, ownerId).getAuthor();
+		Author local = identityManager.getLocalAuthor(txn);
+		Author remote =
+				messageParser.parseMailboxAcceptMessage(m, body).getAuthor();
+		if (local.equals(remote)) throw new FormatException();
+		SessionId sessionId = crypto.getSessionId(owner, local, remote, false);
+		return IntroduceeSession
+				.getInitial(m.getGroupId(), sessionId, owner, false, remote);
+	}
+
+	private <S extends Session> S handleMessage(Transaction txn, Message m,
+			BdfList body, MessageMetadata meta, S session,
+			ProtocolEngine<S> engine) throws DbException, FormatException {
+		if (meta.getCounter() < session.getAbortCounter()) {
+			LOG.warning("Ignoring old client message");
+			throw new FormatException();
+		}
+		switch (meta.getMessageType()) {
+			case MAILBOX_REQUEST: {
+				RequestMessage request =
+						messageParser.parseRequestMessage(m, body);
+				return engine.onRequestMessage(txn, session, request);
+			}
+			case DECLINE:
+				DeclineMessage declineMessage =
+						messageParser.parseDeclineMessage(m, body);
+				return engine.onDeclineMessage(txn, session, declineMessage);
+			case MAILBOX_ACCEPT: {
+				MailboxAcceptMessage acceptMessage =
+						messageParser.parseMailboxAcceptMessage(m, body);
+				return engine
+						.onMailboxAcceptMessage(txn, session, acceptMessage);
+			}
+			case INTRODUCEE_ACCEPT: {
+				IntroduceeAcceptMessage acceptMessage =
+						messageParser.parseIntroduceeAcceptMessage(m, body);
+				return engine
+						.onIntroduceeAcceptMessage(txn, session, acceptMessage);
+			}
+			case MAILBOX_AUTH:
+				MailboxAuthMessage authMessage =
+						messageParser.parseMailboxAuthMessage(m, body);
+				return engine.onAuthMessage(txn, session, authMessage);
+			case ABORT: {
+				AbortMessage abort = messageParser.parseAbortMessage(m, body);
+				return engine.onAbortMessage(txn, session, abort);
+			}
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Nullable
+	private StoredSession getSession(Transaction txn,
+			@Nullable SessionId sessionId) throws DbException, FormatException {
+		if (sessionId == null) return null;
+		BdfDictionary query = sessionParser.getSessionQuery(sessionId);
+		Map<MessageId, BdfDictionary> results = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId(), query);
+		if (results.size() > 1) throw new DbException();
+		if (results.isEmpty()) return null;
+		return new StoredSession(results.keySet().iterator().next(),
+				results.values().iterator().next());
+	}
+
+	@Override
+	public void makeIntroduction(PrivateMailbox privateMailbox, Contact contact,
+			long timestamp) throws DbException {
+		Transaction txn = db.startTransaction(false);
+		try {
+			// Look up the session, if there is one
+			Author introducer = identityManager.getLocalAuthor(txn);
+			SessionId sessionId =
+					crypto.getSessionId(introducer, privateMailbox.getAuthor(),
+							contact.getAuthor(), true);
+			StoredSession ss = getSession(txn, sessionId);
+			// Create or parse the session
+			OwnerSession session;
+			MessageId storageId;
+			if (ss == null) {
+				// This is the first request - create a new session
+				GroupId groupId1 = getContactGroup(privateMailbox).getId();
+				GroupId groupId2 = getContactGroup(contact).getId();
+				// use fixed deterministic roles for the introducees
+				session = new OwnerSession(sessionId, groupId1,
+						privateMailbox.getAuthor(), groupId2,
+						contact.getAuthor(), 0);
+				storageId = createStorageId(txn);
+			} else {
+				// An earlier request exists, so we already have a session
+				session = sessionParser.parseOwnerSession(ss.bdfSession);
+				storageId = ss.storageId;
+			}
+			// Handle the request action
+			session = ownerProtocolEngine
+					.onStartStartIntroduction(txn, session, timestamp);
+			// Store the updated session
+			storeSession(txn, storageId, session);
+			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	private void storeSession(Transaction txn, MessageId storageId,
+			Session session) throws DbException {
+		BdfDictionary d;
+		if (session.getRole() == OWNER) {
+			d = sessionEncoder.encodeIntroducerSession((OwnerSession) session);
+		} else if (session.getRole() == MAILBOX ||
+				session.getRole() == INTRODUCEE) {
+			d = sessionEncoder.encodeIntroduceeSession(
+					(AbstractIntroduceeSession) session);
+		} else {
+			throw new AssertionError();
+		}
+		try {
+			clientHelper.mergeMessageMetadata(txn, storageId, d);
+		} catch (FormatException e) {
+			throw new AssertionError();
+		}
+	}
+
+	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
+			throws DbException, FormatException {
+		BdfDictionary meta =
+				clientHelper.getGroupMetadataAsDictionary(txn, contactGroupId);
+		return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
+	}
+
+	@Override
+	public Group getContactGroup(Contact c) {
+		return contactGroupFactory
+				.createContactGroup(CLIENT_ID, MAJOR_VERSION, c);
+	}
+
+	private MessageId createStorageId(Transaction txn) throws DbException {
+		Message m = clientHelper
+				.createMessageForStoringMetadata(localGroup.getId());
+		db.addLocalMessage(txn, m, new Metadata(), false);
+		return m.getId();
+	}
+
+	@Override
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		removeSessionWithIntroducer(txn, c);
+		abortOrRemoveSessionWithIntroducee(txn, c);
+
+		// Remove the contact group (all messages will be removed with it)
+		db.removeGroup(txn, getContactGroup(c));
+	}
+
+	private void removeSessionWithIntroducer(Transaction txn,
+			Contact introducer) throws DbException {
+		BdfDictionary query = sessionEncoder
+				.getIntroduceeSessionsByIntroducerQuery(introducer.getAuthor());
+		Map<MessageId, BdfDictionary> sessions;
+		try {
+			sessions = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId(),
+							query);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+		for (MessageId id : sessions.keySet()) {
+			db.removeMessage(txn, id);
+		}
+	}
+
+	private void abortOrRemoveSessionWithIntroducee(Transaction txn, Contact c)
+			throws DbException {
+		BdfDictionary query = sessionEncoder.getIntroducerSessionsQuery();
+		Map<MessageId, BdfDictionary> sessions;
+		try {
+			sessions = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId(),
+							query);
+		} catch (FormatException e) {
+			throw new DbException();
+		}
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		for (Map.Entry<MessageId, BdfDictionary> session : sessions
+				.entrySet()) {
+			OwnerSession s;
+			try {
+				s = sessionParser.parseOwnerSession(session.getValue());
+			} catch (FormatException e) {
+				throw new DbException();
+			}
+			if (s.getMailbox().author.equals(c.getAuthor())) {
+				abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
+						s.getIntroducee(), localAuthor);
+			} else if (s.getIntroducee().author.equals(c.getAuthor())) {
+				abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
+						s.getMailbox(), localAuthor);
+			}
+		}
+	}
+
+	private void abortOrRemoveSessionWithIntroducee(Transaction txn,
+			OwnerSession s, MessageId storageId, Introducee i,
+			LocalAuthor localAuthor) throws DbException {
+		if (db.containsContact(txn, i.author.getId(), localAuthor.getId())) {
+			OwnerSession session =
+					ownerProtocolEngine.onIntroduceeRemoved(txn, i, s);
+			storeSession(txn, storageId, session);
+		} else {
+			db.removeMessage(txn, storageId);
+		}
+	}
+
+
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Group.Visibility v) throws DbException {
+		if (!getApplicableContactTypes().contains(c.getType())) return;
+		// Apply the client's visibility to the contact group
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
+	private static class StoredSession {
+
+		private final MessageId storageId;
+		private final BdfDictionary bdfSession;
+
+		private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
+			this.storageId = storageId;
+			this.bdfSession = bdfSession;
+		}
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionModule.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionModule.java
new file mode 100644
index 000000000..409a0149c
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionModule.java
@@ -0,0 +1,95 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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.mailbox.MailboxIntroductionManager;
+import org.briarproject.bramble.api.sync.ValidationManager;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static org.briarproject.bramble.api.mailbox.MailboxIntroductionManager.CLIENT_ID;
+import static org.briarproject.bramble.api.mailbox.MailboxIntroductionManager.MAJOR_VERSION;
+import static org.briarproject.bramble.api.mailbox.MailboxIntroductionManager.MINOR_VERSION;
+
+@Module
+public class MailboxIntroductionModule {
+
+	public static class EagerSingletons {
+		@Inject
+		MailboxIntroductionValidator mailboxIntroductionValidator;
+		@Inject
+		MailboxIntroductionManager mailboxIntroductionManager;
+	}
+
+
+	@Provides
+	@Singleton
+	MailboxIntroductionValidator provideMailboxValidator(
+			ValidationManager validationManager,
+			MailboxMessageEncoder messageEncoder,
+			MetadataEncoder metadataEncoder, ClientHelper clientHelper,
+			Clock clock) {
+		MailboxIntroductionValidator mailboxIntroductionValidator =
+				new MailboxIntroductionValidator(messageEncoder, clientHelper,
+						metadataEncoder, clock);
+		validationManager.registerMessageValidator(CLIENT_ID,
+				MailboxIntroductionManager.MAJOR_VERSION,
+				mailboxIntroductionValidator);
+		return mailboxIntroductionValidator;
+	}
+
+	@Provides
+	@Singleton
+	MailboxIntroductionManager provideMailboxIntroductionManager(
+			LifecycleManager lifecycleManager, ContactManager contactManager,
+			ValidationManager validationManager,
+			ClientVersioningManager clientVersioningManager,
+			MailboxIntroductionManagerImpl mailboxIntroductionManager) {
+		lifecycleManager.registerClient(mailboxIntroductionManager);
+		contactManager.registerContactHook(mailboxIntroductionManager);
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
+				mailboxIntroductionManager);
+		clientVersioningManager
+				.registerClient(CLIENT_ID, MAJOR_VERSION, MINOR_VERSION,
+						mailboxIntroductionManager);
+		return mailboxIntroductionManager;
+	}
+
+	@Provides
+	MailboxMessageParser provideMailboxMessageParser(
+			MailboxMessageParserImpl messageParser) {
+		return messageParser;
+	}
+
+	@Provides
+	MailboxMessageEncoder provideMailboxMessageEncoder(
+			MailboxMessageEncoderImpl messageEncoder) {
+		return messageEncoder;
+	}
+
+	@Provides
+	MailboxSessionParser provideMailboxSessionParser(
+			MailboxSessionParserImpl sessionParser) {
+		return sessionParser;
+	}
+
+	@Provides
+	MailboxSessionEncoder provideMailboxSessionEncoder(
+			MailboxSessionEncoderImpl sessionEncoder) {
+		return sessionEncoder;
+	}
+
+	@Provides
+	MailboxIntroductionCrypto provideMailboxIntroductionCrypto(
+			MailboxIntroductionCryptoImpl mailboxIntroductionCrypto) {
+		return mailboxIntroductionCrypto;
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionValidator.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionValidator.java
new file mode 100644
index 000000000..f6a2e2362
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionValidator.java
@@ -0,0 +1,216 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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.client.SessionId;
+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 java.util.Collections;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_ACCEPT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_AUTH;
+import static org.briarproject.bramble.util.ValidationUtils.checkLength;
+import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+
+
+@Immutable
+@NotNullByDefault
+class MailboxIntroductionValidator extends BdfMessageValidator {
+
+	private final MailboxMessageEncoder messageEncoder;
+
+	MailboxIntroductionValidator(MailboxMessageEncoder messageEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
+		this.messageEncoder = messageEncoder;
+	}
+
+	@Override
+	protected BdfMessageContext validateMessage(Message m, Group g,
+			BdfList body) throws FormatException {
+		MessageType type = MessageType.fromValue(body.getLong(0).intValue());
+
+		switch (type) {
+			case MAILBOX_REQUEST:
+				return validateRequestMessage(m, body);
+			case MAILBOX_ACCEPT:
+				return validateMailboxAcceptMessage(m, body);
+			case MAILBOX_AUTH:
+				return validateAuthMessage(m, body);
+			case INTRODUCEE_ACCEPT:
+				return validateIntroduceeAcceptMessage(m, body);
+			case DECLINE:
+			case ABORT:
+				return validateOtherMessage(type, m, body);
+			default:
+				throw new FormatException();
+		}
+	}
+
+	private BdfMessageContext validateRequestMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 4);
+		byte[] previousMessageId = body.getOptionalRaw(1);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+		BdfList authorList = body.getList(2);
+		if (authorList != null) clientHelper.parseAndValidateAuthor(authorList);
+		long messageCounter = body.getLong(3);
+		BdfDictionary meta = messageEncoder
+				.encodeRequestMetadata(m.getTimestamp(), messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateMailboxAcceptMessage(Message m,
+			BdfList body) throws FormatException {
+		checkSize(body, 7);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		Object author = body.get(3);
+		if (author instanceof BdfList)
+			clientHelper.parseAndValidateAuthor((BdfList) author);
+
+		byte[] ephemeralPublicKey = body.getRaw(4);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		long timestamp = body.getLong(5);
+		if (timestamp < 0) throw new FormatException();
+
+		long messageCounter = body.getLong(6);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(MAILBOX_ACCEPT, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateIntroduceeAcceptMessage(Message m,
+			BdfList body) throws FormatException {
+		checkSize(body, 8);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		byte[] mac = body.getRaw(4);
+		checkLength(mac, MAC_BYTES);
+
+		byte[] signature = body.getRaw(5);
+		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
+
+		long timestamp = body.getLong(6);
+		if (timestamp < 0) throw new FormatException();
+
+		long messageCounter = body.getLong(7);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(INTRODUCEE_ACCEPT, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	private BdfMessageContext validateAuthMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 7);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		BdfDictionary transportProperties = body.getDictionary(3);
+		if (transportProperties.size() < 1) throw new FormatException();
+		clientHelper
+				.parseAndValidateTransportPropertiesMap(transportProperties);
+
+		byte[] mac = body.getRaw(4);
+		checkLength(mac, MAC_BYTES);
+
+		byte[] signature = body.getRaw(5);
+		checkLength(signature, 1, MAX_SIGNATURE_BYTES);
+
+		long messageCounter = body.getLong(6);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(MAILBOX_AUTH, sessionId, m.getTimestamp(),
+						false, messageCounter);
+		MessageId dependency = new MessageId(previousMessageId);
+		return new BdfMessageContext(meta,
+				Collections.singletonList(dependency));
+	}
+
+	private BdfMessageContext validateOtherMessage(MessageType type, Message m,
+			BdfList body) throws FormatException {
+		checkSize(body, 4);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
+
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		long messageCounter = body.getLong(3);
+
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, sessionId, m.getTimestamp(), false,
+						messageCounter);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoder.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoder.java
new file mode 100644
index 000000000..f1ead80fa
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoder.java
@@ -0,0 +1,57 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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 java.util.Map;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+public interface MailboxMessageEncoder {
+
+	BdfDictionary encodeRequestMetadata(long timestamp, long messageCounter);
+
+	BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local,
+			long messsageCounter);
+
+	void addSessionId(BdfDictionary meta, SessionId sessionId);
+
+	void setAvailableToAnswer(BdfDictionary meta, boolean available);
+
+	Message encodeRequestMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, Author introduceeAuthor,
+			long messageCounter);
+
+	Message encodeIntroduceeAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey, byte[] mac,
+			byte[] signature, long acceptTimestamp, long messageCounter);
+
+	Message encodeMailboxAcceptMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			MessageType type, Author author, byte[] ephemeralPublicKey,
+			long acceptTimestamp, long messageCounter);
+
+	Message encodeMailboxAuthMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature, long messageCounter);
+
+	Message encodeDeclineMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			long abortCounter);
+
+	Message encodeAbortMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			long abortCounter);
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoderImpl.java
new file mode 100644
index 000000000..8e438ada9
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageEncoderImpl.java
@@ -0,0 +1,160 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.SessionId;
+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 java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_COUNTER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_LOCAL;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_SESSION_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.ABORT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.DECLINE;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.INTRODUCEE_ACCEPT;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_AUTH;
+import static org.briarproject.bramble.mailbox.introduction.MessageType.MAILBOX_REQUEST;
+
+@NotNullByDefault
+class MailboxMessageEncoderImpl implements MailboxMessageEncoder {
+
+	private final ClientHelper clientHelper;
+	private final MessageFactory messageFactory;
+
+	@Inject
+	MailboxMessageEncoderImpl(ClientHelper clientHelper,
+			MessageFactory messageFactory) {
+		this.clientHelper = clientHelper;
+		this.messageFactory = messageFactory;
+	}
+
+	@Override
+	public BdfDictionary encodeRequestMetadata(long timestamp,
+			long messageCounter) {
+		BdfDictionary meta =
+				encodeMetadata(MAILBOX_REQUEST, null, timestamp, false,
+						messageCounter);
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return meta;
+	}
+
+	@Override
+	public BdfDictionary encodeMetadata(MessageType type,
+			@Nullable SessionId sessionId, long timestamp, boolean local,
+			long messageCounter) {
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue());
+		if (sessionId != null) meta.put(MSG_KEY_SESSION_ID, sessionId);
+		else if (type != MAILBOX_REQUEST) throw new IllegalArgumentException();
+		meta.put(MSG_KEY_TIMESTAMP, timestamp);
+		meta.put(MSG_KEY_LOCAL, local);
+		meta.put(MSG_KEY_COUNTER, messageCounter);
+		return meta;
+	}
+
+	@Override
+	public void addSessionId(BdfDictionary meta, SessionId sessionId) {
+		meta.put(MSG_KEY_SESSION_ID, sessionId);
+	}
+
+	@Override
+	public void setAvailableToAnswer(BdfDictionary meta, boolean available) {
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
+	}
+
+	@Override
+	public Message encodeRequestMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, Author introduceeAuthor,
+			long messageCounter) {
+		BdfList body = BdfList.of(MAILBOX_REQUEST.getValue(), previousMessageId,
+				clientHelper.toList(introduceeAuthor), messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeMailboxAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, MessageType type, Author author,
+			byte[] ephemeralPublicKey, long acceptTimestamp,
+			long messageCounter) {
+		BdfList body = BdfList.of(type.getValue(), sessionId, previousMessageId,
+				author == null ? null : clientHelper.toList(author),
+				ephemeralPublicKey, acceptTimestamp, messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeIntroduceeAcceptMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId, byte[] ephemeralPublicKey, byte[] mac,
+			byte[] signature, long acceptTimestamp, long messageCounter) {
+		BdfList body = BdfList.of(INTRODUCEE_ACCEPT.getValue(), sessionId,
+				previousMessageId, ephemeralPublicKey, mac, signature,
+				acceptTimestamp, messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeMailboxAuthMessage(GroupId contactGroupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			SessionId sessionId,
+			Map<TransportId, TransportProperties> transportProperties,
+			byte[] mac, byte[] signature, long messageCounter) {
+		BdfList body = BdfList.of(MAILBOX_AUTH.getValue(), sessionId,
+				previousMessageId,
+				clientHelper.toDictionary(transportProperties), mac, signature,
+				messageCounter);
+		return createMessage(contactGroupId, timestamp, body);
+	}
+
+	@Override
+	public Message encodeDeclineMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			long abortCounter) {
+		return encodeMessage(DECLINE, contactGroupId, sessionId, timestamp,
+				previousMessageId, abortCounter);
+	}
+
+	@Override
+	public Message encodeAbortMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, SessionId sessionId,
+			long abortCounter) {
+		return encodeMessage(ABORT, contactGroupId, sessionId, timestamp,
+				previousMessageId, abortCounter);
+	}
+
+	private Message encodeMessage(MessageType type, GroupId contactGroupId,
+			SessionId sessionId, long timestamp,
+			@Nullable MessageId previousMessageId, long abortCounter) {
+		BdfList body = BdfList.of(type.getValue(), sessionId, previousMessageId,
+				abortCounter);
+		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/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParser.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParser.java
new file mode 100644
index 000000000..8ca36bda9
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParser.java
@@ -0,0 +1,33 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.SessionId;
+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;
+
+@NotNullByDefault
+public interface MailboxMessageParser {
+
+	MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
+
+	RequestMessage parseRequestMessage(Message m, BdfList body)
+			throws FormatException;
+
+	MailboxAcceptMessage parseMailboxAcceptMessage(Message m, BdfList body)
+			throws FormatException;
+
+	IntroduceeAcceptMessage parseIntroduceeAcceptMessage(Message m,
+			BdfList body) throws FormatException;
+
+	DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException;
+
+	MailboxAuthMessage parseMailboxAuthMessage(Message m, BdfList body)
+			throws FormatException;
+
+	AbortMessage parseAbortMessage(Message m, BdfList body)
+			throws FormatException;
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParserImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParserImpl.java
new file mode 100644
index 000000000..778e76e82
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxMessageParserImpl.java
@@ -0,0 +1,134 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.SessionId;
+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.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_COUNTER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_LOCAL;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_SESSION_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.MSG_KEY_TIMESTAMP;
+
+@NotNullByDefault
+class MailboxMessageParserImpl implements MailboxMessageParser {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxMessageParserImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public MessageMetadata parseMetadata(BdfDictionary d)
+			throws FormatException {
+		MessageType type = MessageType
+				.fromValue(d.getLong(MSG_KEY_MESSAGE_TYPE).intValue());
+		byte[] sessionIdBytes = d.getOptionalRaw(MSG_KEY_SESSION_ID);
+		SessionId sessionId =
+				sessionIdBytes == null ? null : new SessionId(sessionIdBytes);
+		long timestamp = d.getLong(MSG_KEY_TIMESTAMP);
+		boolean local = d.getBoolean(MSG_KEY_LOCAL);
+		long counter = d.getLong(MSG_KEY_COUNTER);
+		boolean available = d.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return new MessageMetadata(type, sessionId, timestamp, local, counter,
+				available);
+	}
+
+	@Override
+	public RequestMessage parseRequestMessage(Message m, BdfList body)
+			throws FormatException {
+		byte[] previousMsgBytes = body.getOptionalRaw(1);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		Author author = clientHelper.parseAndValidateAuthor(body.getList(2));
+		return new RequestMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, author);
+	}
+
+	@Override
+	public MailboxAcceptMessage parseMailboxAcceptMessage(Message m,
+			BdfList body) throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		Author author = null;
+		Object authorList = body.get(3);
+		if (authorList instanceof BdfList)
+			author = clientHelper.parseAndValidateAuthor((BdfList) authorList);
+		byte[] ephemeralPublicKey = body.getRaw(4);
+		long acceptTimestamp = body.getLong(5);
+		return new MailboxAcceptMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId, author,
+				ephemeralPublicKey, acceptTimestamp);
+	}
+
+	@Override
+	public IntroduceeAcceptMessage parseIntroduceeAcceptMessage(Message m,
+			BdfList body) throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		byte[] mac = body.getRaw(4);
+		byte[] signature = body.getRaw(5);
+		long acceptTimestamp = body.getLong(6);
+		return new IntroduceeAcceptMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId,
+				ephemeralPublicKey, mac, signature, acceptTimestamp);
+	}
+
+	@Override
+	public DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		return new DeclineMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, sessionId);
+	}
+
+	@Override
+	public MailboxAuthMessage parseMailboxAuthMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getRaw(2);
+		MessageId previousMessageId = new MessageId(previousMsgBytes);
+		Map<TransportId, TransportProperties> transportProperties = clientHelper
+				.parseAndValidateTransportPropertiesMap(body.getDictionary(3));
+		byte[] mac = body.getRaw(4);
+		byte[] signature = body.getRaw(5);
+		return new MailboxAuthMessage(m.getId(), m.getGroupId(),
+				m.getTimestamp(), previousMessageId, sessionId,
+				transportProperties, mac, signature);
+	}
+
+	@Override
+	public AbortMessage parseAbortMessage(Message m, BdfList body)
+			throws FormatException {
+		SessionId sessionId = new SessionId(body.getRaw(1));
+		byte[] previousMsgBytes = body.getOptionalRaw(2);
+		MessageId previousMessageId = (previousMsgBytes == null ? null :
+				new MessageId(previousMsgBytes));
+		return new AbortMessage(m.getId(), m.getGroupId(), m.getTimestamp(),
+				previousMessageId, sessionId);
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxProtocolEngine.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxProtocolEngine.java
new file mode 100644
index 000000000..9f19e23ac
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxProtocolEngine.java
@@ -0,0 +1,263 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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.client.SessionId;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionAbortedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionSucceededEvent;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.bramble.api.transport.KeySetId;
+
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.mailbox.introduction.MailboxState.AWAIT_INTRODUCEE_RESPONSE;
+import static org.briarproject.bramble.mailbox.introduction.MailboxState.CONTACT_ADDED;
+import static org.briarproject.bramble.mailbox.introduction.MailboxState.INTRODUCEE_DECLINED;
+import static org.briarproject.bramble.mailbox.introduction.MailboxState.START;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+class MailboxProtocolEngine extends AbstractProtocolEngine<MailboxSession> {
+
+	private final static Logger LOG =
+			Logger.getLogger(MailboxProtocolEngine.class.getName());
+
+	@Inject
+	MailboxProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager, MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		super(db, clientHelper, contactManager, contactGroupFactory,
+				identityManager, messageParser, messageEncoder, clock, crypto,
+				keyManager, transportPropertyManager);
+	}
+
+	@Override
+	public MailboxSession onRequestMessage(Transaction txn, MailboxSession s,
+			RequestMessage m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case START:
+				return onRemoteRequest(txn, s, m);
+			case LOCAL_DECLINED:
+			case INTRODUCEE_DECLINED:
+			case AWAIT_INTRODUCEE_RESPONSE:
+			case CONTACT_ADDED:
+				return abort(txn, s);
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Override
+	public MailboxSession onMailboxAcceptMessage(Transaction txn,
+			MailboxSession session, MailboxAcceptMessage m)
+			throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public MailboxSession onIntroduceeAcceptMessage(Transaction txn,
+			MailboxSession s, IntroduceeAcceptMessage m) throws DbException {
+		switch (s.getState()) {
+			case AWAIT_INTRODUCEE_RESPONSE:
+				return handleIntroduceeAccept(txn, s, m);
+			case START:
+			case INTRODUCEE_DECLINED:
+			case CONTACT_ADDED:
+				return abort(txn, s);
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private MailboxSession handleIntroduceeAccept(Transaction txn,
+			MailboxSession s, IntroduceeAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		// Broadcast IntroductionRequestReceivedEvent
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		try {
+			SecretKey secretKey =
+					crypto.deriveMasterKey(s.local.ephemeralPublicKey,
+							s.local.ephemeralPrivateKey,
+							m.getEphemeralPublicKey(), true);
+			SecretKey aliceMacKey = crypto.deriveMacKey(secretKey, true);
+			SecretKey bobMacKey = crypto.deriveMacKey(secretKey, false);
+
+			s = MailboxSession.addIntroduceeAccept(s, m.getMessageId(),
+					m.getEphemeralPublicKey(), secretKey, aliceMacKey,
+					bobMacKey, m.getAcceptTimestamp());
+			crypto.verifyAuthMac(m.getMac(), s, localAuthor.getId());
+			crypto.verifySignature(m.getSignature(), s);
+			long timestamp = Math.min(s.getLocal().acceptTimestamp,
+					s.getRemote().acceptTimestamp);
+			if (timestamp == -1) throw new AssertionError();
+			byte[] mac = crypto.authMac(aliceMacKey, s, localAuthor.getId());
+			byte[] signature =
+					crypto.sign(aliceMacKey, localAuthor.getPrivateKey());
+
+			contactManager
+					.addContact(txn, s.getRemote().author, localAuthor.getId(),
+							false, true);
+			// Only add transport properties and keys when the contact was added
+			// This will be changed once we have a way to reset state for peers
+			// that were contacts already at some point in the past.
+			Contact c = contactManager
+					.getContact(txn, s.getRemote().author.getId(),
+							localAuthor.getId());
+			// add the keys to the new contact
+			//noinspection ConstantConditions
+			Map<TransportId, KeySetId> keys = keyManager
+					.addContact(txn, c.getId(), secretKey, timestamp,
+							s.getLocal().alice, true);
+			// add signed transport properties for the contact
+			//noinspection ConstantConditions
+			transportPropertyManager.addRemoteProperties(txn, c.getId(),
+					transportPropertyManager.getLocalAnonymizedProperties(txn));
+			// Broadcast MailboxIntroductionSucceededEvent, because contact got added
+			MailboxIntroductionSucceededEvent e =
+					new MailboxIntroductionSucceededEvent(c);
+			txn.attach(e);
+			Map<TransportId, TransportProperties> transportProperties =
+					transportPropertyManager.getLocalProperties(txn);
+			Message reply =
+					sendMailboxAuthMessage(txn, s, clock.currentTimeMillis(),
+							transportProperties, mac, signature);
+			LOG.info("Contact from owner added");
+			//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
+			return MailboxSession.clear(s, CONTACT_ADDED, reply.getId(),
+					s.getLocalTimestamp(), m.getMessageId(),
+					s.getAbortCounter() + 1);
+		} catch (GeneralSecurityException e) {
+			logException(LOG, WARNING, e);
+			return abort(txn, s);
+		}
+
+	}
+
+	private MailboxSession abort(Transaction txn, MailboxSession s)
+			throws DbException {
+		// Send an ABORT message
+		Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s));
+		// Broadcast abort event for testing
+		txn.attach(new MailboxIntroductionAbortedEvent(s.getSessionId()));
+		// Reset the session back to initial state
+		return MailboxSession.clear(s, START, sent.getId(), sent.getTimestamp(),
+				s.getLastRemoteMessageId(), s.getAbortCounter() + 1);
+	}
+
+	private long getLocalTimestamp(AbstractIntroduceeSession s) {
+		return getLocalTimestamp(s.getLocalTimestamp(),
+				s.getRequestTimestamp());
+	}
+
+	@Override
+	public MailboxSession onDeclineMessage(Transaction txn,
+			MailboxSession session, DeclineMessage m)
+			throws DbException, FormatException {
+		switch (session.getState()) {
+			case AWAIT_INTRODUCEE_RESPONSE:
+				break;
+			case START:
+			case LOCAL_DECLINED:
+			case INTRODUCEE_DECLINED:
+			case CONTACT_ADDED:
+				return abort(txn, session);
+			default:
+				throw new AssertionError();
+		}
+		return MailboxSession.clear(session, INTRODUCEE_DECLINED,
+				session.getLastLocalMessageId(), m.getTimestamp(),
+				m.getMessageId(), session.getAbortCounter());
+	}
+
+	@Override
+	public MailboxSession onAuthMessage(Transaction txn, MailboxSession session,
+			MailboxAuthMessage m) throws DbException, FormatException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public MailboxSession onAbortMessage(Transaction txn, MailboxSession s,
+			AbortMessage m) throws DbException, FormatException {
+		// Broadcast abort event for testing
+		txn.attach(new MailboxIntroductionAbortedEvent(s.getSessionId()));
+		// Reset the session back to initial state
+		return MailboxSession.clear(s, START, s.getLastLocalMessageId(),
+				s.getLocalTimestamp(), m.getMessageId(),
+				s.getAbortCounter() + 1);
+	}
+
+	private MailboxSession onRemoteRequest(Transaction txn, MailboxSession s,
+			RequestMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+
+		// Add SessionId to message metadata
+		addSessionId(txn, m.getMessageId(), s.getSessionId());
+
+		// Broadcast IntroductionRequestReceivedEvent
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		txn.attach(new MailboxIntroductionRequestReceivedEvent(
+				localAuthor.getId()));
+
+		// Create ephemeral key pair and get local transport properties
+		KeyPair keyPair = crypto.generateKeyPair();
+		byte[] publicKey = keyPair.getPublic().getEncoded();
+		byte[] privateKey = keyPair.getPrivate().getEncoded();
+		long localTimestamp = clock.currentTimeMillis();
+		// Send ephemeral public key and timestamp back
+		Message reply = sendMailboxAcceptMessage(txn, s, localTimestamp, null,
+				publicKey, localTimestamp);
+
+		//TODO: Check for reasons to decline and if any, move to LOCAL_DECLINE
+		// Move to the AWAIT_REMOTE_RESPONSE state
+		return MailboxSession
+				.addLocalAccept(s, AWAIT_INTRODUCEE_RESPONSE, reply, publicKey,
+						privateKey, localTimestamp);
+	}
+
+	private boolean isInvalidDependency(AbstractIntroduceeSession s,
+			@Nullable MessageId dependency) {
+		return isInvalidDependency(s.getLastRemoteMessageId(), dependency);
+	}
+
+	private void addSessionId(Transaction txn, MessageId m, SessionId sessionId)
+			throws DbException {
+		BdfDictionary meta = new BdfDictionary();
+		messageEncoder.addSessionId(meta, sessionId);
+		try {
+			clientHelper.mergeMessageMetadata(txn, m, meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSession.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSession.java
new file mode 100644
index 000000000..e9b7fe6e8
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSession.java
@@ -0,0 +1,136 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.mailbox.Role;
+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 java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.mailbox.Role.MAILBOX;
+import static org.briarproject.bramble.mailbox.introduction.MailboxState.START;
+
+@Immutable
+@NotNullByDefault
+class MailboxSession extends AbstractIntroduceeSession<MailboxState> {
+
+
+	MailboxSession(SessionId sessionId, MailboxState state,
+			long requestTimestamp, GroupId contactGroupId, Author introducer,
+			Local local, Remote remote, @Nullable byte[] masterKey,
+			@Nullable Map<TransportId, KeySetId> transportKeys,
+			long sessionCounter) {
+		super(sessionId, state, requestTimestamp, contactGroupId, introducer,
+				local, remote, masterKey, transportKeys, sessionCounter);
+	}
+
+	static MailboxSession getInitial(GroupId contactGroupId,
+			SessionId sessionId, Author introducer, boolean localIsAlice,
+			Author remoteAuthor) {
+		Local local = new Local(localIsAlice, null, -1, null, null, -1, null);
+		Remote remote =
+				new Remote(!localIsAlice, remoteAuthor, null, null, null, -1,
+						null);
+		return new MailboxSession(sessionId, START, -1, contactGroupId,
+				introducer, local, remote, null, null, 0);
+	}
+
+	static MailboxSession addLocalAccept(MailboxSession s, MailboxState state,
+			Message m, byte[] ephemeralPublicKey, byte[] ephemeralPrivateKey,
+			long acceptTimestamp) {
+		Local local = new Local(s.local.alice, m.getId(), m.getTimestamp(),
+				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp, null);
+		return new MailboxSession(s.getSessionId(), state, m.getTimestamp(),
+				s.contactGroupId, s.introducer, local, s.remote, s.masterKey,
+				s.transportKeys, s.getAbortCounter());
+	}
+
+	static MailboxSession clear(MailboxSession s, MailboxState state,
+			@Nullable MessageId lastLocalMessageId, long localTimestamp,
+			@Nullable MessageId lastRemoteMessageId, long abortCounter) {
+		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, abortCounter);
+	}
+
+	static MailboxSession addIntroduceeAccept(MailboxSession s,
+			MessageId lastMessageId, byte[] ephemeralPublicKey,
+			SecretKey masterKey, SecretKey aliceMacKey, SecretKey bobMacKey,
+			long acceptTimestamp) {
+		Local local = new Local(s.local.alice, s.local.lastMessageId,
+				s.local.lastMessageTimestamp, s.local.ephemeralPublicKey,
+				s.local.ephemeralPrivateKey, s.local.acceptTimestamp,
+				aliceMacKey.getBytes());
+		Remote remote = new Remote(false, s.remote.author, lastMessageId,
+				ephemeralPublicKey, null, acceptTimestamp,
+				bobMacKey.getBytes());
+		return new MailboxSession(s.getSessionId(), s.getState(),
+				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
+				remote, masterKey.getBytes(), null, s.getAbortCounter());
+	}
+
+	@Override
+	Role getRole() {
+		return MAILBOX;
+	}
+
+	@Override
+	public GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	@Override
+	public long getLocalTimestamp() {
+		return local.lastMessageTimestamp;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastLocalMessageId() {
+		return local.lastMessageId;
+	}
+
+	@Nullable
+	@Override
+	public MessageId getLastRemoteMessageId() {
+		return remote.lastMessageId;
+	}
+
+	Author getIntroducer() {
+		return introducer;
+	}
+
+	public Local getLocal() {
+		return local;
+	}
+
+	public Remote getRemote() {
+		return remote;
+	}
+
+	@Nullable
+	byte[] getMasterKey() {
+		return masterKey;
+	}
+
+	@Nullable
+	Map<TransportId, KeySetId> getTransportKeys() {
+		return transportKeys;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoder.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoder.java
new file mode 100644
index 000000000..22bd220e4
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoder.java
@@ -0,0 +1,18 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface MailboxSessionEncoder {
+
+	BdfDictionary getIntroduceeSessionsByIntroducerQuery(Author introducer);
+
+	BdfDictionary getIntroducerSessionsQuery();
+
+	BdfDictionary encodeIntroducerSession(OwnerSession s);
+
+	BdfDictionary encodeIntroduceeSession(AbstractIntroduceeSession s);
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoderImpl.java
new file mode 100644
index 000000000..cb87e76de
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionEncoderImpl.java
@@ -0,0 +1,155 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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.bramble.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.bramble.api.mailbox.Role.OWNER;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Common;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Local;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Remote;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_ALICE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_COUNTER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LOCAL;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_MAC_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_REMOTE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+
+@Immutable
+@NotNullByDefault
+class MailboxSessionEncoderImpl implements MailboxSessionEncoder {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxSessionEncoderImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getIntroduceeSessionsByIntroducerQuery(
+			Author introducer) {
+		return BdfDictionary
+				.of(new BdfEntry(SESSION_KEY_ROLE, INTRODUCEE.getValue()),
+						new BdfEntry(SESSION_KEY_INTRODUCER,
+								clientHelper.toList(introducer)));
+	}
+
+	@Override
+	public BdfDictionary getIntroducerSessionsQuery() {
+		return BdfDictionary
+				.of(new BdfEntry(SESSION_KEY_ROLE, OWNER.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));
+		d.put(SESSION_KEY_COUNTER, i.abortCounter);
+		return d;
+	}
+
+	@Override
+	public BdfDictionary encodeIntroduceeSession(AbstractIntroduceeSession s) {
+		BdfDictionary d = encodeSession(s);
+		d.put(SESSION_KEY_INTRODUCER, clientHelper.toList(s.getIntroducer()));
+		d.put(SESSION_KEY_LOCAL, encodeLocal(s.getLocal()));
+		d.put(SESSION_KEY_REMOTE, encodeRemote(s.getRemote()));
+		putNullable(d, SESSION_KEY_MASTER_KEY, s.getMasterKey());
+		putNullable(d, SESSION_KEY_TRANSPORT_KEYS,
+				encodeTransportKeys(s.getTransportKeys()));
+		return d;
+	}
+
+	private BdfDictionary encodeCommon(Common s) {
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_ALICE, s.alice);
+		putNullable(d, SESSION_KEY_EPHEMERAL_PUBLIC_KEY, s.ephemeralPublicKey);
+		d.put(SESSION_KEY_ACCEPT_TIMESTAMP, s.acceptTimestamp);
+		putNullable(d, SESSION_KEY_MAC_KEY, s.macKey);
+		return d;
+	}
+
+	private BdfDictionary encodeLocal(Local s) {
+		BdfDictionary d = encodeCommon(s);
+		d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.lastMessageTimestamp);
+		putNullable(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, s.lastMessageId);
+		putNullable(d, SESSION_KEY_EPHEMERAL_PRIVATE_KEY,
+				s.ephemeralPrivateKey);
+		return d;
+	}
+
+	private BdfDictionary encodeRemote(Remote s) {
+		BdfDictionary d = encodeCommon(s);
+		d.put(SESSION_KEY_REMOTE_AUTHOR, clientHelper.toList(s.author));
+		putNullable(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID, s.lastMessageId);
+		return d;
+	}
+
+	private BdfDictionary encodeSession(Session s) {
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_SESSION_ID, s.getSessionId());
+		d.put(SESSION_KEY_ROLE, s.getRole().getValue());
+		d.put(SESSION_KEY_STATE, s.getState().getValue());
+		d.put(SESSION_KEY_REQUEST_TIMESTAMP, s.getRequestTimestamp());
+		d.put(SESSION_KEY_COUNTER, s.getAbortCounter());
+		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/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParser.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParser.java
new file mode 100644
index 000000000..a77b3c601
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParser.java
@@ -0,0 +1,27 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.mailbox.Role;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+
+@NotNullByDefault
+public interface MailboxSessionParser {
+
+	BdfDictionary getSessionQuery(SessionId s);
+
+	Role getRole(BdfDictionary d) throws FormatException;
+
+	long getMessageCounter(BdfDictionary d) throws FormatException;
+
+	OwnerSession parseOwnerSession(BdfDictionary d) throws FormatException;
+
+	MailboxSession parseMailboxSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException;
+
+	IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException;
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParserImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParserImpl.java
new file mode 100644
index 000000000..a714fe929
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxSessionParserImpl.java
@@ -0,0 +1,231 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.SessionId;
+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.mailbox.Role;
+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 java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.mailbox.Role.INTRODUCEE;
+import static org.briarproject.bramble.api.mailbox.Role.MAILBOX;
+import static org.briarproject.bramble.api.mailbox.Role.OWNER;
+import static org.briarproject.bramble.api.mailbox.Role.fromValue;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Local;
+import static org.briarproject.bramble.mailbox.introduction.AbstractIntroduceeSession.Remote;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_ALICE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_COUNTER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LOCAL;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_MAC_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_REMOTE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+import static org.briarproject.bramble.mailbox.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
+
+@Immutable
+@NotNullByDefault
+class MailboxSessionParserImpl implements MailboxSessionParser {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MailboxSessionParserImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getSessionQuery(SessionId s) {
+		return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
+	}
+
+	@Override
+	public Role getRole(BdfDictionary d) throws FormatException {
+		return fromValue(d.getLong(SESSION_KEY_ROLE).intValue());
+	}
+
+	@Override
+	public long getMessageCounter(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_COUNTER);
+	}
+
+	@Override
+	public OwnerSession parseOwnerSession(BdfDictionary d)
+			throws FormatException {
+		if (getRole(d) != OWNER) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		long sessionCounter = getSessionCounter(d);
+		OwnerState state = OwnerState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		Introducee introduceeA = parseIntroducee(sessionId,
+				d.getDictionary(SESSION_KEY_INTRODUCEE_A));
+		Introducee introduceeB = parseIntroducee(sessionId,
+				d.getDictionary(SESSION_KEY_INTRODUCEE_B));
+		return new OwnerSession(sessionId, state, requestTimestamp, introduceeA,
+				introduceeB, sessionCounter);
+	}
+
+	private Introducee parseIntroducee(SessionId sessionId, BdfDictionary d)
+			throws FormatException {
+		MessageId lastLocalMessageId =
+				getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
+		MessageId lastRemoteMessageId =
+				getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
+		long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
+		GroupId groupId = getGroupId(d, SESSION_KEY_GROUP_ID);
+		Author author = getAuthor(d, SESSION_KEY_AUTHOR);
+		long abortCounter = d.getLong(SESSION_KEY_COUNTER);
+		return new Introducee(sessionId, groupId, author, localTimestamp,
+				lastLocalMessageId, lastRemoteMessageId, abortCounter);
+	}
+
+	@Override
+	public MailboxSession parseMailboxSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException {
+		if (getRole(d) != MAILBOX) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		long sessionCounter = getSessionCounter(d);
+		MailboxState state = MailboxState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		Author introducer = getAuthor(d, SESSION_KEY_INTRODUCER);
+		Local local = parseLocal(d.getDictionary(SESSION_KEY_LOCAL));
+		Remote remote = parseRemote(d.getDictionary(SESSION_KEY_REMOTE));
+		byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY);
+		Map<TransportId, KeySetId> transportKeys = parseTransportKeys(
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS));
+		return new MailboxSession(sessionId, state, requestTimestamp,
+				introducerGroupId, introducer, local, remote, masterKey,
+				transportKeys, sessionCounter);
+	}
+
+	@Override
+	public IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException {
+		if (getRole(d) != INTRODUCEE) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		long sessionCounter = getSessionCounter(d);
+		IntroduceeState state = IntroduceeState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		Author introducer = getAuthor(d, SESSION_KEY_INTRODUCER);
+		Local local = parseLocal(d.getDictionary(SESSION_KEY_LOCAL));
+		Remote remote = parseRemote(d.getDictionary(SESSION_KEY_REMOTE));
+		byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY);
+		Map<TransportId, KeySetId> transportKeys = parseTransportKeys(
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS));
+		return new IntroduceeSession(sessionId, state, requestTimestamp,
+				introducerGroupId, introducer, local, remote, masterKey,
+				transportKeys, sessionCounter);
+	}
+
+	private Local parseLocal(BdfDictionary d) throws FormatException {
+		boolean alice = d.getBoolean(SESSION_KEY_ALICE);
+		MessageId lastLocalMessageId =
+				getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
+		long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
+		byte[] ephemeralPublicKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PUBLIC_KEY);
+		BdfDictionary tpDict =
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_PROPERTIES);
+		byte[] ephemeralPrivateKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PRIVATE_KEY);
+		Map<TransportId, TransportProperties> transportProperties =
+				tpDict == null ? null : clientHelper
+						.parseAndValidateTransportPropertiesMap(tpDict);
+		long acceptTimestamp = d.getLong(SESSION_KEY_ACCEPT_TIMESTAMP);
+		byte[] macKey = d.getOptionalRaw(SESSION_KEY_MAC_KEY);
+		return new Local(alice, lastLocalMessageId, localTimestamp,
+				ephemeralPublicKey, ephemeralPrivateKey, acceptTimestamp,
+				macKey);
+	}
+
+	private Remote parseRemote(BdfDictionary d) throws FormatException {
+		boolean alice = d.getBoolean(SESSION_KEY_ALICE);
+		Author remoteAuthor = getAuthor(d, SESSION_KEY_REMOTE_AUTHOR);
+		MessageId lastRemoteMessageId =
+				getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
+		byte[] ephemeralPublicKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PUBLIC_KEY);
+		BdfDictionary tpDict =
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_PROPERTIES);
+		Map<TransportId, TransportProperties> transportProperties =
+				tpDict == null ? null : clientHelper
+						.parseAndValidateTransportPropertiesMap(tpDict);
+		long acceptTimestamp = d.getLong(SESSION_KEY_ACCEPT_TIMESTAMP);
+		byte[] macKey = d.getOptionalRaw(SESSION_KEY_MAC_KEY);
+		return new Remote(alice, remoteAuthor, lastRemoteMessageId,
+				ephemeralPublicKey, transportProperties, acceptTimestamp,
+				macKey);
+	}
+
+	private int getState(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_STATE).intValue();
+	}
+
+	private long getSessionCounter(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_COUNTER);
+	}
+
+	private SessionId getSessionId(BdfDictionary d) throws FormatException {
+		byte[] b = d.getRaw(SESSION_KEY_SESSION_ID);
+		return new SessionId(b);
+	}
+
+	@Nullable
+	private MessageId getMessageId(BdfDictionary d, String key)
+			throws FormatException {
+		byte[] b = d.getOptionalRaw(key);
+		return b == null ? null : new MessageId(b);
+	}
+
+	private GroupId getGroupId(BdfDictionary d, String key)
+			throws FormatException {
+		return new GroupId(d.getRaw(key));
+	}
+
+	private Author getAuthor(BdfDictionary d, String key)
+			throws FormatException {
+		return clientHelper.parseAndValidateAuthor(d.getList(key));
+	}
+
+	@Nullable
+	private Map<TransportId, KeySetId> parseTransportKeys(
+			@Nullable BdfDictionary d) throws FormatException {
+		if (d == null) return null;
+		Map<TransportId, KeySetId> map = new HashMap<>(d.size());
+		for (String key : d.keySet()) {
+			map.put(new TransportId(key),
+					new KeySetId(d.getLong(key).intValue()));
+		}
+		return map;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxState.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxState.java
new file mode 100644
index 000000000..9a00c0584
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MailboxState.java
@@ -0,0 +1,34 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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),
+	LOCAL_DECLINED(1),
+	AWAIT_INTRODUCEE_RESPONSE(2),
+	INTRODUCEE_DECLINED(3),
+	CONTACT_ADDED(4);
+
+	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/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageMetadata.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageMetadata.java
new file mode 100644
index 000000000..2acc67711
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageMetadata.java
@@ -0,0 +1,55 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MessageMetadata {
+
+	private final MessageType type;
+	@Nullable
+	private final SessionId sessionId;
+	private final long timestamp, counter;
+	private final boolean local;
+	private final boolean available;
+
+	MessageMetadata(MessageType type, @Nullable SessionId sessionId,
+			long timestamp, boolean local, long counter, boolean available) {
+		this.type = type;
+		this.sessionId = sessionId;
+		this.timestamp = timestamp;
+		this.local = local;
+		this.counter = counter;
+		this.available = available;
+	}
+
+	MessageType getMessageType() {
+		return type;
+	}
+
+	@Nullable
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	boolean isLocal() {
+		return local;
+	}
+
+	long getCounter() {
+		return counter;
+	}
+
+	boolean isAvailableToAnswer() {
+		return available;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageType.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageType.java
new file mode 100644
index 000000000..10a6efd80
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/MessageType.java
@@ -0,0 +1,35 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum MessageType {
+
+	MAILBOX_REQUEST(0),
+	MAILBOX_ACCEPT(1),
+	INTRODUCEE_ACCEPT(3),
+	CONTACT_INFO(4),
+	MAILBOX_AUTH(5),
+	DECLINE(6),
+	ABORT(7);
+
+	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/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerProtocolEngine.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerProtocolEngine.java
new file mode 100644
index 000000000..50be2b64b
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerProtocolEngine.java
@@ -0,0 +1,262 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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.client.ProtocolStateException;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionAbortedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionResponseReceivedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionSucceededEvent;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.KeyManager;
+
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.mailbox.introduction.OwnerState.ADDED;
+import static org.briarproject.bramble.mailbox.introduction.OwnerState.AWAIT_AUTH_M;
+import static org.briarproject.bramble.mailbox.introduction.OwnerState.AWAIT_RESPONSE_B;
+import static org.briarproject.bramble.mailbox.introduction.OwnerState.AWAIT_RESPONSE_M;
+import static org.briarproject.bramble.mailbox.introduction.OwnerState.START;
+
+class OwnerProtocolEngine extends AbstractProtocolEngine<OwnerSession> {
+
+	private static final Logger LOG =
+			Logger.getLogger(OwnerProtocolEngine.class.getName());
+
+	@Inject
+	OwnerProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ContactManager contactManager,
+			ContactGroupFactory contactGroupFactory,
+			IdentityManager identityManager, MailboxMessageParser messageParser,
+			MailboxMessageEncoder messageEncoder, Clock clock,
+			MailboxIntroductionCrypto crypto, KeyManager keyManager,
+			TransportPropertyManager transportPropertyManager) {
+		super(db, clientHelper, contactManager, contactGroupFactory,
+				identityManager, messageParser, messageEncoder, clock, crypto,
+				keyManager, transportPropertyManager);
+	}
+
+	OwnerSession onStartStartIntroduction(Transaction txn, OwnerSession s,
+			long timestamp) throws DbException {
+		switch (s.getState()) {
+			case START:
+				return onLocalRequest(txn, s, timestamp);
+			case AWAIT_RESPONSE_M:
+			case M_DECLINED:
+			case AWAIT_RESPONSE_B:
+			case B_DECLINED:
+			case AWAIT_AUTH_M:
+			case ADDED:
+				throw new ProtocolStateException(); // Invalid in these states
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	@Override
+	public OwnerSession onRequestMessage(Transaction txn, OwnerSession session,
+			RequestMessage m) throws DbException, FormatException {
+		//		return abort(txn, session);
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public OwnerSession onMailboxAcceptMessage(Transaction txn, OwnerSession s,
+			MailboxAcceptMessage m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case START:
+			case AWAIT_RESPONSE_M:
+				return onMailboxAccept(txn, s, m);
+			case M_DECLINED:
+			case AWAIT_RESPONSE_B:
+			case B_DECLINED:
+			case AWAIT_AUTH_M:
+			case ADDED:
+				throw new ProtocolStateException(); // Invalid in these states
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private OwnerSession onMailboxAccept(Transaction txn, OwnerSession s,
+			MailboxAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getMailbox().lastRemoteMessageId,
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendMailboxAcceptMessage(txn, s.getIntroducee(),
+				clock.currentTimeMillis(), s.getMailbox().author,
+				m.getEphemeralPublicKey(), m.getAcceptTimestamp());
+		broadcastMailboxIntroductionResponseReceived(txn, s.getMailbox().author,
+				s.getIntroducee().author);
+		return new OwnerSession(s.getSessionId(), AWAIT_RESPONSE_B,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), m.getMessageId(),
+						s.getAbortCounter()),
+				new Introducee(s.getIntroducee(), forward, s.getAbortCounter()),
+				s.getAbortCounter());
+	}
+
+	@Override
+	public OwnerSession onDeclineMessage(Transaction txn, OwnerSession session,
+			DeclineMessage m) throws DbException, FormatException {
+		return null;
+	}
+
+	@Override
+	public OwnerSession onAuthMessage(Transaction txn, OwnerSession s,
+			MailboxAuthMessage m) throws DbException, FormatException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getMailbox().getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendMailboxAuthMessage(txn, s.getIntroducee(),
+				clock.currentTimeMillis(), m.getTransportProperties(),
+				m.getMac(), m.getSignature());
+		Contact c = contactManager
+				.getContact(txn, s.getIntroducee().author.getId(),
+						identityManager.getLocalAuthor().getId());
+		db.setMailboxForContact(txn, c.getId(), null, c.getId());
+		txn.attach(new MailboxIntroductionSucceededEvent(c));
+		return new OwnerSession(s.getSessionId(), ADDED,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), m.getMessageId(),
+						s.getAbortCounter()),
+				new Introducee(s.getIntroducee(), forward, s.getAbortCounter()),
+				s.getAbortCounter());
+	}
+
+	@Override
+	public OwnerSession onAbortMessage(Transaction txn, OwnerSession s,
+			AbortMessage m) throws DbException {
+		// Forward ABORT message
+		Introducee i = getOtherIntroducee(s, m.getGroupId());
+		long timestamp = getLocalTimestamp(s, i);
+		Message sent = sendAbortMessage(txn, i, timestamp);
+
+		// Broadcast abort event for testing
+		txn.attach(new MailboxIntroductionAbortedEvent(s.getSessionId()));
+
+		// Reset the session back to initial state
+		Introducee mailbox, introduceeB;
+		long abortCounter = s.getAbortCounter() + 1;
+		if (i.equals(s.getMailbox())) {
+			mailbox = new Introducee(s.getMailbox(), sent, abortCounter);
+			introduceeB = new Introducee(s.getIntroducee(), m.getMessageId(),
+					abortCounter);
+		} else if (i.equals(s.getIntroducee())) {
+			mailbox = new Introducee(s.getMailbox(), m.getMessageId(),
+					abortCounter);
+			introduceeB = new Introducee(s.getIntroducee(), sent, abortCounter);
+		} else throw new AssertionError();
+		return new OwnerSession(s.getSessionId(), START,
+				s.getRequestTimestamp(), mailbox, introduceeB, abortCounter);
+	}
+
+	@Override
+	public OwnerSession onIntroduceeAcceptMessage(Transaction txn,
+			OwnerSession s, IntroduceeAcceptMessage m) throws DbException {
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s.getIntroducee().getLastLocalMessageId(),
+				m.getPreviousMessageId())) return abort(txn, s);
+		Message forward = sendIntroduceeResponseMessage(txn, s.getMailbox(),
+				s.getMailbox().lastRemoteMessageId, clock.currentTimeMillis(),
+				m.getEphemeralPublicKey(), m.getMac(), m.getSignature(),
+				m.getAcceptTimestamp());
+		return new OwnerSession(s.getSessionId(), AWAIT_AUTH_M,
+				s.getRequestTimestamp(),
+				new Introducee(s.getMailbox(), forward, s.getAbortCounter()),
+				new Introducee(s.getIntroducee(), m.getMessageId(),
+						s.getAbortCounter()), s.getAbortCounter());
+	}
+
+	private OwnerSession onLocalRequest(Transaction txn, OwnerSession s,
+			long timestamp) throws DbException {
+		// Send REQUEST messages
+		long maxIntroduceeTimestamp =
+				Math.max(getLocalTimestamp(s, s.getMailbox()),
+						getLocalTimestamp(s, s.getIntroducee()));
+		long localTimestamp = Math.max(timestamp, maxIntroduceeTimestamp);
+		Message sentMailbox =
+				sendMailboxRequestMessage(txn, s.getMailbox(), localTimestamp,
+						s.getIntroducee().author);
+		// Move to the AWAIT_RESPONSES state
+		Introducee mailbox = new Introducee(s.getMailbox(), sentMailbox,
+				s.getAbortCounter());
+		Introducee b = new Introducee(s.getIntroducee().sessionId,
+				s.getIntroducee().groupId, s.getIntroducee().author,
+				s.getAbortCounter());
+		return new OwnerSession(s.getSessionId(), AWAIT_RESPONSE_M,
+				localTimestamp, mailbox, b, s.getAbortCounter());
+	}
+
+	OwnerSession onIntroduceeRemoved(Transaction txn,
+			Introducee remainingIntroducee, OwnerSession session)
+			throws DbException {
+		// abort session
+		OwnerSession s = abort(txn, session);
+		// reset information for introducee that was removed
+		Introducee mailbox, introduceeB;
+		if (remainingIntroducee.author.equals(s.getMailbox().author)) {
+			mailbox = s.getMailbox();
+			introduceeB =
+					new Introducee(s.getSessionId(), s.getIntroducee().groupId,
+							s.getIntroducee().author, s.getAbortCounter());
+		} else if (remainingIntroducee.author
+				.equals(s.getIntroducee().author)) {
+			mailbox = new Introducee(s.getSessionId(), s.getMailbox().groupId,
+					s.getMailbox().author, s.getAbortCounter());
+			introduceeB = s.getIntroducee();
+		} else throw new DbException();
+		return new OwnerSession(s.getSessionId(), s.getState(),
+				s.getRequestTimestamp(), mailbox, introduceeB, 0);
+	}
+
+	private long getLocalTimestamp(OwnerSession s, PeerSession p) {
+		return getLocalTimestamp(p.getLocalTimestamp(),
+				s.getRequestTimestamp());
+	}
+
+	private OwnerSession abort(Transaction txn, OwnerSession s)
+			throws DbException {
+		// Broadcast abort event for testing
+		txn.attach(new MailboxIntroductionAbortedEvent(s.getSessionId()));
+		// Send an ABORT message to both introducees
+		long timestampMailbox = getLocalTimestamp(s, s.getMailbox());
+		Message sentMailbox =
+				sendAbortMessage(txn, s.getMailbox(), timestampMailbox);
+		long timestampB = getLocalTimestamp(s, s.getIntroducee());
+		Message sentB = sendAbortMessage(txn, s.getIntroducee(), timestampB);
+		long abortCounter = s.getAbortCounter() + 1;
+		// Reset the session back to initial state
+		Introducee mailbox =
+				new Introducee(s.getMailbox(), sentMailbox, abortCounter);
+		Introducee introducee =
+				new Introducee(s.getIntroducee(), sentB, abortCounter);
+		return new OwnerSession(s.getSessionId(), START,
+				s.getRequestTimestamp(), mailbox, introducee, abortCounter);
+	}
+
+	private Introducee getOtherIntroducee(OwnerSession s, GroupId g) {
+		if (s.getMailbox().groupId.equals(g)) return s.getIntroducee();
+		else if (s.getIntroducee().groupId.equals(g)) return s.getMailbox();
+		else throw new AssertionError();
+	}
+
+	void broadcastMailboxIntroductionResponseReceived(Transaction txn,
+			Author from, Author to) {
+		MailboxIntroductionResponseReceivedEvent e =
+				new MailboxIntroductionResponseReceivedEvent(from, to);
+		txn.attach(e);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerSession.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerSession.java
new file mode 100644
index 000000000..e88389215
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerSession.java
@@ -0,0 +1,52 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.mailbox.Role;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.mailbox.Role.OWNER;
+
+@Immutable
+@NotNullByDefault
+class OwnerSession extends Session<OwnerState> {
+
+	private final Introducee mailbox, introducee;
+
+	OwnerSession(SessionId sessionId, OwnerState state, long requestTimestamp,
+			Introducee mailbox, Introducee introducee, long sessionCounter) {
+		super(sessionId, state, requestTimestamp, sessionCounter);
+		this.mailbox = mailbox;
+		this.introducee = introducee;
+	}
+
+	OwnerSession(SessionId sessionId, GroupId groupIdA, Author authorA,
+			GroupId groupIdB, Author authorB, long abortCounter) {
+		this(sessionId, OwnerState.START, -1,
+				new Introducee(sessionId, groupIdA, authorA, abortCounter),
+				new Introducee(sessionId, groupIdB, authorB, abortCounter),
+				abortCounter);
+	}
+
+	public static OwnerSession finished(OwnerSession s) {
+		return null;
+	}
+
+	@Override
+	Role getRole() {
+		return OWNER;
+	}
+
+	Introducee getMailbox() {
+		return mailbox;
+	}
+
+	Introducee getIntroducee() {
+		return introducee;
+	}
+
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerState.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerState.java
new file mode 100644
index 000000000..bf77ef7c7
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/OwnerState.java
@@ -0,0 +1,36 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum OwnerState implements State {
+
+	START(0),
+	AWAIT_RESPONSE_M(1),
+	M_DECLINED(2),
+	AWAIT_RESPONSE_B(3),
+	B_DECLINED(4),
+	AWAIT_AUTH_M(5),
+	ADDED(6);
+
+	private final int value;
+
+	OwnerState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static OwnerState fromValue(int value) throws FormatException {
+		for (OwnerState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/PeerSession.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/PeerSession.java
new file mode 100644
index 000000000..b832a5f57
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/PeerSession.java
@@ -0,0 +1,27 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+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;
+
+@NotNullByDefault
+interface PeerSession {
+
+	SessionId getSessionId();
+
+	GroupId getContactGroupId();
+
+	long getLocalTimestamp();
+
+	@Nullable
+	MessageId getLastLocalMessageId();
+
+	@Nullable
+	MessageId getLastRemoteMessageId();
+
+	long getAbortCounter();
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/ProtocolEngine.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/ProtocolEngine.java
new file mode 100644
index 000000000..c0b18de6b
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/ProtocolEngine.java
@@ -0,0 +1,30 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface ProtocolEngine<S extends Session> {
+
+	S onRequestMessage(Transaction txn, S session, RequestMessage m)
+			throws DbException, FormatException;
+
+	S onMailboxAcceptMessage(Transaction txn, S session, MailboxAcceptMessage m)
+			throws DbException, FormatException;
+
+	S onIntroduceeAcceptMessage(Transaction txn, S session,
+			IntroduceeAcceptMessage acceptMessage) throws DbException;
+
+	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/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/RequestMessage.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/RequestMessage.java
new file mode 100644
index 000000000..d490ef2ac
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/RequestMessage.java
@@ -0,0 +1,28 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class RequestMessage extends AbstractMailboxIntroductionMessage {
+
+
+	private final Author author;
+
+	protected RequestMessage(MessageId messageId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId,
+			Author author) {
+		super(messageId, groupId, timestamp, previousMessageId);
+		this.author = author;
+	}
+
+	public Author getAuthor() {
+		return author;
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Session.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Session.java
new file mode 100644
index 000000000..6039a5a69
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/Session.java
@@ -0,0 +1,44 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import org.briarproject.bramble.api.client.SessionId;
+import org.briarproject.bramble.api.mailbox.Role;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+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 abortCounter;
+
+	Session(SessionId sessionId, S state, long requestTimestamp,
+			long abortCounter) {
+		this.sessionId = sessionId;
+		this.state = state;
+		this.requestTimestamp = requestTimestamp;
+		this.abortCounter = abortCounter;
+	}
+
+	abstract Role getRole();
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	S getState() {
+		return state;
+	}
+
+	long getRequestTimestamp() {
+		return requestTimestamp;
+	}
+
+	public long getAbortCounter() {
+		return abortCounter;
+	}
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/State.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/State.java
new file mode 100644
index 000000000..1e794cd83
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/introduction/State.java
@@ -0,0 +1,7 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+interface State {
+
+	int getValue();
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
index 98822d8b0..7851663bc 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
@@ -108,6 +108,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	@Override
 	public void onClientVisibilityChanging(Transaction txn, Contact c,
 			Visibility v) throws DbException {
+		if (!getApplicableContactTypes().contains(c.getType())) return;
 		// Apply the client's visibility to the contact group
 		Group g = getContactGroup(c);
 		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
@@ -153,7 +154,19 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			throws DbException {
 		Map<TransportId, TransportProperties> properties = getLocalProperties();
 		for (Entry e : properties.entrySet()) {
-			e.setValue(Collections.EMPTY_MAP);
+			e.setValue(new TransportProperties(Collections.EMPTY_MAP));
+		}
+		return properties;
+	}
+
+	@Override
+	public Map<TransportId, TransportProperties> getLocalAnonymizedProperties(
+			Transaction txn)
+			throws DbException {
+		Map<TransportId, TransportProperties> properties =
+				getLocalProperties(txn);
+		for (Entry e : properties.entrySet()) {
+			e.setValue(new TransportProperties(Collections.EMPTY_MAP));
 		}
 		return properties;
 	}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTest.java
new file mode 100644
index 000000000..8c73d1110
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTest.java
@@ -0,0 +1,404 @@
+package org.briarproject.bramble.integration;
+
+import net.jodah.concurrentunit.Waiter;
+
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.contact.PrivateMailbox;
+import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+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.AuthorFactory;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.bramble.api.sync.MessageFactory;
+import org.briarproject.bramble.api.sync.SyncSession;
+import org.briarproject.bramble.api.sync.SyncSessionFactory;
+import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.StreamWriter;
+import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
+import org.briarproject.bramble.identity.IdentityModule;
+import org.briarproject.bramble.lifecycle.LifecycleModule;
+import org.briarproject.bramble.mailbox.introduction.MailboxIntroductionModule;
+import org.briarproject.bramble.properties.PropertiesModule;
+import org.briarproject.bramble.sync.SyncModule;
+import org.briarproject.bramble.system.SystemModule;
+import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.bramble.test.TestUtils;
+import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
+import org.junit.After;
+import org.junit.Before;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static junit.framework.Assert.assertNotNull;
+import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
+import static org.briarproject.bramble.test.TestPluginConfigModule.MAX_LATENCY;
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
+import static org.junit.Assert.assertTrue;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public abstract class BrambleIntegrationTest<C extends BrambleIntegrationTestComponent>
+		extends BrambleTestCase {
+
+	private static final Logger LOG =
+			Logger.getLogger(BrambleIntegrationTest.class.getName());
+
+	@Nullable
+	protected ContactId contactId1From2, contactId2From1;
+	protected ContactId contactId0From1, contactId0From2, contactId1From0,
+			contactId2From0, privateMailboxIdFrom0, contactId0FromMailbox;
+	protected Contact contact0From1, contact0From2, contact1From0,
+			contact2From0, contact0FromMailbox;
+	protected PrivateMailbox privateMailboxFrom0;
+	protected LocalAuthor author0, author1, author2, authorMailbox;
+	protected ContactManager contactManager0, contactManager1, contactManager2,
+			contactManagerMailbox;
+	protected IdentityManager identityManager0, identityManager1,
+			identityManager2, identityManagerMailbox;
+	protected DatabaseComponent db0, db1, db2, dbMailbox;
+
+	private LifecycleManager lifecycleManager0, lifecycleManager1,
+			lifecycleManager2, lifecycleManagerMailbox;
+	private SyncSessionFactory sync0, sync1, sync2, syncMailbox;
+
+	@Inject
+	protected Clock clock;
+	@Inject
+	protected CryptoComponent crypto;
+	@Inject
+	protected ClientHelper clientHelper;
+	@Inject
+	protected AuthorFactory authorFactory;
+	@Inject
+	protected MessageFactory messageFactory;
+	@Inject
+	protected ContactGroupFactory contactGroupFactory;
+
+	// objects accessed from background threads need to be volatile
+	private volatile Waiter validationWaiter;
+	private volatile Waiter deliveryWaiter;
+
+	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 {
+		assertTrue(testDir.mkdirs());
+		createComponents();
+
+		identityManager0 = c0.getIdentityManager();
+		identityManager1 = c1.getIdentityManager();
+		identityManager2 = c2.getIdentityManager();
+		identityManagerMailbox = cMailbox.getIdentityManager();
+		contactManager0 = c0.getContactManager();
+		contactManager1 = c1.getContactManager();
+		contactManager2 = c2.getContactManager();
+		contactManagerMailbox = cMailbox.getContactManager();
+		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();
+		deliveryWaiter = new Waiter();
+
+		createAndRegisterIdentities();
+		startLifecycles();
+		listenToEvents();
+		addDefaultContacts();
+	}
+
+	abstract protected void createComponents();
+
+	protected void injectEagerSingletons(
+			BrambleIntegrationTestComponent component) {
+		component.inject(new ContactModule.EagerSingletons());
+		component.inject(new CryptoExecutorModule.EagerSingletons());
+		component.inject(new IdentityModule.EagerSingletons());
+		component.inject(new LifecycleModule.EagerSingletons());
+		component.inject(new MailboxIntroductionModule.EagerSingletons());
+		component.inject(new PropertiesModule.EagerSingletons());
+		component.inject(new SyncModule.EagerSingletons());
+		component.inject(new SystemModule.EagerSingletons());
+		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new VersioningModule.EagerSingletons());
+	}
+
+	private void startLifecycles() throws InterruptedException {
+		// Start the lifecycle manager and wait for it to finish starting
+		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() {
+		Listener listener0 = new Listener();
+		c0.getEventBus().addListener(listener0);
+		Listener listener1 = new Listener();
+		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 {
+		@Override
+		public void eventOccurred(Event e) {
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				if (!event.isLocal()) {
+					if (event.getState() == DELIVERED) {
+						LOG.info("Delivered new message");
+						deliveryWaiter.resume();
+					} else if (event.getState() == INVALID ||
+							event.getState() == PENDING) {
+						LOG.info("Validated new " + event.getState().name() +
+								" message");
+						validationWaiter.resume();
+					}
+				}
+			}
+		}
+	}
+
+	private void createAndRegisterIdentities() {
+		author0 = identityManager0.createLocalAuthor(AUTHOR0);
+		identityManager0.registerLocalAuthor(author0);
+		author1 = identityManager1.createLocalAuthor(AUTHOR1);
+		identityManager1.registerLocalAuthor(author1);
+		author2 = identityManager2.createLocalAuthor(AUTHOR2);
+		identityManager2.registerLocalAuthor(author2);
+		authorMailbox =
+				identityManagerMailbox.createLocalAuthor(AUTHOR_MAILBOX);
+		identityManagerMailbox.registerLocalAuthor(authorMailbox);
+	}
+
+	protected void addDefaultContacts() throws Exception {
+		contactId1From0 = contactManager0
+				.addContact(author1, author0.getId(), getSecretKey(),
+						clock.currentTimeMillis(), true, true, true);
+		contact1From0 = contactManager0.getContact(contactId1From0);
+		contactId0From1 = contactManager1
+				.addContact(author0, author1.getId(), getSecretKey(),
+						clock.currentTimeMillis(), true, true, true);
+		contact0From1 = contactManager1.getContact(contactId0From1);
+		contactId2From0 = contactManager0
+				.addContact(author2, author0.getId(), getSecretKey(),
+						clock.currentTimeMillis(), true, true, true);
+		contact2From0 = contactManager0.getContact(contactId2From0);
+		contactId0From2 = contactManager2
+				.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 {
+		contactId2From1 = contactManager1
+				.addContact(author2, author1.getId(), getSecretKey(),
+						clock.currentTimeMillis(), true, true, true);
+		contactId1From2 = contactManager2
+				.addContact(author1, author2.getId(), getSecretKey(),
+						clock.currentTimeMillis(), true, true, true);
+
+		// Sync initial client versioning updates
+		sync1To2(1, true);
+		sync2To1(1, true);
+		sync1To2(1, true);
+	}
+
+	@After
+	public void tearDown() throws Exception {
+		stopLifecycles();
+		TestUtils.deleteTestDirectory(testDir);
+	}
+
+	private void stopLifecycles() throws InterruptedException {
+		// Clean up
+		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);
+	}
+
+	protected void sync0To2(int num, boolean valid)
+			throws IOException, TimeoutException {
+		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);
+	}
+
+	protected void sync2To0(int num, boolean valid)
+			throws IOException, TimeoutException {
+		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);
+	}
+
+	protected void sync1To2(int num, boolean valid)
+			throws IOException, TimeoutException {
+		assertNotNull(contactId2From1);
+		assertNotNull(contactId1From2);
+		syncMessage(sync1, contactId1From2, sync2, contactId2From1, num,
+				valid);
+	}
+
+	private void syncMessage(SyncSessionFactory fromSync, ContactId fromId,
+			SyncSessionFactory toSync, ContactId toId, int num, boolean valid)
+			throws IOException, TimeoutException {
+
+		// Debug output
+		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);
+		// Write whatever needs to be written
+		sessionFrom.run();
+		out.close();
+
+		ByteArrayInputStream in =
+				new ByteArrayInputStream(out.toByteArray());
+		// Create an incoming sync session
+		SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
+		// Read whatever needs to be read
+		sessionTo.run();
+		in.close();
+
+		if (valid) {
+			deliveryWaiter.await(TIMEOUT, num);
+		} else {
+			validationWaiter.await(TIMEOUT, num);
+		}
+	}
+
+	protected void removeAllContacts() throws DbException {
+		contactManager0.removeContact(contactId1From0);
+		contactManager0.removeContact(contactId2From0);
+		contactManager1.removeContact(contactId0From1);
+		contactManager2.removeContact(contactId0From2);
+		contactManagerMailbox.removeContact(contactId0FromMailbox);
+		assertNotNull(contactId2From1);
+		contactManager1.removeContact(contactId2From1);
+		assertNotNull(contactId1From2);
+		contactManager2.removeContact(contactId1From2);
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTestComponent.java
new file mode 100644
index 000000000..2a0e20334
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/integration/BrambleIntegrationTestComponent.java
@@ -0,0 +1,102 @@
+package org.briarproject.bramble.integration;
+
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.identity.AuthorFactory;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.SyncSessionFactory;
+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.api.mailbox.MailboxIntroductionManager;
+import org.briarproject.bramble.mailbox.introduction.MailboxIntroductionModule;
+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 javax.inject.Singleton;
+
+import dagger.Component;
+
+@Singleton
+@Component(modules = {
+		TestDatabaseModule.class,
+		TestPluginConfigModule.class,
+		TestSecureRandomModule.class,
+		ClientModule.class,
+		ContactModule.class,
+		CryptoModule.class,
+		CryptoExecutorModule.class,
+		DataModule.class,
+		DatabaseModule.class,
+		EventModule.class,
+		IdentityModule.class,
+		LifecycleModule.class,
+		MailboxIntroductionModule.class,
+		PropertiesModule.class,
+		RecordModule.class,
+		SyncModule.class,
+		SystemModule.class,
+		TransportModule.class,
+		VersioningModule.class
+})
+public interface BrambleIntegrationTestComponent {
+
+	void inject(BrambleIntegrationTest<BrambleIntegrationTestComponent> init);
+
+	void inject(ContactModule.EagerSingletons init);
+
+	void inject(CryptoExecutorModule.EagerSingletons init);
+
+	void inject(IdentityModule.EagerSingletons init);
+
+	void inject(LifecycleModule.EagerSingletons init);
+
+	void inject(MailboxIntroductionModule.EagerSingletons init);
+
+	void inject(PropertiesModule.EagerSingletons init);
+
+	void inject(SyncModule.EagerSingletons init);
+
+	void inject(SystemModule.EagerSingletons init);
+
+	void inject(TransportModule.EagerSingletons init);
+
+	void inject(VersioningModule.EagerSingletons init);
+
+	LifecycleManager getLifecycleManager();
+
+	EventBus getEventBus();
+
+	IdentityManager getIdentityManager();
+
+	ClientHelper getClientHelper();
+
+	ContactManager getContactManager();
+
+	SyncSessionFactory getSyncSessionFactory();
+
+	DatabaseComponent getDatabaseComponent();
+
+	TransportPropertyManager getTransportPropertyManager();
+
+	AuthorFactory getAuthorFactory();
+
+	MailboxIntroductionManager getMailboxIntroductionManager();
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/integration/TestStreamWriter.java b/bramble-core/src/test/java/org/briarproject/bramble/integration/TestStreamWriter.java
new file mode 100644
index 000000000..6b619169f
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/integration/TestStreamWriter.java
@@ -0,0 +1,25 @@
+package org.briarproject.bramble.integration;
+
+import org.briarproject.bramble.api.transport.StreamWriter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+class TestStreamWriter implements StreamWriter {
+
+	private final OutputStream out;
+
+	TestStreamWriter(OutputStream out) {
+		this.out = out;
+	}
+
+	@Override
+	public OutputStream getOutputStream() {
+		return out;
+	}
+
+	@Override
+	public void sendEndOfStream() throws IOException {
+		out.flush();
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTest.java
new file mode 100644
index 000000000..d69b67fc5
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTest.java
@@ -0,0 +1,303 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+import net.jodah.concurrentunit.Waiter;
+
+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.event.Event;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.identity.AuthorId;
+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.client.SessionId;
+import org.briarproject.bramble.integration.BrambleIntegrationTest;
+import org.briarproject.bramble.integration.BrambleIntegrationTestComponent;
+import org.briarproject.bramble.api.mailbox.MailboxIntroductionManager;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionAbortedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionRequestReceivedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionResponseReceivedEvent;
+import org.briarproject.bramble.api.mailbox.event.MailboxIntroductionSucceededEvent;
+import org.briarproject.bramble.test.TestDatabaseModule;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+
+import static org.briarproject.bramble.api.mailbox.MailboxIntroductionManager.CLIENT_ID;
+import static org.briarproject.bramble.api.mailbox.MailboxIntroductionManager.MAJOR_VERSION;
+import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID;
+import static org.briarproject.bramble.test.TestUtils.getTransportProperties;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class MailboxIntroductionIntegrationTest extends
+		BrambleIntegrationTest<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;
+	private IntroduceeListener listenerMailbox;
+
+	/*
+	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);
+	}
+
+	@Override
+	protected void injectEagerSingletons(
+			BrambleIntegrationTestComponent component) {
+		super.injectEagerSingletons(component);
+		component.inject(new MailboxIntroductionModule.EagerSingletons());
+	}
+
+	@Test
+	public void testIntroductionSession() throws Exception {
+		addListeners(true, 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(listenerMailbox.requestReceived);
+		assertEquals(authorMailbox.getId(), listenerMailbox.getRequest());
+		// sync RESPONSE message
+		syncMailboxTo0(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener0.response1Received);
+		assertEquals(authorMailbox.getId(), listener0.getAuthors()[0]);
+		assertEquals(author1.getId(), listener0.getAuthors()[1]);
+		// sync/forward RESPONSE message to the contact
+		sync0To1(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		sync1To0(1, true);
+		sync0ToMailbox(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listenerMailbox.succeeded);
+		assertEquals(contact1From0.getAuthor(),
+				listenerMailbox.introduceeContact.getAuthor());
+		syncMailboxTo0(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener0.success);
+		sync0To1(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener1.succeeded);
+		assertEquals(privateMailboxFrom0.getAuthor(),
+				listener1.introduceeContact.getAuthor());
+	}
+
+
+	private void addTransportProperties()
+			throws DbException, IOException, TimeoutException {
+		TransportPropertyManager tpm0 = c0.getTransportPropertyManager();
+		TransportPropertyManager tpm1 = c1.getTransportPropertyManager();
+		TransportPropertyManager tpm2 = c2.getTransportPropertyManager();
+		TransportPropertyManager tpmMailbox =
+				cMailbox.getTransportPropertyManager();
+
+		tpm0.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
+		sync0To1(1, true);
+		sync0To2(1, true);
+		sync0ToMailbox(1, true);
+
+		tpm1.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
+		sync1To0(1, true);
+
+		tpm2.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
+		sync2To0(1, true);
+
+		tpmMailbox
+				.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
+		syncMailboxTo0(1, true);
+	}
+
+	private void addListeners(boolean accept1, boolean accept2,
+			boolean accept3) {
+		// 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);
+		listenerMailbox = new IntroduceeListener(3, accept3);
+		cMailbox.getEventBus().addListener(listenerMailbox);
+	}
+
+	@MethodsNotNullByDefault
+	@ParametersNotNullByDefault
+	private abstract class IntroductionListener implements EventListener {
+
+		protected volatile boolean aborted = false;
+		protected volatile Event latestEvent;
+
+		@SuppressWarnings("WeakerAccess")
+		AuthorId[] getAuthors() {
+			assertTrue(
+					latestEvent instanceof MailboxIntroductionResponseReceivedEvent);
+			AuthorId[] authors =
+					{((MailboxIntroductionResponseReceivedEvent) latestEvent)
+							.getFrom().getId(),
+							((MailboxIntroductionResponseReceivedEvent) latestEvent)
+									.getTo().getId()};
+			return authors;
+		}
+	}
+
+	@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 volatile Contact introduceeContact;
+
+		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 MailboxIntroductionRequestReceivedEvent) {
+				latestEvent = e;
+				MailboxIntroductionRequestReceivedEvent introEvent =
+						(MailboxIntroductionRequestReceivedEvent) e;
+				requestReceived = true;
+				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 MailboxIntroductionResponseReceivedEvent) {
+				// only broadcast for DECLINE messages in introducee role
+				latestEvent = e;
+				eventWaiter.resume();
+			} else if (e instanceof MailboxIntroductionSucceededEvent) {
+				latestEvent = e;
+				succeeded = true;
+				Contact contact =
+						((MailboxIntroductionSucceededEvent) e).getContact();
+				eventWaiter
+						.assertFalse(contact.getId().equals(contactId0From1));
+				introduceeContact = contact;
+				eventWaiter.resume();
+			} else if (e instanceof MailboxIntroductionAbortedEvent) {
+				latestEvent = e;
+				aborted = true;
+				eventWaiter.resume();
+			}
+		}
+
+		private AuthorId getRequest() {
+			assertTrue(
+					latestEvent instanceof MailboxIntroductionRequestReceivedEvent);
+			return ((MailboxIntroductionRequestReceivedEvent) latestEvent)
+					.getAuthorId();
+		}
+	}
+
+	@NotNullByDefault
+	private class OwnerListener extends IntroductionListener {
+
+		private volatile boolean response1Received = false;
+		private volatile boolean response2Received = false;
+		private volatile boolean success = false;
+
+		@Override
+		public void eventOccurred(Event e) {
+			if (e instanceof MailboxIntroductionResponseReceivedEvent) {
+				latestEvent = e;
+				AuthorId from =
+						((MailboxIntroductionResponseReceivedEvent) e).getFrom()
+								.getId();
+				if (from.equals(authorMailbox.getId())) {
+					response1Received = true;
+				}
+				eventWaiter.resume();
+			} else if (e instanceof MailboxIntroductionSucceededEvent) {
+				latestEvent = e;
+				success = true;
+				eventWaiter.resume();
+			}
+		}
+
+	}
+
+	private Group getLocalGroup() {
+		return contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
+	}
+
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTestComponent.java
new file mode 100644
index 000000000..706b951ea
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/introduction/MailboxIntroductionIntegrationTestComponent.java
@@ -0,0 +1,62 @@
+package org.briarproject.bramble.mailbox.introduction;
+
+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.integration.BrambleIntegrationTestComponent;
+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 javax.inject.Singleton;
+
+import dagger.Component;
+
+@Singleton
+@Component(modules = {
+		TestDatabaseModule.class,
+		TestPluginConfigModule.class,
+		TestSecureRandomModule.class,
+		ClientModule.class,
+		ContactModule.class,
+		CryptoModule.class,
+		CryptoExecutorModule.class,
+		DataModule.class,
+		DatabaseModule.class,
+		EventModule.class,
+		IdentityModule.class,
+		MailboxIntroductionModule.class,
+		LifecycleModule.class,
+		PropertiesModule.class,
+		RecordModule.class,
+		SyncModule.class,
+		SystemModule.class,
+		TransportModule.class,
+		VersioningModule.class
+})
+interface MailboxIntroductionIntegrationTestComponent
+		extends BrambleIntegrationTestComponent {
+
+	void inject(MailboxIntroductionIntegrationTest init);
+
+	MailboxMessageEncoder getMessageEncoder();
+
+	MailboxMessageParser getMessageParser();
+
+	MailboxSessionParser getSessionParser();
+
+	MailboxIntroductionCrypto getMailboxIntroductionCrypto();
+
+}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java
index 77dddbbe0..cbf36af8f 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationRequest;
 
 import javax.annotation.Nullable;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java
index e49142300..1e9cb9a83 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationResponse;
 
 @NotNullByDefault
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java
index 2d23ab0e8..cc13e0d8b 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationRequest;
 
 import javax.annotation.Nullable;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java
index 48f03ce85..45e8bc1fd 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationResponse;
 
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
index 532d63353..e08b16eef 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
@@ -5,7 +5,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.ClientId;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.messaging.ConversationManager.ConversationClient;
 
 import java.util.Collection;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java
index 009487fa2..0b72ddd3f 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java
@@ -4,7 +4,7 @@ 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.BaseMessageHeader;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java
index b2a804bd8..1201ce516 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.api.introduction;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java
index 816135d43..605e3eb54 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.api.introduction;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java
index 113ba400e..08b4f72ff 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java
@@ -2,7 +2,7 @@ package org.briarproject.briar.api.introduction.event;
 
 import org.briarproject.bramble.api.event.Event;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java
index 1062d0ce1..cf8498474 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java
@@ -6,8 +6,8 @@ import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.ClientId;
 import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.briar.api.client.ProtocolStateException;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.messaging.ConversationManager.ConversationClient;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.sharing.InvitationMessage;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java
index d447ae8ef..505a450a7 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.sharing.InvitationRequest;
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java
index b2e7d54b5..273a39e8e 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationResponse;
 
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java
index df93ebdfa..e6953894e 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java
@@ -5,7 +5,7 @@ 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.BaseMessageHeader;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java
index c5239310d..5a1b90c43 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java
index 517b6068e..18a3102a0 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java
index b593d2fdf..656fa623b 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java
@@ -5,7 +5,7 @@ import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.messaging.ConversationManager.ConversationClient;
 
 import java.util.Collection;
diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
index 7f6e581f5..e4584245b 100644
--- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
@@ -23,6 +23,7 @@ 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.client.BdfIncomingMessageHook;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogCommentHeader;
 import org.briarproject.briar.api.blog.BlogFactory;
@@ -32,7 +33,6 @@ import org.briarproject.briar.api.blog.BlogPostFactory;
 import org.briarproject.briar.api.blog.BlogPostHeader;
 import org.briarproject.briar.api.blog.MessageType;
 import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
-import org.briarproject.briar.client.BdfIncomingMessageHook;
 
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
diff --git a/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java
index 9e47fa3a3..cb8a280b2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java
@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.client.BdfIncomingMessageHook;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.MessageTracker.GroupCount;
 import org.briarproject.briar.api.messaging.ConversationManager.ConversationClient;
diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
index 58b35ea65..1c410c913 100644
--- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
@@ -27,7 +27,7 @@ import org.briarproject.briar.api.forum.ForumPost;
 import org.briarproject.briar.api.forum.ForumPostFactory;
 import org.briarproject.briar.api.forum.ForumPostHeader;
 import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
-import org.briarproject.briar.client.BdfIncomingMessageHook;
+import org.briarproject.bramble.api.client.BdfIncomingMessageHook;
 
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java
index e9a2d1233..cdcacb7b6 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.introduction;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
index 03b3486ad..b56aa0c1c 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
@@ -19,7 +19,7 @@ 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.IntroductionResponse;
 import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java
index 6cadae73b..dfc6e33ea 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java
@@ -5,7 +5,7 @@ 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 org.briarproject.bramble.api.client.SessionId;
 
 import java.util.Map;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java
index 5f767737d..c9c535667 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.introduction;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java
index 1de1a4eb5..f64ae616e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.introduction;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java
index 27386b905..b4b1acd3a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.introduction;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
index 6bdaea9d2..5857c3aa4 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
@@ -24,8 +24,8 @@ import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.api.transport.KeyManager;
 import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.ProtocolStateException;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.IntroductionRequest;
 import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
 import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
index b952b3dc5..203275b0f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
@@ -9,7 +9,7 @@ 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.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.Role;
 
 import java.util.Map;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
index c159697ad..d2d57016e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
@@ -14,7 +14,7 @@ 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.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
 import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 
@@ -556,6 +556,7 @@ class IntroducerProtocolEngine
 	private IntroducerSession abort(Transaction txn,
 			IntroducerSession s) throws DbException {
 		// Broadcast abort event for testing
+
 		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
 
 		// Send an ABORT message to both introducees
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
index 3c50621eb..b48595d70 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
@@ -5,7 +5,7 @@ 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.Role;
 
 import javax.annotation.Nullable;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
index 37f7aa10f..bef20f1d2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
@@ -5,7 +5,7 @@ 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 org.briarproject.bramble.api.client.SessionId;
 
 import java.security.GeneralSecurityException;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
index d892c58b3..045772589 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
@@ -12,7 +12,7 @@ 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 org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.introduction.IntroduceeSession.Common;
 import org.briarproject.briar.introduction.IntroduceeSession.Remote;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
index 19021d113..b2c2d0633 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
@@ -29,7 +29,7 @@ import org.briarproject.bramble.api.sync.MessageStatus;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.IntroductionManager;
 import org.briarproject.briar.api.introduction.IntroductionMessage;
 import org.briarproject.briar.api.introduction.IntroductionRequest;
@@ -155,6 +155,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 	@Override
 	public void onClientVisibilityChanging(Transaction txn, Contact c,
 			Visibility v) throws DbException {
+		if(!getApplicableContactTypes().contains(c.getType())) return;
 		// Apply the client's visibility to the contact group
 		Group g = getContactGroup(c);
 		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java
index 929c8bddf..570f76c68 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java
@@ -13,7 +13,7 @@ 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 org.briarproject.bramble.api.client.SessionId;
 
 import java.util.Collections;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java
index 1327b54a9..9ffad1a64 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java
@@ -8,7 +8,7 @@ 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.bramble.api.client.SessionId;
 
 import java.util.Map;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java
index fb3d66f38..7dde27034 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java
@@ -12,7 +12,7 @@ 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.bramble.api.client.SessionId;
 
 import java.util.Map;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java
index 102d72bfc..6186c50d5 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java
@@ -1,7 +1,7 @@
 package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java
index 503dd4cc6..8403f20c2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java
@@ -5,7 +5,7 @@ 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.bramble.api.client.SessionId;
 
 @NotNullByDefault
 interface MessageParser {
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java
index 69ddd242d..13224d612 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java
@@ -11,7 +11,7 @@ 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.bramble.api.client.SessionId;
 
 import java.util.Map;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java
index 3c453d2fa..d8504db14 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.introduction;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java b/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java
index 086dfb1a2..6e8bcff17 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java
@@ -1,7 +1,7 @@
 package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.Role;
 
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java
index c58cac3d9..82dfa092a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java
@@ -4,7 +4,7 @@ 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.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.Role;
 
 @NotNullByDefault
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
index 52c12e547..5c86e7ed2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
@@ -11,7 +11,7 @@ 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.bramble.api.client.SessionId;
 import org.briarproject.briar.api.introduction.Role;
 import org.briarproject.briar.introduction.IntroduceeSession.Local;
 import org.briarproject.briar.introduction.IntroduceeSession.Remote;
diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
index af46561a3..f70c239ed 100644
--- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
@@ -107,6 +107,7 @@ class MessagingManagerImpl extends ConversationClientImpl
 	@Override
 	public void onClientVisibilityChanging(Transaction txn, Contact c,
 			Visibility v) throws DbException {
+		if(!getApplicableContactTypes().contains(c.getType())) return;
 		// Apply the client's visibility to the contact group
 		Group g = getContactGroup(c);
 		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
index 4a615fe34..50e08acf8 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
@@ -24,7 +24,7 @@ import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.MessageTracker.GroupCount;
-import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.briarproject.briar.api.privategroup.GroupMember;
 import org.briarproject.briar.api.privategroup.GroupMessage;
 import org.briarproject.briar.api.privategroup.GroupMessageHeader;
@@ -37,7 +37,7 @@ import org.briarproject.briar.api.privategroup.Visibility;
 import org.briarproject.briar.api.privategroup.event.ContactRelationshipRevealedEvent;
 import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
 import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
-import org.briarproject.briar.client.BdfIncomingMessageHook;
+import org.briarproject.bramble.api.client.BdfIncomingMessageHook;
 
 import java.util.ArrayList;
 import java.util.Collection;
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
index d043fbad2..551b2d762 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
@@ -12,8 +12,8 @@ import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.ProtocolStateException;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupManager;
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
index b5a771970..c0205049d 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -26,7 +26,7 @@ import org.briarproject.bramble.api.sync.MessageStatus;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupManager;
@@ -588,6 +588,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	@Override
 	public void onClientVisibilityChanging(Transaction txn, Contact c,
 			Visibility v) throws DbException {
+		if(!getApplicableContactTypes().contains(c.getType())) return;
 		// Apply the client's visibility to the contact group
 		Group g = getContactGroup(c);
 		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
@@ -600,6 +601,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	private void onPrivateGroupClientVisibilityChanging(Transaction txn,
 			Contact c, Visibility client) throws DbException {
 		try {
+			if(!getApplicableContactTypes().contains(c.getType())) return;
 			Collection<Group> shareables =
 					db.getGroups(txn, PrivateGroupManager.CLIENT_ID,
 							PrivateGroupManager.MAJOR_VERSION);
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
index a0e73d9b0..efb98f7f5 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
@@ -14,8 +14,8 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.ProtocolStateException;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
index 9b7096b63..f70f8b3ec 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
@@ -13,7 +13,7 @@ import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupManager;
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
index 152ed067e..1c2dbe607 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
@@ -4,7 +4,7 @@ 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.bramble.api.client.SessionId;
 
 @NotNullByDefault
 interface SessionParser {
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
index 19a424d70..e58224cd0 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.data.BdfEntry;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogInvitationFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogInvitationFactoryImpl.java
index 0162f08de..efeca4ac1 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogInvitationFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogInvitationFactoryImpl.java
@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogInvitationRequest;
 import org.briarproject.briar.api.blog.BlogInvitationResponse;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 
 import javax.inject.Inject;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java
index c59acfd00..d98bd6c0c 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.sharing;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.forum.Forum;
 import org.briarproject.briar.api.forum.ForumInvitationRequest;
 import org.briarproject.briar.api.forum.ForumInvitationResponse;
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java
index 0a672f578..9a0bf53ec 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java
@@ -19,7 +19,7 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.briarproject.briar.api.sharing.Shareable;
 import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java
index eec97355f..768ba339d 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java
@@ -4,7 +4,7 @@ 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.bramble.api.client.SessionId;
 
 @NotNullByDefault
 interface SessionParser {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java
index ee0c06f61..d59a23ca0 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java
@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.data.BdfEntry;
 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 org.briarproject.bramble.api.client.SessionId;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
index 7b9c81867..1ef808825 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
@@ -26,7 +26,7 @@ import org.briarproject.bramble.api.sync.MessageStatus;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationMessage;
 import org.briarproject.briar.api.sharing.InvitationRequest;
 import org.briarproject.briar.api.sharing.InvitationResponse;
@@ -510,6 +510,7 @@ abstract class SharingManagerImpl<S extends Shareable>
 	@Override
 	public void onClientVisibilityChanging(Transaction txn, Contact c,
 			Visibility v) throws DbException {
+		if(!getApplicableContactTypes().contains(c.getType())) return;
 		// Apply the client's visibility to the contact group
 		Group g = getContactGroup(c);
 		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
@@ -523,6 +524,7 @@ abstract class SharingManagerImpl<S extends Shareable>
 	private void onShareableClientVisibilityChanging(Transaction txn, Contact c,
 			Visibility client) throws DbException {
 		try {
+			if(!getApplicableContactTypes().contains(c.getType())) return;
 			Collection<Group> shareables = db.getGroups(txn,
 					getShareableClientId(), getShareableMajorVersion());
 			Map<GroupId, Visibility> m = getPreferredVisibilities(txn, c);
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoIntegrationTest.java
index 81c2ae01c..d894866aa 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoIntegrationTest.java
@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.test.BrambleTestCase;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.junit.Test;
 
 import java.util.Map;
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java
index f966aa2cb..7f13a6311 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.test.BrambleMockTestCase;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.jmock.Expectations;
 import org.junit.Test;
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
index b3161af64..51953b616 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
@@ -4,6 +4,8 @@ import net.jodah.concurrentunit.Waiter;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.data.BdfDictionary;
@@ -22,8 +24,6 @@ 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.ProtocolStateException;
-import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.introduction.IntroductionManager;
 import org.briarproject.briar.api.introduction.IntroductionMessage;
 import org.briarproject.briar.api.introduction.IntroductionRequest;
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java
index 4f52aa4de..743946de8 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java
@@ -7,7 +7,7 @@ import org.briarproject.bramble.api.data.BdfEntry;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.test.ValidatorTestCase;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.jmock.Expectations;
 import org.junit.Test;
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
index a29f79445..3dc0588b5 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
@@ -15,7 +15,7 @@ import org.briarproject.bramble.api.sync.MessageFactory;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.test.BrambleTestCase;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.junit.Test;
 
 import java.util.Map;
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
index 9ddb00632..9a6f616ea 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
@@ -12,7 +12,7 @@ import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.bramble.test.BrambleTestCase;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 import org.junit.Test;
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
index 19e996e43..062695da1 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
@@ -1,7 +1,7 @@
 package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.jmock.Expectations;
 import org.junit.Test;
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java
index 66a0bdb03..35660c971 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java
@@ -3,7 +3,7 @@ package org.briarproject.briar.privategroup.invitation;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.test.TestDatabaseModule;
-import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.briarproject.briar.api.privategroup.GroupMessage;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.privategroup.PrivateGroupManager;
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
index 328347b4d..fa6d228e5 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
@@ -23,7 +23,7 @@ import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.test.BrambleMockTestCase;
 import org.briarproject.bramble.test.TestUtils;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupManager;
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
index abbb912de..70d604f5d 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.data.BdfEntry;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.briarproject.briar.api.privategroup.GroupMessage;
 import org.jmock.Expectations;
 import org.junit.Test;
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
index 05c0920c5..d45930a71 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
@@ -1,7 +1,7 @@
 package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.bramble.api.client.ProtocolStateException;
 import org.jmock.Expectations;
 import org.junit.Test;
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java
index c44c1e23f..c85618b9a 100644
--- a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java
@@ -23,7 +23,7 @@ import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogInvitationResponse;
 import org.briarproject.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.bramble.api.client.SessionId;
 import org.jmock.Expectations;
 import org.junit.Test;
 
-- 
GitLab