diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a27c5b450518e5514d01c6ea4bcb5f7ff2372285..935be91d681c9e600b9c8d5b7c163291150ccb05 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,6 +5,9 @@ stages:
   - optional_tests
   - check_reproducibility
 
+variables:
+  GIT_SUBMODULE_STRATEGY: recursive
+
 workflow:
   # when to create a CI pipeline
   rules:
@@ -32,6 +35,7 @@ test:
   extends: .base-test
   stage: test
   script:
+    - git submodule update
     - ./gradlew -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
     - ./gradlew -Djava.security.egd=file:/dev/urandom assembleOfficialDebug :briar-headless:linuxJars
     - ./gradlew -Djava.security.egd=file:/dev/urandom compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources check
@@ -109,12 +113,5 @@ mailbox integration test:
       when: manual
       allow_failure: true # TODO figure out how not to allow failure while leaving this optional
   script:
-    # start mailbox
-    - cd /opt && git clone --depth 1 https://code.briarproject.org/briar/briar-mailbox.git briar-mailbox
-    - cd briar-mailbox
-    - mkdir -p /root/.local/share # create directory that mailbox (currently) expects to exist
-    - ./gradlew run --args="--debug --setup-token 54686973206973206120736574757020746f6b656e20666f722042726961722e" &
-    # run mailbox integration test once mailbox has started
-    - cd "$CI_PROJECT_DIR"
-    - bramble-core/src/test/bash/wait-for-mailbox.sh
-    - OPTIONAL_TESTS=org.briarproject.bramble.mailbox.MailboxIntegrationTest ./gradlew --info bramble-core:test --tests MailboxIntegrationTest
+    - (cd briar-mailbox; git fetch; git reset --hard origin/mailbox-lib)
+    - MAILBOX_INTEGRATION_TESTS=true ./gradlew --info mailbox-integration-tests:test
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..10aa127867c7466e701cc1b7ef8d7784ba300291
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "briar-mailbox"]
+	path = briar-mailbox
+	url = https://code.briarproject.org/briar/briar-mailbox.git
diff --git a/.idea/runConfigurations/All_tests_in_mailbox_integration_tests.xml b/.idea/runConfigurations/All_tests_in_mailbox_integration_tests.xml
new file mode 100644
index 0000000000000000000000000000000000000000..68b02d4cb9c48cfc46f387d419552cc9ab076258
--- /dev/null
+++ b/.idea/runConfigurations/All_tests_in_mailbox_integration_tests.xml
@@ -0,0 +1,29 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="All tests in mailbox-integration-tests" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="env">
+        <map>
+          <entry key="MAILBOX_INTEGRATION_TESTS" value="true" />
+        </map>
+      </option>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value=":mailbox-integration-tests:test" />
+          <option value=":mailbox-integration-tests:cleanTest" />
+        </list>
+      </option>
+      <option name="vmOptions" value="" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java
index 3d7845799d5ac165febc4d52b84806665c276978..29509cd474046840227296e5fbb1618982dda64e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java
@@ -65,11 +65,6 @@ public class MailboxModule {
 		return mailboxSettingsManager;
 	}
 
