diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 966457e8e253cafba47123a364439f9d2ae1a433..2f93054bf0210af5af70e361c69fba1b86ea7dfa 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -107,6 +107,10 @@ bridge test:
 mailbox integration test:
   extends: .optional_tests
   rules:
+    - changes:
+        - mailbox-integration-tests/**/*
+      when: on_success
+      allow_failure: false
     - if: '$CI_PIPELINE_SOURCE == "schedule"'
       when: on_success
     - if: '$CI_COMMIT_TAG == null'
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java b/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java
index 5be846f61c66d139770efa17e45dcaae70a74dda..7859bd6521091bb87ddb981f08e603e15b9503b4 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java
@@ -9,6 +9,7 @@ import org.briarproject.bramble.socks.SocksModule;
 import org.briarproject.bramble.system.AndroidSystemModule;
 import org.briarproject.bramble.system.AndroidTaskSchedulerModule;
 import org.briarproject.bramble.system.AndroidWakefulIoExecutorModule;
+import org.briarproject.bramble.system.DefaultThreadFactoryModule;
 
 import dagger.Module;
 
@@ -18,6 +19,7 @@ import dagger.Module;
 		AndroidSystemModule.class,
 		AndroidTaskSchedulerModule.class,
 		AndroidWakefulIoExecutorModule.class,
+		DefaultThreadFactoryModule.class,
 		CircumventionModule.class,
 		DnsModule.class,
 		ReportingModule.class,
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/TimeLoggingExecutor.java b/bramble-core/src/main/java/org/briarproject/bramble/TimeLoggingExecutor.java
index dc0b96d3d487b28ee9faea453b510dfc3d219b72..3513b5f540a948575a38298332df2f5fc7358023 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/TimeLoggingExecutor.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/TimeLoggingExecutor.java
@@ -4,6 +4,7 @@ import org.briarproject.nullsafety.NotNullByDefault;
 
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Logger;
@@ -19,9 +20,10 @@ public class TimeLoggingExecutor extends ThreadPoolExecutor {
 	public TimeLoggingExecutor(String tag, int corePoolSize, int maxPoolSize,
 			long keepAliveTime, TimeUnit unit,
 			BlockingQueue<Runnable> workQueue,
+			ThreadFactory threadFactory,
 			RejectedExecutionHandler handler) {
 		super(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue,
-				handler);
+				threadFactory, handler);
 		log = Logger.getLogger(tag);
 	}
 
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoExecutorModule.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoExecutorModule.java
index ab0e4114f5672469fd0c26b8061389c1e1b7172c..7a069b2eb817cbfd0d1943e4b5cf01b27ec59192 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoExecutorModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoExecutorModule.java
@@ -9,6 +9,7 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 
 import javax.inject.Inject;
@@ -37,31 +38,31 @@ public class CryptoExecutorModule {
 	private static final int MAX_EXECUTOR_THREADS =
 			Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
 
-	private final ExecutorService cryptoExecutor;
-
 	public CryptoExecutorModule() {
-		// Use an unbounded queue
-		BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
-		// Discard tasks that are submitted during shutdown
-		RejectedExecutionHandler policy =
-				new ThreadPoolExecutor.DiscardPolicy();
-		// Create a limited # of threads and keep them in the pool for 60 secs
-		cryptoExecutor = new TimeLoggingExecutor("CryptoExecutor", 0,
-				MAX_EXECUTOR_THREADS, 60, SECONDS, queue, policy);
 	}
 
 	@Provides
 	@Singleton
 	@CryptoExecutor
 	ExecutorService provideCryptoExecutorService(
-			LifecycleManager lifecycleManager) {
+			LifecycleManager lifecycleManager, ThreadFactory threadFactory) {
+		// Use an unbounded queue
+		BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
+		// Discard tasks that are submitted during shutdown
+		RejectedExecutionHandler policy =
+				new ThreadPoolExecutor.DiscardPolicy();
+		// Create a limited # of threads and keep them in the pool for 60 secs
+		ExecutorService cryptoExecutor = new TimeLoggingExecutor(
+				"CryptoExecutor", 0, MAX_EXECUTOR_THREADS, 60, SECONDS, queue,
+				threadFactory, policy);
 		lifecycleManager.registerForShutdown(cryptoExecutor);
 		return cryptoExecutor;
 	}
 
 	@Provides
 	@CryptoExecutor
-	Executor provideCryptoExecutor() {
+	Executor provideCryptoExecutor(
+			@CryptoExecutor ExecutorService cryptoExecutor) {
 		return cryptoExecutor;
 	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseExecutorModule.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseExecutorModule.java
index a65feef89da60c3f967fb891906ee1a13c14a3d8..73b26710a4b323dc36380728ae38eb13b85ec59e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseExecutorModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseExecutorModule.java
@@ -9,6 +9,7 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 
 import javax.inject.Inject;
@@ -28,24 +29,20 @@ public class DatabaseExecutorModule {
 		ExecutorService executorService;
 	}
 
-	private final ExecutorService databaseExecutor;
-
-	public DatabaseExecutorModule() {
+	@Provides
+	@Singleton
+	@DatabaseExecutor
+	ExecutorService provideDatabaseExecutorService(
+			LifecycleManager lifecycleManager, ThreadFactory threadFactory) {
 		// Use an unbounded queue
 		BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
 		// Discard tasks that are submitted during shutdown
 		RejectedExecutionHandler policy =
 				new ThreadPoolExecutor.DiscardPolicy();
 		// Use a single thread and keep it in the pool for 60 secs
-		databaseExecutor = new TimeLoggingExecutor("DatabaseExecutor", 0, 1,
-				60, SECONDS, queue, policy);
-	}
-
-	@Provides
-	@Singleton
-	@DatabaseExecutor
-	ExecutorService provideDatabaseExecutorService(
-			LifecycleManager lifecycleManager) {
+		ExecutorService databaseExecutor = new TimeLoggingExecutor(
+				"DatabaseExecutor", 0, 1, 60, SECONDS, queue, threadFactory,
+				policy);
 		lifecycleManager.registerForShutdown(databaseExecutor);
 		return databaseExecutor;
 	}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/event/DefaultEventExecutorModule.java b/bramble-core/src/main/java/org/briarproject/bramble/event/DefaultEventExecutorModule.java
index 5fc21cc86a3142a3f2c1ff5f07188dc2c3e1c7c7..2d43800c174ff48ccd7dfc941e2802c115c60f18 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/event/DefaultEventExecutorModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/event/DefaultEventExecutorModule.java
@@ -3,6 +3,7 @@ package org.briarproject.bramble.event;
 import org.briarproject.bramble.api.event.EventExecutor;
 
 import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadFactory;
 
 import javax.inject.Singleton;
 
@@ -22,10 +23,11 @@ public class DefaultEventExecutorModule {
 	@Provides
 	@Singleton
 	@EventExecutor
-	Executor provideEventExecutor() {
+	Executor provideEventExecutor(ThreadFactory threadFactory) {
 		return newSingleThreadExecutor(r -> {
-			Thread t = new Thread(r);
+			Thread t = threadFactory.newThread(r);
 			t.setDaemon(true);
+			t.setName(t.getName() + "-Event");
 			return t;
 		});
 	}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleModule.java b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleModule.java
index 43db266efb188d5b3a3c2bbe90d48e194f7b1990..b38f79b1c5933f731b1a13c62255be94f3336e0d 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleModule.java
@@ -9,6 +9,7 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.RejectedExecutionHandler;
 import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 
 import javax.inject.Inject;
@@ -28,19 +29,6 @@ public class LifecycleModule {
 		Executor executor;
 	}
 
-	private final ExecutorService ioExecutor;
-
-	public LifecycleModule() {
-		// The thread pool is unbounded, so use direct handoff
-		BlockingQueue<Runnable> queue = new SynchronousQueue<>();
-		// Discard tasks that are submitted during shutdown
-		RejectedExecutionHandler policy =
-				new ThreadPoolExecutor.DiscardPolicy();
-		// Create threads as required and keep them in the pool for 60 seconds
-		ioExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
-				60, SECONDS, queue, policy);
-	}
-
 	@Provides
 	@Singleton
 	ShutdownManager provideShutdownManager() {
@@ -57,7 +45,16 @@ public class LifecycleModule {
 	@Provides
 	@Singleton
 	@IoExecutor
-	Executor provideIoExecutor(LifecycleManager lifecycleManager) {
+	Executor provideIoExecutor(LifecycleManager lifecycleManager,
+			ThreadFactory threadFactory) {
+		// The thread pool is unbounded, so use direct handoff
+		BlockingQueue<Runnable> queue = new SynchronousQueue<>();
+		// Discard tasks that are submitted during shutdown
+		RejectedExecutionHandler policy =
+				new ThreadPoolExecutor.DiscardPolicy();
+		// Create threads as required and keep them in the pool for 60 seconds
+		ExecutorService ioExecutor = new ThreadPoolExecutor(0,
+				Integer.MAX_VALUE, 60, SECONDS, queue, threadFactory, policy);
 		lifecycleManager.registerForShutdown(ioExecutor);
 		return ioExecutor;
 	}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiCallerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiCallerImpl.java
index ce08b2f2522019587ba56132ed6e990334c9abdf..368c7ed7c67647d315ac0bbd58b1bbce72976d9d 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiCallerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiCallerImpl.java
@@ -20,12 +20,15 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
 class MailboxApiCallerImpl implements MailboxApiCaller {
 
 	private final TaskScheduler taskScheduler;
+	private final MailboxConfig mailboxConfig;
 	private final Executor ioExecutor;
 
 	@Inject
 	MailboxApiCallerImpl(TaskScheduler taskScheduler,
+			MailboxConfig mailboxConfig,
 			@IoExecutor Executor ioExecutor) {
 		this.taskScheduler = taskScheduler;
+		this.mailboxConfig = mailboxConfig;
 		this.ioExecutor = ioExecutor;
 	}
 
@@ -49,7 +52,8 @@ class MailboxApiCallerImpl implements MailboxApiCaller {
 		private boolean cancelled = false;
 
 		@GuardedBy("lock")
-		private long retryIntervalMs = MIN_RETRY_INTERVAL_MS;
+		private long retryIntervalMs =
+				mailboxConfig.getApiCallerMinRetryInterval();
 
 		private Task(ApiCall apiCall) {
 			this.apiCall = apiCall;
@@ -74,8 +78,9 @@ class MailboxApiCallerImpl implements MailboxApiCaller {
 					scheduledTask = taskScheduler.schedule(this::callApi,
 							ioExecutor, retryIntervalMs, MILLISECONDS);
 					// Increase the retry interval each time we retry
-					retryIntervalMs =
-							min(MAX_RETRY_INTERVAL_MS, retryIntervalMs * 2);
+					retryIntervalMs = min(
+							mailboxConfig.getApiCallerMaxRetryInterval(),
+							retryIntervalMs * 2);
 				}
 			} else {
 				synchronized (lock) {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxConfig.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f03dff6dd35712799377a1243b4a4bc80b52fb3
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxConfig.java
@@ -0,0 +1,24 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.plugin.Plugin;
+
+interface MailboxConfig {
+
+	/**
+	 * The minimum interval between API call retries in milliseconds.
+	 */
+	long getApiCallerMinRetryInterval();
+
+	/**
+	 * The maximum interval between API call retries in milliseconds.
+	 */
+	long getApiCallerMaxRetryInterval();
+
+	/**
+	 * How long (in milliseconds) the Tor plugin needs to be continuously
+	 * {@link Plugin.State#ACTIVE active} before we assume our contacts can
+	 * reach our hidden service.
+	 */
+	long getTorReachabilityPeriod();
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxConfigImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxConfigImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa6a1f528b9357fd973af5cdac763af26b69f95d
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxConfigImpl.java
@@ -0,0 +1,30 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+@Immutable
+@NotNullByDefault
+class MailboxConfigImpl implements MailboxConfig {
+
+	@Inject
+	MailboxConfigImpl() {
+	}
+
+	@Override
+	public long getApiCallerMinRetryInterval() {
+		return MailboxApiCaller.MIN_RETRY_INTERVAL_MS;
+	}
+
+	@Override
+	public long getApiCallerMaxRetryInterval() {
+		return MailboxApiCaller.MAX_RETRY_INTERVAL_MS;
+	}
+
+	@Override
+	public long getTorReachabilityPeriod() {
+		return TorReachabilityMonitor.REACHABILITY_PERIOD_MS;
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverterModule.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ModularMailboxModule.java
similarity index 58%
rename from bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverterModule.java
rename to bramble-core/src/main/java/org/briarproject/bramble/mailbox/ModularMailboxModule.java
index cc9d4469875b2674696a0f6e56dd8d3ae32be79d..8ddbd782466cb56cc60ee3edf9793ec4b40cfdab 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/UrlConverterModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ModularMailboxModule.java
@@ -4,7 +4,11 @@ import dagger.Module;
 import dagger.Provides;
 
 @Module
-public class UrlConverterModule {
+public class ModularMailboxModule {
+	@Provides
+	MailboxConfig provideMailboxConfig(MailboxConfigImpl mailboxConfig) {
+		return mailboxConfig;
+	}
 
 	@Provides
 	UrlConverter provideUrlConverter(UrlConverterImpl urlConverter) {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImpl.java
index 359e70460538cc8eb0fb29ad8bad8d439ed7feea..20afebe30f2862b230d66c3bc7e175b81f5e7dc2 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImpl.java
@@ -32,6 +32,7 @@ class TorReachabilityMonitorImpl
 
 	private final Executor ioExecutor;
 	private final TaskScheduler taskScheduler;
+	private final MailboxConfig mailboxConfig;
 	private final PluginManager pluginManager;
 	private final EventBus eventBus;
 	private final Object lock = new Object();
@@ -50,10 +51,12 @@ class TorReachabilityMonitorImpl
 	TorReachabilityMonitorImpl(
 			@IoExecutor Executor ioExecutor,
 			TaskScheduler taskScheduler,
+			MailboxConfig mailboxConfig,
 			PluginManager pluginManager,
 			EventBus eventBus) {
 		this.ioExecutor = ioExecutor;
 		this.taskScheduler = taskScheduler;
+		this.mailboxConfig = mailboxConfig;
 		this.pluginManager = pluginManager;
 		this.eventBus = eventBus;
 	}
@@ -110,7 +113,7 @@ class TorReachabilityMonitorImpl
 		synchronized (lock) {
 			if (destroyed || task != null) return;
 			task = taskScheduler.schedule(this::onTorReachable, ioExecutor,
-					REACHABILITY_PERIOD_MS, MILLISECONDS);
+					mailboxConfig.getTorReachabilityPeriod(), MILLISECONDS);
 		}
 	}
 
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/system/DefaultTaskSchedulerModule.java b/bramble-core/src/main/java/org/briarproject/bramble/system/DefaultTaskSchedulerModule.java
index 3e36faff275884f76df2e001d8b3c4ac2f9709f7..30e4732370db1c4abe8e7717021d6755248c5628 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/system/DefaultTaskSchedulerModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/system/DefaultTaskSchedulerModule.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.system.TaskScheduler;
 import java.util.concurrent.RejectedExecutionHandler;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -21,18 +22,15 @@ public class DefaultTaskSchedulerModule {
 		TaskScheduler scheduler;
 	}
 
-	private final ScheduledExecutorService scheduledExecutorService;
-
-	public DefaultTaskSchedulerModule() {
+	@Provides
+	@Singleton
+	TaskScheduler provideTaskScheduler(LifecycleManager lifecycleManager,
+			ThreadFactory threadFactory) {
 		// Discard tasks that are submitted during shutdown
 		RejectedExecutionHandler policy =
 				new ScheduledThreadPoolExecutor.DiscardPolicy();
-		scheduledExecutorService = new ScheduledThreadPoolExecutor(1, policy);
-	}
-
-	@Provides
-	@Singleton
-	TaskScheduler provideTaskScheduler(LifecycleManager lifecycleManager) {
+		ScheduledExecutorService scheduledExecutorService =
+				new ScheduledThreadPoolExecutor(1, threadFactory, policy);
 		lifecycleManager.registerForShutdown(scheduledExecutorService);
 		return new TaskSchedulerImpl(scheduledExecutorService);
 	}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/system/DefaultThreadFactoryModule.java b/bramble-core/src/main/java/org/briarproject/bramble/system/DefaultThreadFactoryModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..d2ea5f976dd022bed1dafa285e0a46eb2e91aed7
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/system/DefaultThreadFactoryModule.java
@@ -0,0 +1,18 @@
+package org.briarproject.bramble.system;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class DefaultThreadFactoryModule {
+	@Provides
+	@Singleton
+	ThreadFactory provideThreadFactory() {
+		return Executors.defaultThreadFactory();
+	}
+}
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 becb74db811a8b8cf2b57b0b656bf7fecb85c0ed..f01e365f1dda5f503d0a9ab8ffcf239c0888a08b 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,7 +9,7 @@ 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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
@@ -25,7 +25,7 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiCallerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiCallerImplTest.java
index 2612fe16abcbc21aaf8beddf19bdf5f411d77f64..b625a94a8e65e95ced4e593da5f63e5ac754af1e 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiCallerImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiCallerImplTest.java
@@ -21,12 +21,13 @@ public class MailboxApiCallerImplTest extends BrambleMockTestCase {
 
 	private final TaskScheduler taskScheduler =
 			context.mock(TaskScheduler.class);
+	private final MailboxConfig mailboxConfig = new MailboxConfigImpl();
 	private final Executor ioExecutor = context.mock(Executor.class);
 	private final ApiCall apiCall = context.mock(ApiCall.class);
 	private final Cancellable scheduledTask = context.mock(Cancellable.class);
 
 	private final MailboxApiCallerImpl caller =
-			new MailboxApiCallerImpl(taskScheduler, ioExecutor);
+			new MailboxApiCallerImpl(taskScheduler, mailboxConfig, ioExecutor);
 
 	@Test
 	public void testSubmitsTaskImmediately() {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java
index 3b46320c8830c0845f2f3b087a06c3e6fee2a1d8..aeaaccbafd5a4da7f394aa0cc314f846d5d4ba1c 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java
@@ -24,9 +24,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-import javax.annotation.Nonnull;
-import javax.net.SocketFactory;
-
 import okhttp3.OkHttpClient;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
@@ -34,8 +31,8 @@ import okhttp3.mockwebserver.RecordedRequest;
 import okio.Buffer;
 
 import static java.util.Collections.singletonList;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
+import static org.briarproject.bramble.mailbox.MailboxTestUtils.createHttpClientProvider;
 import static org.briarproject.bramble.test.TestUtils.getContactId;
 import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
@@ -56,18 +53,8 @@ public class MailboxApiTest extends BrambleTestCase {
 	@Rule
 	public TemporaryFolder folder = new TemporaryFolder();
 
-	private final OkHttpClient client = new OkHttpClient.Builder()
-			.socketFactory(SocketFactory.getDefault())
-			.connectTimeout(60_000, MILLISECONDS)
-			.build();
 	private final WeakSingletonProvider<OkHttpClient> httpClientProvider =
-			new WeakSingletonProvider<OkHttpClient>() {
-				@Override
-				@Nonnull
-				public OkHttpClient createInstance() {
-					return client;
-				}
-			};
+			createHttpClientProvider();
 	// We aren't using a real onion address, so use the given address verbatim
 	private final UrlConverter urlConverter = onion -> onion;
 	private final MailboxApiImpl api = new MailboxApiImpl(httpClientProvider,
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImplTest.java
index 6a3e312ed1716865dccdd71a40cd0f86b9ef3c98..fbc817d3f7f86882f69bd0106f6f3b5593ac2326 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImplTest.java
@@ -22,13 +22,12 @@ import org.jmock.Expectations;
 import org.junit.Test;
 
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static java.util.Collections.singletonList;
+import static org.briarproject.bramble.mailbox.MailboxTestUtils.getQrCodePayload;
 import static org.briarproject.bramble.test.TestUtils.getContact;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
@@ -59,7 +58,8 @@ public class MailboxPairingTaskImplTest extends BrambleMockTestCase {
 			new MailboxAuthToken(getRandomId());
 	private final MailboxAuthToken ownerToken =
 			new MailboxAuthToken(getRandomId());
-	private final String validPayload = getValidPayload();
+	private final String validPayload =
+			getQrCodePayload(onionBytes, setupToken.getBytes());
 	private final long time = System.currentTimeMillis();
 	private final MailboxProperties setupProperties = new MailboxProperties(
 			onion, setupToken, new ArrayList<>());
@@ -194,16 +194,6 @@ public class MailboxPairingTaskImplTest extends BrambleMockTestCase {
 				MailboxPairingState.UnexpectedError.class));
 	}
 
-	private String getValidPayload() {
-		byte[] payloadBytes = ByteBuffer.allocate(65)
-				.put((byte) 32) // 1
-				.put(onionBytes) // 32
-				.put(setupToken.getBytes()) // 32
-				.array();
-		//noinspection CharsetObjectCanBeUsed
-		return new String(payloadBytes, Charset.forName("ISO-8859-1"));
-	}
-
 	private PredicateMatcher<MailboxProperties> matches(MailboxProperties p2) {
 		return new PredicateMatcher<>(MailboxProperties.class, p1 ->
 				p1.getAuthToken().equals(p2.getAuthToken()) &&
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxTestUtils.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxTestUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..4795fb739b5f484fc88e29a286b79bdd4355debf
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxTestUtils.java
@@ -0,0 +1,44 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.WeakSingletonProvider;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+import javax.annotation.Nonnull;
+import javax.net.SocketFactory;
+
+import okhttp3.OkHttpClient;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.briarproject.bramble.test.TestUtils.getRandomId;
+
+class MailboxTestUtils {
+
+	static String getQrCodePayload(byte[] onionBytes, byte[] setupToken) {
+		byte[] payloadBytes = ByteBuffer.allocate(65)
+				.put((byte) 32) // 1
+				.put(onionBytes) // 32
+				.put(setupToken) // 32
+				.array();
+		//noinspection CharsetObjectCanBeUsed
+		return new String(payloadBytes, Charset.forName("ISO-8859-1"));
+	}
+
+	static String getQrCodePayload(byte[] setupToken) {
+		return getQrCodePayload(getRandomId(), setupToken);
+	}
+
+	static WeakSingletonProvider<OkHttpClient> createHttpClientProvider() {
+		return new WeakSingletonProvider<OkHttpClient>() {
+			@Override
+			@Nonnull
+			public OkHttpClient createInstance() {
+				return new OkHttpClient.Builder()
+						.socketFactory(SocketFactory.getDefault())
+						.connectTimeout(60_000, MILLISECONDS)
+						.build();
+			}
+		};
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImplTest.java
index 032095cd6bd798ab4246bf7df48fe94de5bb5b51..547041a1f2538a22c13ce040f46c3aa62bb9e163 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/TorReachabilityMonitorImplTest.java
@@ -29,6 +29,7 @@ public class TorReachabilityMonitorImplTest extends BrambleMockTestCase {
 	private final Executor ioExecutor = context.mock(Executor.class);
 	private final TaskScheduler taskScheduler =
 			context.mock(TaskScheduler.class);
+	private final MailboxConfig mailboxConfig = new MailboxConfigImpl();
 	private final PluginManager pluginManager =
 			context.mock(PluginManager.class);
 	private final EventBus eventBus = context.mock(EventBus.class);
@@ -39,7 +40,7 @@ public class TorReachabilityMonitorImplTest extends BrambleMockTestCase {
 
 	private final TorReachabilityMonitorImpl monitor =
 			new TorReachabilityMonitorImpl(ioExecutor, taskScheduler,
-					pluginManager, eventBus);
+					mailboxConfig, pluginManager, eventBus);
 
 	@Test
 	public void testSchedulesTaskWhenStartedIfTorIsActive() {
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 59435bdb0fe248beb76a0acf6c77868aa8e0ea0a..6a071e2deb17225fb3ee4d6cbf4f6be80844a430 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,7 +9,8 @@ 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.mailbox.ModularMailboxModule;
+import org.briarproject.bramble.system.DefaultThreadFactoryModule;
 import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
 import org.briarproject.bramble.system.TimeTravelModule;
 import org.briarproject.bramble.test.TestDatabaseConfigModule;
@@ -29,13 +30,14 @@ import dagger.Component;
 		DefaultBatteryManagerModule.class,
 		DefaultEventExecutorModule.class,
 		DefaultWakefulIoExecutorModule.class,
+		DefaultThreadFactoryModule.class,
 		TestDatabaseConfigModule.class,
 		TestDnsModule.class,
 		TestFeatureFlagModule.class,
 		TestMailboxDirectoryModule.class,
 		RemovableDriveIntegrationTestModule.class,
 		RemovableDriveModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestSecureRandomModule.class,
 		TimeTravelModule.class,
 		TestSocksModule.class,
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 d6571298e3af75d67320e4bed974a848d14216a4..8a4cda6548753b2020e210c6885e986ae0d87539 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,7 +2,7 @@ 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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
@@ -16,7 +16,7 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
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 8d4224df6ccc561d82dcc4b356e3ee09f770725b..e2ddb2fa94e99e9f4ce1cb4d4e6e43b5d1fede7f 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
@@ -11,6 +11,7 @@ import dagger.Module;
 		DefaultBatteryManagerModule.class,
 		DefaultEventExecutorModule.class,
 		DefaultWakefulIoExecutorModule.class,
+		TestThreadFactoryModule.class,
 		TestDatabaseConfigModule.class,
 		TestFeatureFlagModule.class,
 		TestMailboxDirectoryModule.class,
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTest.java
index e4cd5182696b1bf59c339acbc4ba93d414cd5ab3..9e6c41ab6bdda192310fae6c216bc1cfd75ac441 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTest.java
@@ -235,11 +235,16 @@ public abstract class BrambleIntegrationTest<C extends BrambleIntegrationTestCom
 
 	protected void awaitPendingMessageDelivery(int num)
 			throws TimeoutException {
-		deliveryWaiter.await(TIMEOUT, num);
+		awaitPendingMessageDelivery(num, TIMEOUT);
+	}
+
+	protected void awaitPendingMessageDelivery(int num, long timeout)
+			throws TimeoutException {
+		deliveryWaiter.await(timeout, num);
 		assertEquals("Messages delivered", num, deliveryCounter.getAndSet(0));
 
 		try {
-			messageSemaphore.tryAcquire(num, TIMEOUT, MILLISECONDS);
+			messageSemaphore.tryAcquire(num, timeout, MILLISECONDS);
 		} catch (InterruptedException e) {
 			LOG.info("Interrupted while waiting for messages");
 			Thread.currentThread().interrupt();
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 db5ef337be3c0d30d1bf5a109aec60cfc4082acc..9544d7e9bfc732405c9d447737cb5cf5d7d88f0e 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,7 +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 org.briarproject.bramble.mailbox.ModularMailboxModule;
 
 import javax.inject.Singleton;
 
@@ -16,7 +16,7 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestThreadFactoryModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestThreadFactoryModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd8432d98495a3d90c661c9b0c602a061d7dd2a7
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestThreadFactoryModule.java
@@ -0,0 +1,66 @@
+package org.briarproject.bramble.test;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class TestThreadFactoryModule {
+
+	@Nullable
+	private final String prefix;
+
+	public TestThreadFactoryModule() {
+		this(null);
+	}
+
+	public TestThreadFactoryModule(@Nullable String prefix) {
+		this.prefix = prefix;
+	}
+
+	@Provides
+	ThreadFactory provideThreadFactory() {
+		if (prefix == null) return Executors.defaultThreadFactory();
+		return new TestThreadFactory(prefix);
+	}
+
+	/**
+	 * This class is mostly copied from
+	 * {@link Executors#defaultThreadFactory()} only adds a given prefix.
+	 */
+	static class TestThreadFactory implements ThreadFactory {
+		private static final AtomicInteger poolNumber = new AtomicInteger(1);
+		private final ThreadGroup group;
+		private final AtomicInteger threadNumber = new AtomicInteger(1);
+		private final String namePrefix;
+
+		private TestThreadFactory(String prefix) {
+			SecurityManager s = System.getSecurityManager();
+			this.group = s != null ? s.getThreadGroup() :
+					Thread.currentThread().getThreadGroup();
+			this.namePrefix =
+					prefix + "-p-" + poolNumber.getAndIncrement() + "-t-";
+		}
+
+		@Override
+		public Thread newThread(@Nonnull Runnable r) {
+			Thread t = new Thread(this.group, r,
+					this.namePrefix + this.threadNumber.getAndIncrement(), 0L);
+			if (t.isDaemon()) {
+				t.setDaemon(false);
+			}
+
+			if (t.getPriority() != 5) {
+				t.setPriority(5);
+			}
+
+			return t;
+		}
+	}
+}
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 88a4747a2137d4479eb904e905c9a5e8fd835452..64dfa6f2caa542555a820f90c750337437b5efa9 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,7 +7,7 @@ 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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
 import org.briarproject.bramble.test.TestDnsModule;
@@ -22,7 +22,7 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
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 001c44d634f367ec42a3b0dd13abf417a39d24b0..d627025ab616d509d852161c06c880bf27d26a77 100644
--- a/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
+++ b/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
@@ -1,7 +1,7 @@
 package org.briarproject.bramble;
 
 import org.briarproject.bramble.io.DnsModule;
-import org.briarproject.bramble.mailbox.UrlConverterModule;
+import org.briarproject.bramble.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.network.JavaNetworkModule;
 import org.briarproject.bramble.plugin.tor.CircumventionModule;
 import org.briarproject.bramble.socks.SocksModule;
@@ -14,7 +14,7 @@ import dagger.Module;
 		DnsModule.class,
 		JavaNetworkModule.class,
 		JavaSystemModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.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 8003bb7e55dae1743be26e4b2bedf518b6395855..3f47a3541cf8bfab23440048b13777db29bf860b 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,7 +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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.plugin.tor.BridgeTest;
 import org.briarproject.bramble.plugin.tor.CircumventionProvider;
 
@@ -16,7 +16,7 @@ import dagger.Component;
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
 		BrambleJavaModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestTorPortsModule.class,
 		TestPluginConfigModule.class,
 })
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 6216e241a2a846ebbff311eac68ab30b4d073336..df1d043c34d4e285861a5119239f365130969f70 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,7 +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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.plugin.file.RemovableDriveModule;
 import org.briarproject.bramble.system.ClockModule;
 import org.briarproject.briar.BriarCoreModule;
@@ -28,7 +28,7 @@ import dagger.Component;
 		BrambleAndroidModule.class,
 		BriarAccountModule.class,
 		BrambleCoreModule.class,
-		UrlConverterModule.class
+		ModularMailboxModule.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 a12e480d23d59457201b785aa2b95339fc093b12..e597363786151ffb0485c668b077f088d83b2242 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,7 +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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.plugin.file.RemovableDriveModule;
 import org.briarproject.bramble.system.ClockModule;
 import org.briarproject.briar.BriarCoreModule;
@@ -27,7 +27,7 @@ import dagger.Component;
 		BrambleAndroidModule.class,
 		BriarAccountModule.class,
 		BrambleCoreModule.class,
-		UrlConverterModule.class
+		ModularMailboxModule.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 c1ec805a6220505c598174473449b3c3034f4d3f..468782cd495bd79397cb3e080033664c1bd0fbd2 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,7 +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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.plugin.file.RemovableDriveModule;
 import org.briarproject.bramble.plugin.tor.CircumventionProvider;
 import org.briarproject.bramble.system.ClockModule;
@@ -102,7 +102,7 @@ import dagger.Component;
 		AttachmentModule.class,
 		ClockModule.class,
 		MediaModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.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 fe4afa241cfac6197354ce131680a0d4a4127cbd..896df814abc2d6acf79e635a560f9981e3b7eca6 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,7 +4,7 @@ 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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
@@ -29,7 +29,7 @@ import dagger.Component;
 		BriarClientModule.class,
 		FeedModule.class,
 		IdentityModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
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 69f7bdc4d1a253453bee24725a9b61d40ef3b364..f054a55cb3c7605279f0fcf69caba0bbb08929ab 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,7 +1,7 @@
 package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.BrambleCoreModule;
-import org.briarproject.bramble.mailbox.UrlConverterModule;
+import org.briarproject.bramble.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
@@ -41,7 +41,7 @@ import dagger.Component;
 		MessagingModule.class,
 		PrivateGroupModule.class,
 		SharingModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
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 6b4083adef945972385f5f658da36410c5a40bd1..c01400b8206e041713a4d315ee4be1f3a4da0d99 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,7 +2,7 @@ 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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
@@ -29,7 +29,7 @@ import dagger.Component;
 		ForumModule.class,
 		IdentityModule.class,
 		MessagingModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
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 e3498608dd18b0127640266f772ffb68d4fb26a0..f929bab690aa355c811665caa16d11f4b88a83a2 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,7 +7,7 @@ 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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.TestDnsModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
@@ -30,7 +30,7 @@ import dagger.Component;
 		BriarClientModule.class,
 		ConversationModule.class,
 		MessagingModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
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 a2486c3470255e8f2b7993f69912e990f47932e0..c2010c345894c5e3c903b9dc98225e5b183dce83 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,7 +8,7 @@ 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.mailbox.ModularMailboxModule;
 import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
 import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
 import org.briarproject.bramble.test.TestDnsModule;
@@ -66,7 +66,7 @@ import dagger.Component;
 		MessagingModule.class,
 		PrivateGroupModule.class,
 		SharingModule.class,
-		UrlConverterModule.class,
+		ModularMailboxModule.class,
 		TestDnsModule.class,
 		TestSocksModule.class,
 		TestPluginConfigModule.class,
diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt
index a841f9913289d4aeed8afdecd6229ed9e5b1649f..d15db40dd92716270eacb8dab8de80e33a859ca7 100644
--- a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt
+++ b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt
@@ -22,6 +22,7 @@ import org.briarproject.bramble.plugin.tor.UnixTorPluginFactory
 import org.briarproject.bramble.plugin.tor.WindowsTorPluginFactory
 import org.briarproject.bramble.system.ClockModule
 import org.briarproject.bramble.system.DefaultTaskSchedulerModule
+import org.briarproject.bramble.system.DefaultThreadFactoryModule
 import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule
 import org.briarproject.bramble.system.DesktopSecureRandomModule
 import org.briarproject.bramble.util.OsUtils.isLinux
@@ -44,6 +45,7 @@ import javax.inject.Singleton
         DefaultEventExecutorModule::class,
         DefaultTaskSchedulerModule::class,
         DefaultWakefulIoExecutorModule::class,
+        DefaultThreadFactoryModule::class,
         DesktopSecureRandomModule::class,
         HeadlessBlogModule::class,
         HeadlessContactModule::class,
diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt
index 2725b70a5195a3f5bf72529b53baaa41aa6558fd..219051d16cb67aa984f47963cb9e9f3ec4faa4b8 100644
--- a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt
+++ b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt
@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory
 import org.briarproject.bramble.event.DefaultEventExecutorModule
 import org.briarproject.bramble.system.ClockModule
 import org.briarproject.bramble.system.DefaultTaskSchedulerModule
+import org.briarproject.bramble.system.DefaultThreadFactoryModule
 import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule
 import org.briarproject.bramble.test.TestFeatureFlagModule
 import org.briarproject.bramble.test.TestSecureRandomModule
@@ -37,6 +38,7 @@ import javax.inject.Singleton
         DefaultEventExecutorModule::class,
         DefaultTaskSchedulerModule::class,
         DefaultWakefulIoExecutorModule::class,
+        DefaultThreadFactoryModule::class,
         TestFeatureFlagModule::class,
         TestSecureRandomModule::class,
         HeadlessBlogModule::class,
diff --git a/mailbox-integration-tests/build.gradle b/mailbox-integration-tests/build.gradle
index cd2f67fde05be2bb016316dd4494e1d4d9b5e5b1..3b833cc2bb6089d57991cfd722854179714ea9e4 100644
--- a/mailbox-integration-tests/build.gradle
+++ b/mailbox-integration-tests/build.gradle
@@ -7,14 +7,15 @@ apply from: '../dagger.gradle'
 
 dependencies {
 	testImplementation project(path: ':bramble-api', configuration: 'default')
+	testImplementation project(path: ':bramble-api', configuration: 'testOutput')
 	testImplementation project(path: ':bramble-core', configuration: 'default')
+	testImplementation project(path: ':bramble-core', configuration: 'testOutput')
 	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"
+	testImplementation 'net.jodah:concurrentunit:0.4.6'
 
 	testAnnotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
 }
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/AbstractMailboxIntegrationTest.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/AbstractMailboxIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2eb0eef4fa5a0aaede9fe687c88da04436fe16b8
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/AbstractMailboxIntegrationTest.java
@@ -0,0 +1,221 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbCallable;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.identity.Identity;
+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.mailbox.MailboxAuthToken;
+import org.briarproject.bramble.api.mailbox.MailboxPairingState;
+import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.test.BrambleIntegrationTest;
+import org.briarproject.bramble.test.TestDatabaseConfigModule;
+import org.briarproject.bramble.test.TestLogFormatter;
+import org.briarproject.bramble.test.TestThreadFactoryModule;
+import org.briarproject.mailbox.lib.AbstractMailbox;
+import org.briarproject.mailbox.lib.TestMailbox;
+import org.junit.After;
+import org.junit.Before;
+
+import java.io.File;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.briarproject.bramble.api.mailbox.MailboxAuthToken.fromString;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestComponent.Helper.injectEagerSingletons;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createMailboxApi;
+import static org.briarproject.bramble.mailbox.MailboxTestUtils.getQrCodePayload;
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
+import static org.briarproject.bramble.util.StringUtils.getRandomString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+abstract class AbstractMailboxIntegrationTest
+		extends BrambleIntegrationTest<MailboxIntegrationTestComponent> {
+
+	static final String URL_BASE = "http://127.0.0.1:8000";
+
+	AbstractMailboxIntegrationTest() {
+		TestLogFormatter.use();
+	}
+
+	private final TransportId transportId = new TransportId(getRandomString(4));
+	private final File dir1 = new File(testDir, "alice");
+	private final File dir2 = new File(testDir, "bob");
+	private final SecretKey rootKey = getSecretKey();
+
+	MailboxIntegrationTestComponent c1, c2;
+	Contact contact1From2, contact2From1;
+	TestMailbox mailbox;
+	MailboxApi api = createMailboxApi();
+
+	@Before
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		c1 = startTestComponent(dir1, "Alice");
+		c2 = startTestComponent(dir2, "Bob");
+		mailbox = new TestMailbox(new File(testDir, "mailbox"));
+		mailbox.startLifecycle();
+	}
+
+	@After
+	@Override
+	public void tearDown() throws Exception {
+		super.tearDown();
+		c1.getLifecycleManager().stopServices();
+		c2.getLifecycleManager().stopServices();
+		c1.getLifecycleManager().waitForShutdown();
+		c2.getLifecycleManager().waitForShutdown();
+		mailbox.stopLifecycle(true);
+	}
+
+	private MailboxIntegrationTestComponent startTestComponent(
+			File databaseDir, String name) throws Exception {
+		TestThreadFactoryModule threadFactoryModule =
+				new TestThreadFactoryModule(name);
+		TestDatabaseConfigModule dbModule =
+				new TestDatabaseConfigModule(databaseDir);
+		MailboxIntegrationTestComponent component =
+				DaggerMailboxIntegrationTestComponent
+						.builder()
+						.testThreadFactoryModule(threadFactoryModule)
+						.testDatabaseConfigModule(dbModule)
+						.build();
+		injectEagerSingletons(component);
+
+		setUp(component, name);
+		return component;
+	}
+
+	private void 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();
+		addEventListener(device);
+	}
+
+	MailboxProperties pair(MailboxIntegrationTestComponent c,
+			AbstractMailbox mailbox) throws Exception {
+		MailboxAuthToken setupToken = fromString(mailbox.getSetupToken());
+
+		MailboxPairingTask pairingTask = c.getMailboxManager()
+				.startPairingTask(getQrCodePayload(setupToken.getBytes()));
+
+		CountDownLatch latch = new CountDownLatch(1);
+		pairingTask.addObserver((state) -> {
+			if (state instanceof MailboxPairingState.Paired) {
+				latch.countDown();
+			}
+		});
+		if (!latch.await(10, SECONDS)) {
+			fail("Timeout reached when waiting for pairing.");
+		}
+		MailboxProperties properties = c.getDatabaseComponent()
+				.transactionWithNullableResult(true, txn ->
+						c.getMailboxSettingsManager()
+								.getOwnMailboxProperties(txn)
+				);
+		assertNotNull(properties);
+		return properties;
+	}
+
+	void addContacts() throws Exception {
+		LocalAuthor author1 = c1.getIdentityManager().getLocalAuthor();
+		LocalAuthor author2 = c2.getIdentityManager().getLocalAuthor();
+
+		ContactId contactId2From1 =
+				c1.getContactManager().addContact(author2,
+						author1.getId(), rootKey,
+						c1.getClock().currentTimeMillis(),
+						true, true, true);
+		ContactId contactId1From2 =
+				c2.getContactManager().addContact(author1,
+						author2.getId(), rootKey,
+						c2.getClock().currentTimeMillis(),
+						false, true, true);
+
+		contact2From1 = c1.getContactManager().getContact(contactId2From1);
+		contact1From2 = c2.getContactManager().getContact(contactId1From2);
+
+		// Sync client versioning update from 1 to 2
+		sync1To2(1, true);
+		// Sync client versioning update and ack from 2 to 1
+		sync2To1(1, true);
+		// Sync second client versioning update, mailbox properties and ack
+		// from 1 to 2
+		sync1To2(2, true);
+		// Sync mailbox properties and ack from 2 to 1
+		sync2To1(1, true);
+		// Sync final ack from 1 to 2
+		ack1To2(1);
+	}
+
+	<T> T getFromDb(MailboxIntegrationTestComponent device,
+			DbCallable<T, ?> callable) throws Exception {
+		return device.getDatabaseComponent()
+				.transactionWithResult(true, callable::call);
+	}
+
+	MailboxProperties getMailboxProperties(
+			MailboxIntegrationTestComponent device, ContactId contactId)
+			throws DbException {
+		DatabaseComponent db = device.getDatabaseComponent();
+		MailboxUpdateWithMailbox update = (MailboxUpdateWithMailbox)
+				db.transactionWithNullableResult(true, txn ->
+						device.getMailboxUpdateManager()
+								.getRemoteUpdate(txn, contactId)
+				);
+		if (update == null) fail();
+		return update.getMailboxProperties();
+	}
+
+	void broadcastMessage(MailboxIntegrationTestComponent from)
+			throws Exception {
+		TransportProperties p = from.getTransportPropertyManager()
+				.getLocalProperties(transportId);
+		p.put(getRandomString(23), getRandomString(8));
+		from.getTransportPropertyManager().mergeLocalProperties(transportId, p);
+	}
+
+	void sync1To2(int num, boolean valid) throws Exception {
+		syncMessage(c1, c2, contact2From1.getId(), num, valid);
+	}
+
+	void sync2To1(int num, boolean valid) throws Exception {
+		syncMessage(c2, c1, contact1From2.getId(), num, valid);
+	}
+
+	void ack1To2(int num) throws Exception {
+		sendAcks(c1, c2, contact2From1.getId(), num);
+	}
+
+	void ack2To1(int num) throws Exception {
+		sendAcks(c2, c1, contact1From2.getId(), num);
+	}
+
+	void assertNumMessages(MailboxIntegrationTestComponent c,
+			ContactId contactId, int num) throws DbException {
+		Map<ContactId, TransportProperties> p = c.getTransportPropertyManager()
+				.getRemoteProperties(transportId);
+		assertEquals(num, p.get(contactId).size());
+	}
+
+}
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxApiIntegrationTest.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxApiIntegrationTest.java
index e6ab681216f9f8cbdbcb0d59435953363c776701..d196d180dcff07c8af9e5f836bf26bf881e01263 100644
--- a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxApiIntegrationTest.java
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxApiIntegrationTest.java
@@ -26,7 +26,7 @@ import java.util.List;
 
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
-import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.URL_BASE;
+import static org.briarproject.bramble.mailbox.AbstractMailboxIntegrationTest.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;
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..043257eaf5367d6aacb27e651e73000490868eb0
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java
@@ -0,0 +1,70 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.retryUntilSuccessOrTimeout;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class MailboxIntegrationTest extends AbstractMailboxIntegrationTest {
+
+	private static final Logger LOG =
+			getLogger(MailboxIntegrationTest.class.getSimpleName());
+
+	@Test
+	public void testSendMessageViaMailbox() throws Exception {
+		addContacts();
+
+		// c1 one pairs the mailbox
+		MailboxProperties props1 = pair(c1, mailbox);
+
+		// Check for number of contacts on mailbox via API every 100ms.
+		// This should be quick and will succeed with first call.
+		retryUntilSuccessOrTimeout(1_000, 100, () -> {
+			Collection<ContactId> contacts = api.getContacts(props1);
+			return contacts.size() == 1;
+		});
+
+		// tell contact about mailbox
+		sync1To2(1, true);
+		ack2To1(1);
+
+		// contact should have received their MailboxProperties
+		MailboxProperties props2 =
+				getMailboxProperties(c2, contact1From2.getId());
+		assertNotNull(props2.getInboxId());
+
+		// wait until file containing mailbox properties arrived on mailbox
+		retryUntilSuccessOrTimeout(5_000, 500, () -> {
+			List<MailboxFile> files = api.getFiles(props2, props2.getInboxId());
+			return files.size() == 1;
+		});
+		LOG.info("Mailbox properties uploaded");
+
+		// send message and wait for it to arrive via mailbox
+		broadcastMessage(c1);
+
+		// we don't check for two messages now, because 2 (Bob) might have
+		// download the first message still in their 1st download cycle.
+
+		// wait for message to arrive
+		// this might require 2nd download cycle after Tor reachability period
+		LOG.info("Waiting for delivery of broadcast message");
+		awaitPendingMessageDelivery(1);
+
+		// assert that message arrived for c2
+		assertNumMessages(c2, contact1From2.getId(), 1);
+
+		// all files were deleted from mailbox
+		assertEquals(0, api.getFiles(props2, props2.getInboxId()).size());
+	}
+
+}
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
index a4fc1adb57bc559f8e0c3eca12e291964bc03743..c6c7fd9318f30871bf029070970fbf7b69ec1bcf 100644
--- 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
@@ -1,8 +1,8 @@
 package org.briarproject.bramble.mailbox;
 
+import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons;
 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;
@@ -11,10 +11,9 @@ 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.MailboxTestPluginConfigModule;
 import org.briarproject.bramble.test.TestDnsModule;
 import org.briarproject.bramble.test.TestSocksModule;
 
@@ -26,31 +25,37 @@ import dagger.Component;
 @Component(modules = {
 		BrambleCoreIntegrationTestModule.class,
 		BrambleCoreModule.class,
-		TestUrlConverterModule.class,
-		FakeTorPluginConfigModule.class,
+		TestModularMailboxModule.class,
+		MailboxTestPluginConfigModule.class,
 		TestSocksModule.class,
 		TestDnsModule.class,
 })
 interface MailboxIntegrationTestComponent extends
 		BrambleIntegrationTestComponent {
 
+	LifecycleManager getLifecycleManager();
+
 	DatabaseComponent getDatabaseComponent();
 
-	MailboxManager getMailboxManager();
+	ContactManager getContactManager();
 
-	MailboxUpdateManager getMailboxUpdateManager();
+	AuthorFactory getAuthorFactory();
 
-	MailboxSettingsManager getMailboxSettingsManager();
+	Clock getClock();
 
-	LifecycleManager getLifecycleManager();
+	MailboxManager getMailboxManager();
 
-	ContactManager getContactManager();
+	MailboxSettingsManager getMailboxSettingsManager();
 
-	Clock getClock();
+	MailboxUpdateManager getMailboxUpdateManager();
 
 	TransportPropertyManager getTransportPropertyManager();
 
-	AuthorFactory getAuthorFactory();
-
-	CryptoComponent getCrypto();
+	class Helper {
+		static void injectEagerSingletons(
+				MailboxIntegrationTestComponent c) {
+			BrambleCoreIntegrationTestEagerSingletons.Helper
+					.injectEagerSingletons(c);
+		}
+	}
 }
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
index 3f27c648dc0bb703be017fc5a1b34a720c8c981a..bcea13ea363ddc4c4830879724ab455f842bd056 100644
--- 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
@@ -1,89 +1,16 @@
 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.briarproject.bramble.mailbox.MailboxTestUtils.createHttpClientProvider;
+import static org.briarproject.bramble.mailbox.TestModularMailboxModule.urlConverter;
 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();
-		}
+		return new MailboxApiImpl(createHttpClientProvider(), urlConverter);
 	}
 
 	interface Check {
@@ -126,6 +53,7 @@ class MailboxIntegrationTestUtils {
 				return;
 			}
 			try {
+				//noinspection BusyWait
 				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
index d91ca40a085dabe83cd977f8c694c23c3941d6e7..40b12ffc52627ee11ab8fb66d63f82ff44eb680a 100644
--- 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
@@ -6,112 +6,49 @@ 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.identity.LocalAuthor;
 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;
+		extends AbstractMailboxIntegrationTest {
 
 	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 LocalAuthor localAuthor1;
 
 	private final SecretKey rootKey = getSecretKey();
 	private final long timestamp = System.currentTimeMillis();
 
 	@Before
+	@Override
 	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);
+		super.setUp();
+		localAuthor1 = c1.getIdentityManager().getLocalAuthor();
+		ownerProperties = pair(c1, mailbox);
 	}
 
-	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();
+	@Test
+	public void testUploadContacts() throws Exception {
 		// Check the initial conditions
-		ContactManager contactManager = device.getContactManager();
+		ContactManager contactManager = c1.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);
+		List<ContactId> expectedContacts = createContacts(c1, numContactsToAdd);
 
 		// Check for number of contacts on mailbox via API every 100ms
 		retryUntilSuccessOrTimeout(1000, 100, () -> {
@@ -125,15 +62,16 @@ public class OwnMailboxContactListWorkerIntegrationTest
 	}
 
 	private List<ContactId> createContacts(
-			MailboxIntegrationTestComponent component, Identity local,
-			int numContacts) throws DbException {
+			MailboxIntegrationTestComponent component, 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));
+			ContactId c = contactManager.addContact(remote,
+					localAuthor1.getId(), rootKey, timestamp, true, true, true);
+			contactIds.add(c);
 		}
 		return contactIds;
 	}
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestMailboxConfigImpl.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestMailboxConfigImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..2552fca776fb63baf8f7bf249fad800095a4fe58
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestMailboxConfigImpl.java
@@ -0,0 +1,30 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+@Immutable
+@NotNullByDefault
+class TestMailboxConfigImpl implements MailboxConfig {
+
+	@Inject
+	TestMailboxConfigImpl() {
+	}
+
+	@Override
+	public long getApiCallerMinRetryInterval() {
+		return 1000;
+	}
+
+	@Override
+	public long getApiCallerMaxRetryInterval() {
+		return 2000;
+	}
+
+	@Override
+	public long getTorReachabilityPeriod() {
+		return 10_000;
+	}
+}
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestModularMailboxModule.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestModularMailboxModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..22e001bae8d17375c2eeedc7f573e26b207c92f4
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestModularMailboxModule.java
@@ -0,0 +1,25 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static org.briarproject.bramble.mailbox.AbstractMailboxIntegrationTest.URL_BASE;
+
+@Module
+@NotNullByDefault
+class TestModularMailboxModule {
+
+	@Provides
+	MailboxConfig provideMailboxConfig(TestMailboxConfigImpl mailboxConfig) {
+		return mailboxConfig;
+	}
+
+	static UrlConverter urlConverter = onion -> URL_BASE;
+
+	@Provides
+	UrlConverter provideUrlConverter() {
+		return urlConverter;
+	}
+}
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
deleted file mode 100644
index 5841e7cc97d9b26f0977a04b7fdd33ef700db4e2..0000000000000000000000000000000000000000
--- a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/mailbox/TestUrlConverter.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.briarproject.bramble.mailbox;
-
-import org.briarproject.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/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java
similarity index 89%
rename from bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java
rename to mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java
index 8e68f4f35ad601dea92a0323105c052f0eeaf606..fd702cb3c5b0c2e4727753fa8666da941f6906a1 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/FakeTorPlugin.java
@@ -4,7 +4,7 @@ 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.plugin.ConnectionHandler;
-import org.briarproject.bramble.api.plugin.PluginException;
+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;
@@ -21,7 +21,6 @@ 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
@@ -29,9 +28,14 @@ public class FakeTorPlugin implements DuplexPlugin {
 
 	private static final Logger LOG =
 			getLogger(FakeTorPlugin.class.getName());
+	private final PluginCallback callback;
 
 	private State state = INACTIVE;
 
+	FakeTorPlugin(PluginCallback callback) {
+		this.callback = callback;
+	}
+
 	@Override
 	public TransportId getId() {
 		return TorConstants.ID;
@@ -48,15 +52,17 @@ public class FakeTorPlugin implements DuplexPlugin {
 	}
 
 	@Override
-	public void start() throws PluginException {
+	public void start() {
 		LOG.info("Starting plugin");
 		state = ACTIVE;
+		callback.pluginStateChanged(state);
 	}
 
 	@Override
-	public void stop() throws PluginException {
+	public void stop() {
 		LOG.info("Stopping plugin");
-		state = DISABLED;
+		state = INACTIVE;
+		callback.pluginStateChanged(state);
 	}
 
 	@Override
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java
similarity index 95%
rename from bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java
rename to mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java
index 32ea3139d4706a02c0f52ab74bbff66e19aea03f..9e718991b04c670a543bdc4ee3da7d36ab699142 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/FakeTorPluginFactory.java
@@ -32,6 +32,6 @@ public class FakeTorPluginFactory implements DuplexPluginFactory {
 	@Nullable
 	@Override
 	public DuplexPlugin createPlugin(PluginCallback callback) {
-		return new FakeTorPlugin();
+		return new FakeTorPlugin(callback);
 	}
 }
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginConfigModule.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/MailboxTestPluginConfigModule.java
similarity index 52%
rename from bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginConfigModule.java
rename to mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/MailboxTestPluginConfigModule.java
index 49b9048ad8827f4ff96deb9af431329379f8eeeb..a8c681bd2292b5c450b0d1f1220bfdab68a667f5 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/FakeTorPluginConfigModule.java
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/MailboxTestPluginConfigModule.java
@@ -1,27 +1,56 @@
 package org.briarproject.bramble.test;
 
+import org.briarproject.bramble.api.plugin.PluginCallback;
 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.SimplexPlugin;
 import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
+import org.briarproject.bramble.plugin.file.MailboxPluginFactory;
 import org.briarproject.nullsafety.NotNullByDefault;
 
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+import javax.annotation.Nullable;
+
 import dagger.Module;
 import dagger.Provides;
 
-import static java.util.Collections.emptyList;
+import static java.util.Arrays.asList;
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
+import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID;
 
 @Module
-public class FakeTorPluginConfigModule {
+public class MailboxTestPluginConfigModule {
+
+	private static final int MAX_LATENCY = 30_000; // 30 seconds
+
+	@NotNullByDefault
+	private final SimplexPluginFactory simplex = new SimplexPluginFactory() {
+
+		@Override
+		public TransportId getId() {
+			return SIMPLEX_TRANSPORT_ID;
+		}
+
+		@Override
+		public long getMaxLatency() {
+			return MAX_LATENCY;
+		}
+
+		@Override
+		@Nullable
+		public SimplexPlugin createPlugin(PluginCallback callback) {
+			return null;
+		}
+	};
 
 	@Provides
-	PluginConfig providePluginConfig(FakeTorPluginFactory tor) {
+	PluginConfig providePluginConfig(FakeTorPluginFactory tor,
+			MailboxPluginFactory mailboxPluginFactory) {
 		@NotNullByDefault
 		PluginConfig pluginConfig = new PluginConfig() {
 
@@ -32,7 +61,7 @@ public class FakeTorPluginConfigModule {
 
 			@Override
 			public Collection<SimplexPluginFactory> getSimplexFactories() {
-				return emptyList();
+				return asList(simplex, mailboxPluginFactory);
 			}
 
 			@Override
diff --git a/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/TestLogFormatter.java b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/TestLogFormatter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5b819d15ff828cf9bf9d984cf37019d5aebd560
--- /dev/null
+++ b/mailbox-integration-tests/src/test/java/org/briarproject/bramble/test/TestLogFormatter.java
@@ -0,0 +1,57 @@
+package org.briarproject.bramble.test;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
+public class TestLogFormatter extends SimpleFormatter {
+
+	private final Object lock = new Object();
+	private final DateFormat dateFormat; // Locking: lock
+	private final Date date; // Locking: lock
+
+	public static void use() {
+		LogManager.getLogManager().reset();
+		Logger rootLogger = LogManager.getLogManager().getLogger("");
+		ConsoleHandler handler = new ConsoleHandler();
+		handler.setFormatter(new TestLogFormatter());
+		rootLogger.addHandler(handler);
+	}
+
+	private TestLogFormatter() {
+		synchronized (lock) {
+			dateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
+			date = new Date();
+		}
+	}
+
+	@Override
+	public String format(LogRecord rec) {
+		if (rec.getThrown() == null) {
+			String dateString;
+			synchronized (lock) {
+				date.setTime(rec.getMillis());
+				dateString = dateFormat.format(date);
+			}
+			return String.format("%s [%s] %s %s - %s\n",
+					dateString,
+					Thread.currentThread().getName(),
+					rec.getLevel().getName(),
+					rec.getLoggerName(),
+					rec.getMessage());
+		} else {
+			return super.format(rec);
+		}
+	}
+}