-	@Provides
-	UrlConverter provideUrlConverter(UrlConverterImpl urlConverter) {
-		return urlConverter;
-	}
-
 	@Provides
 	MailboxApi provideMailboxApi(MailboxApiImpl mailboxApi) {
 		return mailboxApi;
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverter.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverter.java
index 264d8e664b07ba9432dbee75ece1647e9b603021..48a3884d39080aa75ab7814168959afd664423df 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverter.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverter.java
@@ -9,9 +9,9 @@ import org.briarproject.nullsafety.NotNullByDefault;
 @NotNullByDefault
 interface UrlConverter {
 
-   /**
-    * Converts a raw onion address, excluding the .onion suffix, into an
-    * HTTP URL.
-    */
-   String convertOnionToBaseUrl(String onion);
+	/**
+	 * Converts a raw onion address, excluding the .onion suffix, into an
+	 * HTTP URL.
+	 */
+	String convertOnionToBaseUrl(String onion);
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverterModule.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverterModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..cc9d4469875b2674696a0f6e56dd8d3ae32be79d
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverterModule.java
@@ -0,0 +1,13 @@
+package org.briarproject.bramble.mailbox;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class UrlConverterModule {
+
+	@Provides
+	UrlConverter provideUrlConverter(UrlConverterImpl urlConverter) {
+		return urlConverter;
+	}
+}
diff --git a/bramble-core/src/test/bash/wait-for-mailbox.sh b/bramble-core/src/test/bash/wait-for-mailbox.sh
deleted file mode 100755
index 955a53d226481b2644209d472b88fa8eac1bc14f..0000000000000000000000000000000000000000
--- a/bramble-core/src/test/bash/wait-for-mailbox.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-set -e
-
-URL="http://127.0.0.1:8000/status"
-attempt_counter=0
-max_attempts=200 # 10min - CI for mailbox currently takes ~5min
-
-echo "Waiting for mailbox to come online at $URL"
-
-until [[ "$(curl -s -o /dev/null -w '%{http_code}' $URL)" == "401" ]]; do
-  if [ ${attempt_counter} -eq ${max_attempts} ]; then
-    echo "Timed out waiting for mailbox"
-    exit 1
-  fi
-
-  printf '.'
-  attempt_counter=$((attempt_counter + 1))
-  sleep 3
-done
-
-echo "Mailbox started"
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTestComponent.java
index 97f1eeaf71e128f11948e52133fad0a42a995658..becb74db811a8b8cf2b57b0b656bf7fecb85c0ed 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTestComponent.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTestComponent.java
@@ -9,8 +9,10 @@ import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.lifecycle.IoExecutor;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 
 import java.util.concurrent.Executor;
@@ -23,8 +25,10 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 interface ContactExchangeIntegrationTestComponent
 		extends BrambleCoreIntegrationTestEagerSingletons {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java
index 6c5080dc67c413b644fda56291b5ecb417789484..59435bdb0fe248beb76a0acf6c77868aa8e0ea0a 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java
@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.plugin.file.RemovableDriveManager;
 import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
 import org.briarproject.bramble.event.DefaultEventExecutorModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
 import org.briarproject.bramble.system.TimeTravelModule;
 import org.briarproject.bramble.test.TestDatabaseConfigModule;
@@ -34,9 +35,10 @@ import dagger.Component;
 		TestMailboxDirectoryModule.class,
 		RemovableDriveIntegrationTestModule.class,
 		RemovableDriveModule.class,
+		UrlConverterModule.class,
 		TestSecureRandomModule.class,
 		TimeTravelModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
 })
 interface RemovableDriveIntegrationTestComponent
 		extends BrambleCoreEagerSingletons {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java
index 4a0a8a83af05abfebd5d2ba5afde9e68a3fab823..d6571298e3af75d67320e4bed974a848d14216a4 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java
@@ -2,8 +2,10 @@ package org.briarproject.bramble.sync;
 
 import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons;
 import org.briarproject.bramble.BrambleCoreModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 
 import javax.inject.Singleton;
@@ -14,8 +16,10 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 interface SyncIntegrationTestComponent extends
 		BrambleCoreIntegrationTestEagerSingletons {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java
index 6a2055e7e0152ae7d375109ba67eda6d416d8d50..8d4224df6ccc561d82dcc4b356e3ee09f770725b 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java
@@ -14,7 +14,6 @@ import dagger.Module;
 		TestDatabaseConfigModule.class,
 		TestFeatureFlagModule.class,
 		TestMailboxDirectoryModule.class,
-		TestPluginConfigModule.class,
 		TestSecureRandomModule.class,
 		TimeTravelModule.class
 })
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTestComponent.java
index 444da32a3175b87fc4965c584eb570c73715800a..db5ef337be3c0d30d1bf5a109aec60cfc4082acc 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTestComponent.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTestComponent.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.connection.ConnectionManager;
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 
 import javax.inject.Singleton;
 
@@ -15,8 +16,10 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 public interface BrambleIntegrationTestComponent
 		extends BrambleCoreIntegrationTestEagerSingletons {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java b/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cffa75031f69fc1c2cee13766ff6b51b7cd42ae
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java
@@ -0,0 +1,124 @@
+package org.briarproject.bramble.test;
+
+import org.briarproject.bramble.api.Pair;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.ConnectionHandler;
+import org.briarproject.bramble.api.plugin.PluginException;
+import org.briarproject.bramble.api.plugin.TorConstants;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
+import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
+import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
+
+import java.util.Collection;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
+import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
+import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
+
+@NotNullByDefault
+public class FakeTorPlugin implements DuplexPlugin {
+
+	private static final Logger LOG =
+			getLogger(FakeTorPlugin.class.getName());
+
+	private State state = INACTIVE;
+
+	@Override
+	public TransportId getId() {
+		return TorConstants.ID;
+	}
+
+	@Override
+	public long getMaxLatency() {
+		return 0;
+	}
+
+	@Override
+	public int getMaxIdleTime() {
+		return 0;
+	}
+
+	@Override
+	public void start() throws PluginException {
+		LOG.info("Starting plugin");
+		state = ACTIVE;
+	}
+
+	@Override
+	public void stop() throws PluginException {
+		LOG.info("Stopping plugin");
+		state = DISABLED;
+	}
+
+	@Override
+	public State getState() {
+		return state;
+	}
+
+	@Override
+	public int getReasonsDisabled() {
+		return 0;
+	}
+
+	@Override
+	public boolean shouldPoll() {
+		return false;
+	}
+
+	@Override
+	public int getPollingInterval() {
+		return 0;
+	}
+
+	@Override
+	public void poll(
+			Collection<Pair<TransportProperties, ConnectionHandler>> properties) {
+		// no-op
+	}
+
+	@Nullable
+	@Override
+	public DuplexTransportConnection createConnection(TransportProperties p) {
+		return null;
+	}
+
+	@Override
+	public boolean supportsKeyAgreement() {
+		return false;
+	}
+
+	@Nullable
+	@Override
+	public KeyAgreementListener createKeyAgreementListener(
+			byte[] localCommitment) {
+		return null;
+	}
+
+	@Nullable
+	@Override
+	public DuplexTransportConnection createKeyAgreementConnection(
+			byte[] remoteCommitment, BdfList descriptor) {
+		return null;
+	}
+
+	@Override
+	public boolean supportsRendezvous() {
+		return false;
+	}
+
+	@Nullable
+	@Override
+	public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
+			boolean alice, ConnectionHandler incoming) {
+		return null;
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginConfigModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginConfigModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..e65b39b0531a80ef5419e38dac9427c2e34ec17f
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginConfigModule.java
@@ -0,0 +1,51 @@
+package org.briarproject.bramble.test;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.PluginConfig;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
+import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+
+@Module
+public class FakeTorPluginConfigModule {
+
+	@Provides
+	PluginConfig providePluginConfig(FakeTorPluginFactory tor) {
+		@NotNullByDefault
+		PluginConfig pluginConfig = new PluginConfig() {
+
+			@Override
+			public Collection<DuplexPluginFactory> getDuplexFactories() {
+				return singletonList(tor);
+			}
+
+			@Override
+			public Collection<SimplexPluginFactory> getSimplexFactories() {
+				return emptyList();
+			}
+
+			@Override
+			public boolean shouldPoll() {
+				return false;
+			}
+
+			@Override
+			public Map<TransportId, List<TransportId>> getTransportPreferences() {
+				return emptyMap();
+			}
+
+		};
+		return pluginConfig;
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java b/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..70123aa5edb13f5a157f63cf5756d8f5ecde4ef7
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java
@@ -0,0 +1,37 @@
+package org.briarproject.bramble.test;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.PluginCallback;
+import org.briarproject.bramble.api.plugin.TorConstants;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
+import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+@NotNullByDefault
+public class FakeTorPluginFactory implements DuplexPluginFactory {
+
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
+
+	@Inject
+	FakeTorPluginFactory() {
+	}
+
+	@Override
+	public TransportId getId() {
+		return TorConstants.ID;
+	}
+
+	@Override
+	public long getMaxLatency() {
+		return MAX_LATENCY;
+	}
+
+	@Nullable
+	@Override
+	public DuplexPlugin createPlugin(PluginCallback callback) {
+		return new FakeTorPlugin();
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementTestComponent.java
index a54852b17f96ad8264e858266f6a0fc4669e6c60..88a4747a2137d4479eb904e905c9a5e8fd835452 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementTestComponent.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementTestComponent.java
@@ -7,9 +7,11 @@ import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.transport.KeyManager;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 
 import javax.inject.Singleton;
@@ -20,8 +22,10 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 interface TransportKeyAgreementTestComponent
 		extends BrambleIntegrationTestComponent {
diff --git a/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java b/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
index cdb764909a31737a219e1e94f7da8d4f1ee9467e..001c44d634f367ec42a3b0dd13abf417a39d24b0 100644
--- a/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
+++ b/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
@@ -1,6 +1,7 @@
 package org.briarproject.bramble;
 
 import org.briarproject.bramble.io.DnsModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.network.JavaNetworkModule;
 import org.briarproject.bramble.plugin.tor.CircumventionModule;
 import org.briarproject.bramble.socks.SocksModule;
@@ -13,6 +14,7 @@ import dagger.Module;
 		DnsModule.class,
 		JavaNetworkModule.class,
 		JavaSystemModule.class,
+		UrlConverterModule.class,
 		SocksModule.class
 })
 public class BrambleJavaModule {
diff --git a/bramble-java/src/test/java/org/briarproject/bramble/test/BrambleJavaIntegrationTestComponent.java b/bramble-java/src/test/java/org/briarproject/bramble/test/BrambleJavaIntegrationTestComponent.java
index 857f408e602f4757a09bc70828313b74cf7f3dac..8003bb7e55dae1743be26e4b2bedf518b6395855 100644
--- a/bramble-java/src/test/java/org/briarproject/bramble/test/BrambleJavaIntegrationTestComponent.java
+++ b/bramble-java/src/test/java/org/briarproject/bramble/test/BrambleJavaIntegrationTestComponent.java
@@ -3,6 +3,7 @@ package org.briarproject.bramble.test;
 import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons;
 import org.briarproject.bramble.BrambleCoreModule;
 import org.briarproject.bramble.BrambleJavaModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.plugin.tor.BridgeTest;
 import org.briarproject.bramble.plugin.tor.CircumventionProvider;
 
@@ -15,7 +16,9 @@ import dagger.Component;
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
 		BrambleJavaModule.class,
-		TestTorPortsModule.class
+		UrlConverterModule.class,
+		TestTorPortsModule.class,
+		TestPluginConfigModule.class,
 })
 public interface BrambleJavaIntegrationTestComponent
 		extends BrambleCoreIntegrationTestEagerSingletons {
diff --git a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
index 99c25220480cb92f8611ef1ca715d5a53c2fb52a..6216e241a2a846ebbff311eac68ab30b4d073336 100644
--- a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
+++ b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
 import org.briarproject.bramble.BrambleAndroidModule;
 import org.briarproject.bramble.BrambleCoreModule;
 import org.briarproject.bramble.account.BriarAccountModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.plugin.file.RemovableDriveModule;
 import org.briarproject.bramble.system.ClockModule;
 import org.briarproject.briar.BriarCoreModule;
@@ -26,7 +27,8 @@ import dagger.Component;
 		BriarCoreModule.class,
 		BrambleAndroidModule.class,
 		BriarAccountModule.class,
-		BrambleCoreModule.class
+		BrambleCoreModule.class,
+		UrlConverterModule.class
 })
 public interface BriarUiTestComponent extends AndroidComponent {
 
diff --git a/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java
index 0427f2c7f7d62efb406faa89bea3d2f05f36d179..a12e480d23d59457201b785aa2b95339fc093b12 100644
--- a/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java
+++ b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
 import org.briarproject.bramble.BrambleAndroidModule;
 import org.briarproject.bramble.BrambleCoreModule;
 import org.briarproject.bramble.account.BriarAccountModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.plugin.file.RemovableDriveModule;
 import org.briarproject.bramble.system.ClockModule;
 import org.briarproject.briar.BriarCoreModule;
@@ -25,7 +26,8 @@ import dagger.Component;
 		BriarCoreModule.class,
 		BrambleAndroidModule.class,
 		BriarAccountModule.class,
-		BrambleCoreModule.class
+		BrambleCoreModule.class,
+		UrlConverterModule.class
 })
 public interface BriarUiTestComponent extends AndroidComponent {
 
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
index 340160ddcb70f47246578b28260adb5b64adebc0..c1ec805a6220505c598174473449b3c3034f4d3f 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
@@ -28,6 +28,7 @@ import org.briarproject.bramble.api.system.AndroidExecutor;
 import org.briarproject.bramble.api.system.AndroidWakeLockManager;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.api.system.LocationUtils;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.plugin.file.RemovableDriveModule;
 import org.briarproject.bramble.plugin.tor.CircumventionProvider;
 import org.briarproject.bramble.system.ClockModule;
@@ -101,6 +102,7 @@ import dagger.Component;
 		AttachmentModule.class,
 		ClockModule.class,
 		MediaModule.class,
+		UrlConverterModule.class,
 		RemovableDriveModule.class
 })
 public interface AndroidComponent
diff --git a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java
index 8cdaa7bb31a1047304c17889d218bf73fa676dda..fe4afa241cfac6197354ce131680a0d4a4127cbd 100644
--- a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java
@@ -4,8 +4,10 @@ import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons;
 import org.briarproject.bramble.BrambleCoreModule;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 import org.briarproject.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.feed.FeedManager;
@@ -27,8 +29,10 @@ import dagger.Component;
 		BriarClientModule.class,
 		FeedModule.class,
 		IdentityModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 interface FeedManagerIntegrationTestComponent
 		extends BrambleCoreIntegrationTestEagerSingletons {
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
index ec856d55323e8c0c72a921968fbbaa5ddd8ec660..69f7bdc4d1a253453bee24725a9b61d40ef3b364 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
@@ -1,8 +1,10 @@
 package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.BrambleCoreModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 import org.briarproject.briar.attachment.AttachmentModule;
 import org.briarproject.briar.autodelete.AutoDeleteModule;
@@ -39,8 +41,10 @@ import dagger.Component;
 		MessagingModule.class,
 		PrivateGroupModule.class,
 		SharingModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 interface IntroductionIntegrationTestComponent
 		extends BriarIntegrationTestComponent {
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java
index e84f80afe3e8f58ffdcbfa8b8b963efbc1fbe464..6b4083adef945972385f5f658da36410c5a40bd1 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java
@@ -2,8 +2,10 @@ package org.briarproject.briar.messaging;
 
 import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons;
 import org.briarproject.bramble.BrambleCoreModule;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 import org.briarproject.briar.autodelete.AutoDeleteModule;
 import org.briarproject.briar.avatar.AvatarModule;
@@ -27,8 +29,10 @@ import dagger.Component;
 		ForumModule.class,
 		IdentityModule.class,
 		MessagingModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 interface MessageSizeIntegrationTestComponent
 		extends BrambleCoreIntegrationTestEagerSingletons {
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
index 511946edf2c9d807dca05ff0230a338beb94505d..e3498608dd18b0127640266f772ffb68d4fb26a0 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
@@ -7,8 +7,10 @@ import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 import org.briarproject.briar.api.messaging.MessagingManager;
 import org.briarproject.briar.api.messaging.PrivateMessageFactory;
@@ -28,8 +30,10 @@ import dagger.Component;
 		BriarClientModule.class,
 		ConversationModule.class,
 		MessagingModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 interface SimplexMessagingIntegrationTestComponent
 		extends BrambleCoreIntegrationTestEagerSingletons {
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
index 494961d9b322698b77ec535338e863e6cda62358..a2486c3470255e8f2b7993f69912e990f47932e0 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
@@ -8,9 +8,11 @@ import org.briarproject.bramble.api.identity.AuthorFactory;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.mailbox.UrlConverterModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
 import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSocksModule;
 import org.briarproject.bramble.test.TimeTravel;
 import org.briarproject.briar.api.attachment.AttachmentReader;
@@ -64,8 +66,10 @@ import dagger.Component;
 		MessagingModule.class,
 		PrivateGroupModule.class,
 		SharingModule.class,
+		UrlConverterModule.class,
 		TestDnsModule.class,
-		TestSocksModule.class
+		TestSocksModule.class,
+		TestPluginConfigModule.class,
 })
 public interface BriarIntegrationTestComponent
 		extends BrambleIntegrationTestComponent {
diff --git a/briar-headless/build.gradle b/briar-headless/build.gradle
index 501b67872f6454692c0fea90b6bd478a48aa1f15..efff4722b08b349bc1403c3f02eba69b5a244caf 100644
--- a/briar-headless/build.gradle
+++ b/briar-headless/build.gradle
@@ -7,8 +7,8 @@ import static java.util.Collections.list
 plugins {
 	id 'java'
 	id 'idea'
-	id 'org.jetbrains.kotlin.jvm' version '1.4.32'
-	id 'org.jetbrains.kotlin.kapt' version '1.4.32'
+	id 'org.jetbrains.kotlin.jvm'
+	id 'org.jetbrains.kotlin.kapt'
 	id 'witness'
 }
 apply from: 'witness.gradle'
@@ -20,7 +20,7 @@ dependencies {
 	implementation project(path: ':briar-core', configuration: 'default')
 	implementation project(path: ':bramble-java', configuration: 'default')
 
-	implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32'
+	implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10'
 	implementation 'io.javalin:javalin:3.5.0'
 	implementation 'org.slf4j:slf4j-simple:1.7.30'
 	implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
@@ -36,7 +36,7 @@ dependencies {
 	def junitVersion = '5.5.2'
 	testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
 	testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
-	testRuntime "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
+	testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
 	testImplementation 'io.mockk:mockk:1.10.4'
 	testImplementation 'org.skyscreamer:jsonassert:1.5.0'
 	testImplementation 'khttp:khttp:0.1.0'
@@ -77,7 +77,7 @@ void jarFactory(Jar jarTask, jarArchitecture) {
 	jarTask.with jar
 	jarTask.doLast {
 		// Rename the original jar
-		File jar = jarTask.archivePath
+		File jar = jarTask.archiveFile.get().asFile
 		String srcPath = jar.toString().replaceFirst('\\.jar$', '.unsorted.jar')
 		File srcFile = new File(srcPath)
 		jar.renameTo(srcFile)
diff --git a/briar-headless/witness.gradle b/briar-headless/witness.gradle
index 5935eb0daf6828270f35276dface853210f21ddf..17f1b91d381e8e4d33ec440613ca39681320a7bd 100644
--- a/briar-headless/witness.gradle
+++ b/briar-headless/witness.gradle
@@ -4,9 +4,7 @@ dependencyVerification {
         'com.fasterxml.jackson.core:jackson-core:2.13.0:jackson-core-2.13.0.jar:348bc59b348df2e807b356f1d62d2afb41a974073328abc773eb0932b855d2c8',
         'com.fasterxml.jackson.core:jackson-databind:2.13.0:jackson-databind-2.13.0.jar:9c826d27176268777adcf97e1c6e2051c7e33a7aaa2c370c2e8c6077fd9da3f4',
         'com.github.ajalt:clikt:2.2.0:clikt-2.2.0.jar:beb3136d06764ec8ce0810a8fd6c8b7b49d04287d1deef3a07c016e43a458d33',
-        'com.github.gundy:semver4j:0.16.4:semver4j-0.16.4.jar:def9b4225fa37219e18f81d01f0e52d73dca1257a38f5475be9dd58f87736510',
         'com.google.code.findbugs:jsr305:3.0.2:jsr305-3.0.2.jar:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7',
-        'com.google.code.gson:gson:2.8.6:gson-2.8.6.jar:c8fb4839054d280b3033f800d1f5a97de2f028eb8ba2eb458ad287e536f3f25f',
         'com.google.dagger:dagger-compiler:2.24:dagger-compiler-2.24.jar:3c5afb955fb188da485cb2c048eff37dce0e1530b9780a0f2f7187d16d1ccc1f',
         'com.google.dagger:dagger-producers:2.24:dagger-producers-2.24.jar:f10f45b95191954d5d6b043fca9e62fb621d21bf70634b8f8476c7988b504c3a',
         'com.google.dagger:dagger-spi:2.24:dagger-spi-2.24.jar:c038445d14dbcb4054e61bf49e05009edf26fce4fdc7ec1a9db544784f68e718',
@@ -20,7 +18,6 @@ dependencyVerification {
         'com.google.j2objc:j2objc-annotations:1.1:j2objc-annotations-1.1.jar:2994a7eb78f2710bd3d3bfb639b2c94e219cedac0d4d084d516e78c16dddecf6',
         'com.squareup:javapoet:1.11.1:javapoet-1.11.1.jar:9cbf2107be499ec6e95afd36b58e3ca122a24166cdd375732e51267d64058e90',
         'com.vaadin.external.google:android-json:0.0.20131108.vaadin1:android-json-0.0.20131108.vaadin1.jar:dfb7bae2f404cfe0b72b4d23944698cb716b7665171812a0a4d0f5926c0fac79',
-        'de.undercouch:gradle-download-task:4.0.2:gradle-download-task-4.0.2.jar:952cbfcc5f21beeccb5925cc5ba648af09839258441dd44d087d64a57d34e87a',
         'io.javalin:javalin:3.5.0:javalin-3.5.0.jar:6618f99ad4c241eefcaf3a02c85adc52ec346c9710e8eb5a3f1a916e3d7acec4',
         'io.mockk:mockk-agent-api:1.10.4:mockk-agent-api-1.10.4.jar:8deb59189b48d5870a746f954ca681424040544812c7ae295f3bef87a9499cfe',
         'io.mockk:mockk-agent-common:1.10.4:mockk-agent-common-1.10.4.jar:13b81a3297a3c15ed9f62b838aaede20347018f07c30cad2ca74a4dd99786f8f',
@@ -35,8 +32,8 @@ dependencyVerification {
         'khttp:khttp:0.1.0:khttp-0.1.0.jar:48ab3bd22e461f2c2e74e3446d8f9568e24aab157f61fdc85ded6c0bfbe9a926',
         'net.bytebuddy:byte-buddy-agent:1.10.14:byte-buddy-agent-1.10.14.jar:30272167eceb1cb68fa84730a12d1abfd1daed6ae0c19fdefee47a9a9a0cfd33',
         'net.bytebuddy:byte-buddy:1.10.14:byte-buddy-1.10.14.jar:0e6b935bfcb3e451d525956acad53ec86ff916d714abdbd32b3d2039771896f8',
+        'net.java.dev.jna:jna:5.6.0:jna-5.6.0.jar:5557e235a8aa2f9766d5dc609d67948f2a8832c2d796cea9ef1d6cbe0b3b7eaf',
         'net.ltgt.gradle.incap:incap:0.2:incap-0.2.jar:b625b9806b0f1e4bc7a2e3457119488de3cd57ea20feedd513db070a573a4ffd',
-        'org.antlr:antlr4-runtime:4.5.2-1:antlr4-runtime-4.5.2-1.jar:e831413004bceed7d915c3a175927b1daabc4974b7b8a6f87bbce886d3550398',
         'org.apiguardian:apiguardian-api:1.1.0:apiguardian-api-1.1.0.jar:a9aae9ff8ae3e17a2a18f79175e82b16267c246fbbd3ca9dfbbb290b08dcfdd4',
         'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
         'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
@@ -55,32 +52,22 @@ dependencyVerification {
         'org.eclipse.jetty:jetty-util:9.4.20.v20190813:jetty-util-9.4.20.v20190813.jar:5816ef44f73e76b8ef1c1ea848cc34c7b1f24771f3675353e2ef23eb920121d8',
         'org.eclipse.jetty:jetty-webapp:9.4.20.v20190813:jetty-webapp-9.4.20.v20190813.jar:59d9b5f238acb14eac3bf90f755eeabd9fc16c630217d0e7e01b99a38194036c',
         'org.eclipse.jetty:jetty-xml:9.4.20.v20190813:jetty-xml-9.4.20.v20190813.jar:f4411ad9998e4cc202c849bb9b9e93aa2aa761b89a27cc746ca025849d659fd0',
-        'org.jetbrains.intellij.deps:trove4j:1.0.20181211:trove4j-1.0.20181211.jar:affb7c85a3c87bdcf69ff1dbb84de11f63dc931293934bc08cd7ab18de083601',
-        'org.jetbrains.kotlin:kotlin-android-extensions:1.4.32:kotlin-android-extensions-1.4.32.jar:be4dcefa4274c9c93703fec984e53d19cac9b9c95e3567247aa0257267266529',
-        'org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.4.32:kotlin-annotation-processing-gradle-1.4.32.jar:0ef86e325c44cb7476b862e3319226cb85852b2dc9f37a545e856b617ded1691',
-        'org.jetbrains.kotlin:kotlin-build-common:1.4.32:kotlin-build-common-1.4.32.jar:d8c1fab9ff7dfdb385fc0789da5f2574114926897060fcf7cc6d93207ae88ee4',
-        'org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.32:kotlin-compiler-embeddable-1.4.32.jar:083d80ea6262faac293d248c32bf89e062a4e44d657ea6a095c8066e31791e5e',
-        'org.jetbrains.kotlin:kotlin-compiler-runner:1.4.32:kotlin-compiler-runner-1.4.32.jar:9f668c4033b8c28eed076f39ad93749911d01671e887369a86fc2a9ed5cb2bc3',
-        'org.jetbrains.kotlin:kotlin-daemon-client:1.4.32:kotlin-daemon-client-1.4.32.jar:4c77d463ba41fb43f9e8a7868fc99712431e8f6b3b8df24aa7df3e5778863a6c',
-        'org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.32:kotlin-daemon-embeddable-1.4.32.jar:0c52722dfb15d6c79f77e1c1c55caf93d0a480f9e1ee76da751cf0cc1e4b6d19',
-        'org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.4.32:kotlin-gradle-plugin-api-1.4.32.jar:d0655390868ebade8b30a36607f30b0031c898f7f433d3ea5ff8426a9afa056b',
-        'org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.4.32:kotlin-gradle-plugin-model-1.4.32.jar:628b5abe97e47fa8d1bf38e5e58be600f720084a871e8f77d9713a895d0e3b40',
-        'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32:kotlin-gradle-plugin-1.4.32.jar:369d6c3636d74e1328a12a689adbf76cc16bcc11cf9d594dda2e4b0952068ad8',
-        'org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.4.32:kotlin-klib-commonizer-embeddable-1.4.32.jar:6e38c9c7dc14c2913a67f1690ccb1efb9bb2d1fe211a5628c9470195cc6e4edf',
+        'org.jetbrains.intellij.deps:trove4j:1.0.20200330:trove4j-1.0.20200330.jar:c5fd725bffab51846bf3c77db1383c60aaaebfe1b7fe2f00d23fe1b7df0a439d',
+        'org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.7.10:kotlin-annotation-processing-gradle-1.7.10.jar:5351105490f668a4582966ee149ccd5eaf286f7a0b4cf7a301268b8b8910dfd1',
+        'org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10:kotlin-compiler-embeddable-1.7.10.jar:470ba8941794f818a34b0a8f387ee27e44268e95a108322d18d9749ae345e22b',
+        'org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10:kotlin-daemon-embeddable-1.7.10.jar:77c5f3ab1ed653a899e96835937a9daf3a46e496fdfa6915ff6d20b2953619a4',
+        'org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10:kotlin-klib-commonizer-embeddable-1.7.10.jar:1c2550f1e7ec4d1590aacddd7852b90a4cf05de6e66cee31ad747c8dc0834e33',
         'org.jetbrains.kotlin:kotlin-reflect:1.4.20:kotlin-reflect-1.4.20.jar:3b7c82def79fb96c4579d40a47e37dec872f9f8209ee0da3ce828c39dba612e1',
-        'org.jetbrains.kotlin:kotlin-reflect:1.4.32:kotlin-reflect-1.4.32.jar:dbf19e9cdaa9c3c170f3f6f6ce3922f38dfc1d7fa1cab5b7c23a19da8b5eec5b',
-        'org.jetbrains.kotlin:kotlin-script-runtime:1.4.32:kotlin-script-runtime-1.4.32.jar:4496e90565b6cc312213acd65fe8ad6d149264ff12d2f1f6b6ba4122afffbbfe',
-        'org.jetbrains.kotlin:kotlin-scripting-common:1.4.32:kotlin-scripting-common-1.4.32.jar:58705f21ba97f2d2e8b818d3c8167252e2b210a610e5678b008bc779f3745112',
-        'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.4.32:kotlin-scripting-compiler-embeddable-1.4.32.jar:cc4db11fd2ca73250a30e42d6783973aae13b1e3e71520273d4c1354262ee384',
-        'org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.4.32:kotlin-scripting-compiler-impl-embeddable-1.4.32.jar:66940ccb8c5e182d7d2ac47f0dfeccc224c4deea077361cf3935c4e0460d70ad',
-        'org.jetbrains.kotlin:kotlin-scripting-jvm:1.4.32:kotlin-scripting-jvm-1.4.32.jar:d2ccd108b7d68bf38657487114bd54c95deae375ee959f9e7805c59eb037fb98',
-        'org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32:kotlin-stdlib-common-1.4.32.jar:e1ff6f55ee9e7591dcc633f7757bac25a7edb1cc7f738b37ec652f10f66a4145',
-        'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.32:kotlin-stdlib-jdk7-1.4.32.jar:5f801e75ca27d8791c14b07943c608da27620d910a8093022af57f543d5d98b6',
-        'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32:kotlin-stdlib-jdk8-1.4.32.jar:adc43e54757b106e0cd7b3b7aa257dff471b61efdabe067fc02b2f57e2396262',
-        'org.jetbrains.kotlin:kotlin-stdlib:1.4.32:kotlin-stdlib-1.4.32.jar:13e9fd3e69dc7230ce0fc873a92a4e5d521d179bcf1bef75a6705baac3bfecba',
-        'org.jetbrains.kotlin:kotlin-util-io:1.4.32:kotlin-util-io-1.4.32.jar:d8b33d8840ff755e686d41b0fa3a27272849a2ac8242554606e8d66462bc607f',
-        'org.jetbrains.kotlin:kotlin-util-klib:1.4.32:kotlin-util-klib-1.4.32.jar:4a80f7a521f70a87798e74416b596336c76d8306594172a4cf142c16e1720081',
-        'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8:kotlinx-coroutines-core-1.3.8.jar:f8c8b7485d4a575e38e5e94945539d1d4eccd3228a199e1a9aa094e8c26174ee',
+        'org.jetbrains.kotlin:kotlin-reflect:1.7.10:kotlin-reflect-1.7.10.jar:187c5e5a588a6ed18c3a41b54df138a5944121bdb396be1c3fa4abee67397955',
+        'org.jetbrains.kotlin:kotlin-script-runtime:1.7.10:kotlin-script-runtime-1.7.10.jar:84bfc2aa4eec6768113930cdaef8b5b9f59ac4138fbca3b11300fff4d076950c',
+        'org.jetbrains.kotlin:kotlin-scripting-common:1.7.10:kotlin-scripting-common-1.7.10.jar:c3a346f38a3d6e242f2316c5a7a4a6b526f2cc42b44ebd40654a0f885cbc4940',
+        'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10:kotlin-scripting-compiler-embeddable-1.7.10.jar:fcb8a0b3b7a95263dab8a0ccdd34fed02888700511eabb5613f75a007a4aa802',
+        'org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10:kotlin-scripting-compiler-impl-embeddable-1.7.10.jar:7119205985ebd721179fb0f35d1d511f96de14fbd48e6465119fcac6bffc8090',
+        'org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10:kotlin-scripting-jvm-1.7.10.jar:cf85511ce4e26fa3286d722f95ed54f16f2513a39ce3b85f2b567e575cb45a60',
+        'org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10:kotlin-stdlib-common-1.7.10.jar:19f102efe9629f8eabc63853ad15c533e47c47f91fca09285c5bde86e59f91d4',
+        'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10:kotlin-stdlib-jdk7-1.7.10.jar:54f61351b1936ad88f4e53059fe781e723eae51d78ed9e7422d8b403574ec682',
+        'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10:kotlin-stdlib-jdk8-1.7.10.jar:8aafdd60c94f454c92e5066d266a5ed53ecc63c78f623b3fd9db56fea4032873',
+        'org.jetbrains.kotlin:kotlin-stdlib:1.7.10:kotlin-stdlib-1.7.10.jar:e771fe74250a943e8f6346713201ff1d8cb95c3a5d1a91a22b65a9e04f6a8901',
         'org.jetbrains:annotations:13.0:annotations-13.0.jar:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478',
         'org.json:json:20150729:json-20150729.jar:38c21b9c3d6d24919cd15d027d20afab0a019ac9205f7ed9083b32bdd42a2353',
         'org.junit.jupiter:junit-jupiter-api:5.5.2:junit-jupiter-api-5.5.2.jar:249a2fdbd3931987c0298d00ca08ed248496e0fc11e0463c08c4f82e0cc79b1c',
diff --git a/briar-mailbox b/briar-mailbox
new file mode 160000
index 0000000000000000000000000000000000000000..d887c49ab342d1d48d79e46fa6632852c4ab40b7
--- /dev/null
+++ b/briar-mailbox
@@ -0,0 +1 @@
+Subproject commit d887c49ab342d1d48d79e46fa6632852c4ab40b7
diff --git a/build.gradle b/build.gradle
index f0de0c7809488bbe48c6df2c6fd774ee674e0480..4059874cb22439ded02d4e4d4772bbc524cb667c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -28,12 +28,8 @@ buildscript {
 		}
 	}
 
-	dependencies {
-		classpath 'com.android.tools.build:gradle:7.0.3'
-		classpath 'ru.vyarus:gradle-animalsniffer-plugin:1.5.3'
-		classpath files('libs/gradle-witness.jar')
-	}
 	ext {
+		kotlin_version = '1.7.10'
 		dagger_version = "2.33"
 		// okhttp 3.12.x is supported until end of 2021, newer versions need minSdk 21
 		okhttp_version = "3.12.13"
@@ -44,4 +40,17 @@ buildscript {
 		junit_version = "4.13.2"
 		jmock_version = '2.12.0'
 	}
+	dependencies {
+		classpath 'com.android.tools.build:gradle:7.0.3'
+		classpath 'ru.vyarus:gradle-animalsniffer-plugin:1.5.3'
+		classpath files('libs/gradle-witness.jar')
+		classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+	}
+}
+
+if ((project.hasProperty("briar.mailbox_integration_tests") && project.property("briar.mailbox_integration_tests") == "true")
+		|| System.env.MAILBOX_INTEGRATION_TESTS) {
+	configure([project(':mailbox-core'), project(':mailbox-lib')]) {
+		apply from: "../gradle/variables.gradle"
+	}
 }
diff --git a/gradle.properties b/gradle.properties
index 00fc6b9063375e3849d4f52712cd68313a66c6e4..54044786c8733ba607656e11ae39ae809a815917 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,5 @@
 noWitness=androidApis,_internal_aapt2_binary
 org.gradle.jvmargs=-Xmx1g
 android.useAndroidX=true
-android.enableJetifier=true
\ No newline at end of file
+android.enableJetifier=true
+briar.mailbox_integration_tests=false
diff --git a/mailbox-integration-tests/.gitignore b/mailbox-integration-tests/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..217c03c15290909413558c6b2453ab0718e96d8c
--- /dev/null
+++ b/mailbox-integration-tests/.gitignore
@@ -0,0 +1,4 @@
+bin
+build
+test.tmp
+.settings
diff --git a/mailbox-integration-tests/build.gradle b/mailbox-integration-tests/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..cd2f67fde05be2bb016316dd4494e1d4d9b5e5b1
--- /dev/null
+++ b/mailbox-integration-tests/build.gradle
@@ -0,0 +1,20 @@
+apply plugin: 'java-library'
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+apply plugin: 'idea'
+apply from: '../dagger.gradle'
+
+dependencies {
+	testImplementation project(path: ':bramble-api', configuration: 'default')
+	testImplementation project(path: ':bramble-core', configuration: 'default')
+	testImplementation project(path: ':mailbox-core', configuration: 'default')
+	testImplementation project(path: ':mailbox-lib', configuration: 'default')
+	testImplementation project(path: ':bramble-api', configuration: 'testOutput')
+	testImplementation project(path: ':bramble-core', configuration: 'testOutput')
+
+	testImplementation "junit:junit:$junit_version"
+	testImplementation "ch.qos.logback:logback-classic:1.2.11"
+
+	testAnnotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxApiIntegrationTest.java
similarity index 79%
rename from bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java
rename to mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxApiIntegrationTest.java
index f1b88946d9e37cd56deef01ed4af239e0da182d8..e6ab681216f9f8cbdbcb0d59435953363c776701 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxApiIntegrationTest.java
@@ -1,6 +1,5 @@
 package org.briarproject.bramble.mailbox;
 
-import org.briarproject.bramble.api.WeakSingletonProvider;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
 import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
@@ -12,9 +11,9 @@ import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
 import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
 import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
 import org.briarproject.bramble.test.BrambleTestCase;
-import org.junit.AfterClass;
+import org.briarproject.mailbox.lib.TestMailbox;
+import org.junit.After;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -25,92 +24,65 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-import javax.annotation.Nonnull;
-import javax.net.SocketFactory;
-
-import okhttp3.OkHttpClient;
-
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.URL_BASE;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createMailboxApi;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
-import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
 import static org.briarproject.bramble.test.TestUtils.readBytes;
 import static org.briarproject.bramble.test.TestUtils.writeBytes;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
 
-public class MailboxIntegrationTest extends BrambleTestCase {
+public class MailboxApiIntegrationTest extends BrambleTestCase {
 
 	@Rule
 	public TemporaryFolder folder = new TemporaryFolder();
 
-	private static final String URL_BASE = "http://127.0.0.1:8000";
-	private static final MailboxAuthToken SETUP_TOKEN;
+	@Rule
+	public TemporaryFolder dataDirectory = new TemporaryFolder();
 
-	static {
-		try {
-			SETUP_TOKEN = MailboxAuthToken.fromString(
-					"54686973206973206120736574757020746f6b656e20666f722042726961722e");
-		} catch (InvalidMailboxIdException e) {
-			throw new IllegalStateException();
-		}
-	}
+	private TestMailbox mailbox;
+	private MailboxAuthToken setupToken;
+
+	private final MailboxApi api = createMailboxApi();
+
+	private MailboxProperties ownerProperties;
 
-	private static final OkHttpClient client = new OkHttpClient.Builder()
-			.socketFactory(SocketFactory.getDefault())
-			.connectTimeout(60_000, MILLISECONDS)
-			.build();
-	private static final WeakSingletonProvider<OkHttpClient>
-			httpClientProvider =
-			new WeakSingletonProvider<OkHttpClient>() {
-				@Override
-				@Nonnull
-				public OkHttpClient createInstance() {
-					return client;
-				}
-			};
-	// We aren't using a real onion address, so use the given address verbatim
-	private static final UrlConverter urlConverter = onion -> onion;
-	private static final MailboxApiImpl api =
-			new MailboxApiImpl(httpClientProvider, urlConverter);
-	// needs to be static to keep values across different tests
-	private static MailboxProperties ownerProperties;
-
-	/**
-	 * Called before each test to make sure the mailbox is setup once
-	 * before starting with individual tests.
-	 * {@link BeforeClass} needs to be static, so we can't use the API class.
-	 */
 	@Before
-	public void ensureSetup() throws IOException, ApiException {
-		// Skip this test unless it's explicitly enabled in the environment
-		assumeTrue(isOptionalTestEnabled(MailboxIntegrationTest.class));
+	public void setUp()
+			throws IOException, ApiException, InvalidMailboxIdException {
+		mailbox = new TestMailbox(dataDirectory.getRoot());
+		mailbox.startLifecycle();
+
+		setupToken = MailboxAuthToken.fromString(mailbox.getSetupToken());
 
-		if (ownerProperties != null) return;
+		assertNull(ownerProperties);
 		MailboxProperties setupProperties = new MailboxProperties(
-				URL_BASE, SETUP_TOKEN, new ArrayList<>());
+				URL_BASE, setupToken, new ArrayList<>());
 		ownerProperties = api.setup(setupProperties);
 	}
 
-	@AfterClass
-	// we can't test wiping as a regular test as it stops the mailbox
-	public static void wipe() throws IOException, ApiException {
-		if (!isOptionalTestEnabled(MailboxIntegrationTest.class)) return;
+	@After
+	public void tearDown() {
+		mailbox.stopLifecycle(true);
+	}
 
+	@Test
+	public void wipe() throws IOException, ApiException {
 		api.wipeMailbox(ownerProperties);
 
 		// check doesn't work anymore
-		assertThrows(ApiException.class, () ->
-				api.checkStatus(ownerProperties));
+		assertThrows(ApiException.class,
+				() -> api.checkStatus(ownerProperties));
 
 		// new setup doesn't work as mailbox is stopping
 		MailboxProperties setupProperties = new MailboxProperties(
-				URL_BASE, SETUP_TOKEN, new ArrayList<>());
+				URL_BASE, setupToken, new ArrayList<>());
 		assertThrows(ApiException.class, () -> api.setup(setupProperties));
 	}
 
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTestComponent.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTestComponent.java
new file mode 100644
index 0000000000000000000000000000000000000000..a4fc1adb57bc559f8e0c3eca12e291964bc03743
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTestComponent.java
@@ -0,0 +1,56 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.BrambleCoreModule;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.identity.AuthorFactory;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.mailbox.MailboxManager;
+import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
+import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.TestUrlConverterModule;
+import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
+import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
+import org.briarproject.bramble.test.FakeTorPluginConfigModule;
+import org.briarproject.bramble.test.TestDnsModule;
+import org.briarproject.bramble.test.TestSocksModule;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+@Singleton
+@Component(modules = {
+		BrambleCoreIntegrationTestModule.class,
+		BrambleCoreModule.class,
+		TestUrlConverterModule.class,
+		FakeTorPluginConfigModule.class,
+		TestSocksModule.class,
+		TestDnsModule.class,
+})
+interface MailboxIntegrationTestComponent extends
+		BrambleIntegrationTestComponent {
+
+	DatabaseComponent getDatabaseComponent();
+
+	MailboxManager getMailboxManager();
+
+	MailboxUpdateManager getMailboxUpdateManager();
+
+	MailboxSettingsManager getMailboxSettingsManager();
+
+	LifecycleManager getLifecycleManager();
+
+	ContactManager getContactManager();
+
+	Clock getClock();
+
+	TransportPropertyManager getTransportPropertyManager();
+
+	AuthorFactory getAuthorFactory();
+
+	CryptoComponent getCrypto();
+}
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTestUtils.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTestUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f27c648dc0bb703be017fc5a1b34a720c8c981a
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTestUtils.java
@@ -0,0 +1,135 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons;
+import org.briarproject.bramble.api.WeakSingletonProvider;
+import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
+import org.briarproject.bramble.test.TestDatabaseConfigModule;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nonnull;
+import javax.net.SocketFactory;
+
+import dagger.Module;
+import dagger.Provides;
+import okhttp3.OkHttpClient;
+
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.Assert.fail;
+
+class MailboxIntegrationTestUtils {
+
+	static final String URL_BASE = "http://127.0.0.1:8000";
+
+	static String getQrCodePayload(MailboxAuthToken setupToken) {
+		byte[] bytes = getQrCodeBytes(setupToken);
+		Charset charset = Charset.forName("ISO-8859-1");
+		return new String(bytes, charset);
+	}
+
+	private static byte[] getQrCodeBytes(MailboxAuthToken setupToken) {
+		byte[] hiddenServiceBytes = getHiddenServiceBytes();
+		byte[] setupTokenBytes = setupToken.getBytes();
+		return ByteBuffer.allocate(65).put((byte) 32)
+				.put(hiddenServiceBytes).put(setupTokenBytes).array();
+	}
+
+	private static byte[] getHiddenServiceBytes() {
+		byte[] data = new byte[32];
+		Arrays.fill(data, (byte) 'a');
+		return data;
+	}
+
+	private static WeakSingletonProvider<OkHttpClient> createHttpClientProvider() {
+		OkHttpClient client = new OkHttpClient.Builder()
+				.socketFactory(SocketFactory.getDefault())
+				.connectTimeout(60_000, MILLISECONDS)
+				.build();
+		return new WeakSingletonProvider<OkHttpClient>() {
+			@Override
+			@Nonnull
+			public OkHttpClient createInstance() {
+				return client;
+			}
+		};
+	}
+
+	static MailboxApi createMailboxApi() {
+		return new MailboxApiImpl(createHttpClientProvider(),
+				new TestUrlConverter());
+	}
+
+	static MailboxIntegrationTestComponent createTestComponent(
+			File databaseDir) {
+		MailboxIntegrationTestComponent component =
+				DaggerMailboxIntegrationTestComponent
+						.builder()
+						.testDatabaseConfigModule(
+								new TestDatabaseConfigModule(databaseDir))
+						.build();
+		BrambleCoreIntegrationTestEagerSingletons.Helper
+				.injectEagerSingletons(component);
+		return component;
+	}
+
+	@Module
+	static class TestUrlConverterModule {
+
+		@Provides
+		UrlConverter provideUrlConverter() {
+			return new TestUrlConverter();
+		}
+	}
+
+	interface Check {
+		boolean check() throws Exception;
+	}
+
+	/**
+	 * Run the specified method {@code check} every {@code step} milliseconds
+	 * until either {@code check} returns true or longer than {@code totalTime}
+	 * milliseconds have been spent checking the function and waiting for the
+	 * next invocation.
+	 */
+	static void retryUntilSuccessOrTimeout(long totalTime, long step,
+			Check check) throws Exception {
+		AtomicBoolean success = new AtomicBoolean(false);
+
+		checkRepeatedly(totalTime, step, () -> {
+					boolean result = check.check();
+					if (result) success.set(true);
+					return result;
+				}
+		);
+
+		if (!success.get()) {
+			fail("timeout reached");
+		}
+	}
+
+	/**
+	 * Run the specified method {@code check} every {@code step} milliseconds
+	 * until either {@code check} returns true or longer than {@code totalTime}
+	 * milliseconds have been spent checking the function and waiting for the
+	 * next invocation.
+	 */
+	private static void checkRepeatedly(long totalTime, long step,
+			Check check) throws Exception {
+		long start = currentTimeMillis();
+		while (currentTimeMillis() - start < totalTime) {
+			if (check.check()) {
+				return;
+			}
+			try {
+				Thread.sleep(step);
+			} catch (InterruptedException ignore) {
+				// continue
+			}
+		}
+	}
+}
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorkerIntegrationTest.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorkerIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d91ca40a085dabe83cd977f8c694c23c3941d6e7
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxContactListWorkerIntegrationTest.java
@@ -0,0 +1,140 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorFactory;
+import org.briarproject.bramble.api.identity.Identity;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
+import org.briarproject.bramble.api.mailbox.MailboxPairingState.Paired;
+import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.mailbox.lib.TestMailbox;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import static org.briarproject.bramble.api.mailbox.MailboxAuthToken.fromString;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createMailboxApi;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createTestComponent;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.getQrCodePayload;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.retryUntilSuccessOrTimeout;
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
+import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
+import static org.junit.Assert.assertEquals;
+
+public class OwnMailboxContactListWorkerIntegrationTest
+		extends BrambleTestCase {
+
+	@Rule
+	public TemporaryFolder mailboxDataDirectory = new TemporaryFolder();
+
+	private TestMailbox mailbox;
+
+	private final MailboxApi api = createMailboxApi();
+
+	private MailboxProperties ownerProperties;
+
+	private final File testDir = getTestDirectory();
+	private final File aliceDir = new File(testDir, "alice");
+
+	private MailboxIntegrationTestComponent component;
+	private Identity identity;
+
+	private final SecretKey rootKey = getSecretKey();
+	private final long timestamp = System.currentTimeMillis();
+
+	@Before
+	public void setUp() throws Exception {
+		mailbox = new TestMailbox(mailboxDataDirectory.getRoot());
+		mailbox.startLifecycle();
+
+		MailboxAuthToken setupToken = fromString(mailbox.getSetupToken());
+
+		component = createTestComponent(aliceDir);
+		identity = setUp(component, "Alice");
+
+		MailboxPairingTask pairingTask = component.getMailboxManager()
+				.startPairingTask(getQrCodePayload(setupToken));
+
+		CountDownLatch latch = new CountDownLatch(1);
+		pairingTask.addObserver((state) -> {
+			if (state instanceof Paired) {
+				latch.countDown();
+			}
+		});
+		latch.await();
+
+		ownerProperties = component.getDatabaseComponent()
+				.transactionWithNullableResult(false, txn ->
+						component.getMailboxSettingsManager()
+								.getOwnMailboxProperties(txn)
+				);
+	}
+
+	@After
+	public void tearDown() {
+		mailbox.stopLifecycle(true);
+	}
+
+	private Identity setUp(MailboxIntegrationTestComponent device, String name)
+			throws Exception {
+		// Add an identity for the user
+		IdentityManager identityManager = device.getIdentityManager();
+		Identity identity = identityManager.createIdentity(name);
+		identityManager.registerIdentity(identity);
+		// Start the lifecycle manager
+		LifecycleManager lifecycleManager = device.getLifecycleManager();
+		lifecycleManager.startServices(getSecretKey());
+		lifecycleManager.waitForStartup();
+		// Check the initial conditions
+		ContactManager contactManager = device.getContactManager();
+		assertEquals(0, contactManager.getPendingContacts().size());
+		assertEquals(0, contactManager.getContacts().size());
+		return identity;
+	}
+
+	@Test
+	public void testUploadContacts() throws Exception {
+		int numContactsToAdd = 5;
+		List<ContactId> expectedContacts =
+				createContacts(component, identity, numContactsToAdd);
+
+		// Check for number of contacts on mailbox via API every 100ms
+		retryUntilSuccessOrTimeout(1000, 100, () -> {
+			Collection<ContactId> contacts = api.getContacts(ownerProperties);
+			if (contacts.size() == numContactsToAdd) {
+				assertEquals(expectedContacts, contacts);
+				return true;
+			}
+			return false;
+		});
+	}
+
+	private List<ContactId> createContacts(
+			MailboxIntegrationTestComponent component, Identity local,
+			int numContacts) throws DbException {
+		List<ContactId> contactIds = new ArrayList<>();
+		ContactManager contactManager = component.getContactManager();
+		AuthorFactory authorFactory = component.getAuthorFactory();
+		for (int i = 0; i < numContacts; i++) {
+			Author remote = authorFactory.createLocalAuthor("Bob " + i);
+			contactIds.add(contactManager.addContact(remote, local.getId(),
+					rootKey, timestamp, true, true, true));
+		}
+		return contactIds;
+	}
+}
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestUrlConverter.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestUrlConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d95a3eca0ab95ed01190dbd876a77de36ba1a6bf
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestUrlConverter.java
@@ -0,0 +1,14 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.URL_BASE;
+
+@NotNullByDefault
+class TestUrlConverter implements UrlConverter {
+
+	@Override
+	public String convertOnionToBaseUrl(String onion) {
+		return URL_BASE;
+	}
+}
diff --git a/mailbox-integration-tests/src/test/resources/logback.xml b/mailbox-integration-tests/src/test/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cd7cac14b9c3a1ce8ea28471983afcf408c4af42
--- /dev/null
+++ b/mailbox-integration-tests/src/test/resources/logback.xml
@@ -0,0 +1,13 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="trace">
+        <appender-ref ref="STDOUT" />
+    </root>
+    <logger name="org.eclipse.jetty" level="INFO" />
+    <logger name="io.netty" level="INFO" />
+    <logger name="io.mockk" level="INFO" />
+</configuration>
diff --git a/settings.gradle b/settings.gradle
index def39c0cd9ae5490d079baed012780e6cbb49bf9..f795ed7359b778cddcf28a425221caede44a1f46 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,3 +6,15 @@ include ':briar-api'
 include ':briar-core'
 include ':briar-android'
 include ':briar-headless'
+// Enable the mailbox integration tests by passing
+// `MAILBOX_INTEGRATION_TESTS=true ./gradlew mailbox-integration-tests:test`
+// on the command line (for CI etc) or set `briar.mailbox_integration_tests=true`
+// in gradle.properties to enable the modules for local development.
+if (ext.has("briar.mailbox_integration_tests") && ext.get("briar.mailbox_integration_tests") == "true"
+		|| System.env.MAILBOX_INTEGRATION_TESTS) {
+	include ':mailbox-integration-tests'
+	include(":mailbox-core")
+	include(":mailbox-lib")
+	project(":mailbox-core").projectDir = file("briar-mailbox/mailbox-core")
+	project(":mailbox-lib").projectDir = file("briar-mailbox/mailbox-lib")
+}