diff --git a/build.gradle b/build.gradle
index 46b97f8065e818812d78e5c1720bb3633360e86a..08d2c86301ccb8bb1c695f7eadf81e988dc16344 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,5 @@
 buildscript {
-    ext.kotlin_version = '1.6.21'
-    ext.hilt_version = '2.40'
-    ext.nav_version = '2.4.2'
-    ext.tor_version = '0.4.5.12-2'
-    ext.obfs4_version = '0.0.12'
-    ext.junit_version = '5.7.2'
-    ext.mockk_version = '1.10.4'
-    ext.ktlint_plugin_version = '10.2.1'
+    apply from: "gradle/variables.gradle"
 
     repositories {
         google()
diff --git a/gradle/variables.gradle b/gradle/variables.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..93ffcbcb9dc31042435d8efeccf4ab60f01be706
--- /dev/null
+++ b/gradle/variables.gradle
@@ -0,0 +1,10 @@
+ext {
+    kotlin_version = '1.7.10'
+    hilt_version = '2.43.2'
+    nav_version = '2.4.2'
+    tor_version = '0.4.5.12-2'
+    obfs4_version = '0.0.12'
+    junit_version = '5.7.2'
+    mockk_version = '1.10.4'
+    ktlint_plugin_version = '10.2.1'
+}
diff --git a/mailbox-android/build.gradle b/mailbox-android/build.gradle
index b90aa3a89cb46cda4626a0c5b983c59faad7454a..7f0aa388a6f3740cf3e7d5e9776157375dcac03b 100644
--- a/mailbox-android/build.gradle
+++ b/mailbox-android/build.gradle
@@ -10,6 +10,10 @@ plugins {
     id 'checkstyle' // only needed for Java code
 }
 
+checkstyle {
+    configFile = new File('../config/checkstyle/checkstyle.xml')
+}
+
 android {
     compileSdkVersion 32
     buildToolsVersion "32.0.0"
@@ -148,5 +152,5 @@ tasks.withType(MergeResources) {
     dependsOn unpackTorBinaries
 }
 
-apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
-apply from: "${rootProject.rootDir}/gradle/checkstyle.gradle"
+apply from: "../gradle/ktlint.gradle"
+apply from: "../gradle/checkstyle.gradle"
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt
index 918541561ffd28db6cb217b45024757e2e3dbd19..02ce9bc366c3deab30161e44d7e5fcce4a2e239b 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt
@@ -32,8 +32,10 @@ import org.briarproject.mailbox.core.db.DatabaseConfig
 import org.briarproject.mailbox.core.files.FileProvider
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.system.DozeWatchdog
+import org.briarproject.mailbox.core.system.System
 import java.io.File
 import javax.inject.Singleton
+import kotlin.system.exitProcess
 
 @Module(
     includes = [
@@ -73,4 +75,8 @@ internal class AppModule {
     @Singleton
     @Provides
     fun provideDozeHelper(): DozeHelper = DozeHelperImpl()
+
+    @Singleton
+    @Provides
+    fun provideSystem() = System { code -> exitProcess(code) }
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
index e03afbf92355bf008dca8be3141fa1291101f98e..9063966276531a1f217a494dbeb3bd881128b8f9 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
@@ -42,11 +42,11 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCC
 import org.briarproject.mailbox.core.system.AndroidExecutor
 import org.briarproject.mailbox.core.system.AndroidWakeLock
 import org.briarproject.mailbox.core.system.AndroidWakeLockManager
+import org.briarproject.mailbox.core.system.System
 import org.briarproject.mailbox.core.util.LogUtils.warn
 import org.slf4j.LoggerFactory.getLogger
 import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
-import kotlin.system.exitProcess
 
 @AndroidEntryPoint
 class MailboxService : Service() {
@@ -83,6 +83,9 @@ class MailboxService : Service() {
     @Inject
     internal lateinit var androidExecutor: AndroidExecutor
 
+    @Inject
+    internal lateinit var system: System
+
     private lateinit var lifecycleWakeLock: AndroidWakeLock
 
     override fun onCreate() {
@@ -179,7 +182,7 @@ class MailboxService : Service() {
             }
             stopSelf()
             LOG.info("Exiting")
-            exitProcess(1)
+            system.exit(1)
         }
     }
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteFragment.kt
index dcd9c7aafe81a2ed6ce77ed52f0df215aa57eb95..874cddc3108f73d7578e3359e744fa3cbf22ac8f 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteFragment.kt
@@ -27,8 +27,9 @@ import android.widget.Button
 import androidx.fragment.app.Fragment
 import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.R
+import org.briarproject.mailbox.core.system.System
 import org.slf4j.LoggerFactory.getLogger
-import kotlin.system.exitProcess
+import javax.inject.Inject
 
 @AndroidEntryPoint
 class WipeCompleteFragment : Fragment() {
@@ -37,6 +38,9 @@ class WipeCompleteFragment : Fragment() {
         private val LOG = getLogger(WipeCompleteFragment::class.java)
     }
 
+    @Inject
+    internal lateinit var system: System
+
     private lateinit var button: Button
 
     override fun onCreateView(
@@ -52,7 +56,7 @@ class WipeCompleteFragment : Fragment() {
 
         button.setOnClickListener {
             LOG.info("Exiting")
-            exitProcess(0)
+            system.exit(0)
         }
     }
 
diff --git a/mailbox-cli/build.gradle b/mailbox-cli/build.gradle
index af3ca3157a02eb65c1757126bb02d0a1e62a9860..531841c63d53dd9ead0f5bf18cf562cf8c934bb7 100644
--- a/mailbox-cli/build.gradle
+++ b/mailbox-cli/build.gradle
@@ -10,29 +10,18 @@ plugins {
     id 'org.jetbrains.kotlin.jvm'
     id 'org.jetbrains.kotlin.kapt'
     id "org.jlleitschuh.gradle.ktlint" version "$ktlint_plugin_version"
-    id 'checkstyle'
 }
 
 sourceCompatibility = 1.8
 targetCompatibility = 1.8
 
-configurations {
-    tor
-}
-
 dependencies {
     implementation project(path: ':mailbox-core', configuration: 'default')
+    implementation project(path: ':mailbox-lib', configuration: 'default')
 
     implementation "ch.qos.logback:logback-classic:1.2.10"
     implementation 'com.github.ajalt:clikt:2.2.0'
 
-    def jna_version = '5.8.0'
-    implementation "net.java.dev.jna:jna:$jna_version"
-    implementation "net.java.dev.jna:jna-platform:$jna_version"
-    tor "org.briarproject:tor-linux:$tor_version"
-    tor "org.briarproject:obfs4proxy-linux:$obfs4_version"
-
-    implementation "com.google.dagger:hilt-core:$hilt_version"
     kapt "com.google.dagger:hilt-compiler:$hilt_version"
 
     testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version"
@@ -52,7 +41,7 @@ test {
     }
 }
 
-apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
+apply from: "../gradle/ktlint.gradle"
 
 // At the moment for non-Android projects we need to explicitly mark the code generated by kapt
 // as 'generated source code' for correct highlighting and resolve in IDE.
@@ -64,31 +53,6 @@ idea {
     }
 }
 
-def torBinariesDir = 'src/main/resources'
-
-task cleanTorBinaries {
-    doLast {
-        delete fileTree(torBinariesDir) { include '*.zip' }
-    }
-}
-
-clean.dependsOn cleanTorBinaries
-
-task unpackTorBinaries {
-    doLast {
-        copy {
-            from configurations.tor.collect { zipTree(it) }
-            into torBinariesDir
-        }
-    }
-    dependsOn cleanTorBinaries
-}
-
-processResources {
-    inputs.dir torBinariesDir
-    dependsOn unpackTorBinaries
-}
-
 void jarFactory(Jar jarTask, jarArchitecture) {
     jarTask.doFirst {
         println 'Building ' + jarArchitecture + ' version has started'
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
index a2292d4b0b66303b48957d712b91430fcd6e7009..da436f36811783ebe5a1f13d05d44fd73a554bfb 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
@@ -25,21 +25,9 @@ import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.parameters.options.counted
 import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
-import kotlinx.coroutines.flow.takeWhile
-import kotlinx.coroutines.runBlocking
-import org.briarproject.mailbox.core.CoreEagerSingletons
-import org.briarproject.mailbox.core.JavaCliEagerSingletons
-import org.briarproject.mailbox.core.db.TransactionManager
-import org.briarproject.mailbox.core.lifecycle.LifecycleManager
-import org.briarproject.mailbox.core.setup.QrCodeEncoder
-import org.briarproject.mailbox.core.setup.SetupManager
-import org.briarproject.mailbox.core.setup.WipeManager
 import org.briarproject.mailbox.core.system.InvalidIdException
-import org.briarproject.mailbox.core.tor.TorPlugin
-import org.briarproject.mailbox.core.tor.TorState
+import org.briarproject.mailbox.lib.Mailbox
 import org.slf4j.LoggerFactory.getLogger
-import javax.inject.Inject
-import kotlin.system.exitProcess
 
 class Main : CliktCommand(
     name = "briar-mailbox",
@@ -62,30 +50,6 @@ class Main : CliktCommand(
     ).counted()
     private val setupToken: String? by option("--setup-token", hidden = true)
 
-    @Inject
-    internal lateinit var coreEagerSingletons: CoreEagerSingletons
-
-    @Inject
-    internal lateinit var javaCliEagerSingletons: JavaCliEagerSingletons
-
-    @Inject
-    internal lateinit var lifecycleManager: LifecycleManager
-
-    @Inject
-    internal lateinit var db: TransactionManager
-
-    @Inject
-    internal lateinit var setupManager: SetupManager
-
-    @Inject
-    internal lateinit var wipeManager: WipeManager
-
-    @Inject
-    internal lateinit var torPlugin: TorPlugin
-
-    @Inject
-    internal lateinit var qrCodeEncoder: QrCodeEncoder
-
     override fun run() {
         // logging
         val levelNamed = when {
@@ -107,56 +71,46 @@ class Main : CliktCommand(
         getLogger(this.javaClass).debug("Hello Mailbox")
         println("Hello Mailbox")
 
-        val javaCliComponent = DaggerJavaCliComponent.builder().build()
-        javaCliComponent.inject(this)
+        val mailbox = Mailbox()
 
         if (wipe) {
-            wipeManager.wipeFilesOnly()
+            mailbox.wipeFilesOnly()
             println("Mailbox wiped successfully \\o/")
-            exitProcess(0)
+            mailbox.getSystem().exit(0)
         }
-        startLifecycle()
+        startLifecycle(mailbox)
     }
 
-    private fun startLifecycle() {
+    private fun startLifecycle(mailbox: Mailbox) {
         Runtime.getRuntime().addShutdownHook(
             Thread {
-                lifecycleManager.stopServices(false)
-                lifecycleManager.waitForShutdown()
+                mailbox.stopLifecycle(false)
+                mailbox.waitForShutdown()
             }
         )
 
-        lifecycleManager.startServices()
-        lifecycleManager.waitForStartup()
+        mailbox.startLifecycle()
 
         if (setupToken != null) {
             try {
-                setupManager.setToken(setupToken, null)
+                mailbox.setSetupToken(setupToken!!)
             } catch (e: InvalidIdException) {
                 System.err.println("Invalid setup token")
-                exitProcess(1)
+                mailbox.getSystem().exit(1)
             }
         }
 
-        val ownerTokenExists = db.read { txn ->
-            setupManager.getOwnerToken(txn) != null
-        }
+        val ownerTokenExists = mailbox.getOwnerToken() != null
         if (!ownerTokenExists) {
             if (debug) {
-                val token = setupToken ?: db.read { setupManager.getSetupToken(it) }
+                val token = setupToken ?: mailbox.getSetupToken()
                 println(
                     "curl -v -H \"Authorization: Bearer $token\" -X PUT " +
                         "http://localhost:8000/setup"
                 )
             }
-            // If not set up, show QR code for manual setup
-            runBlocking {
-                // wait until Tor becomes active and published the onion service
-                torPlugin.state.takeWhile { state ->
-                    state != TorState.Published
-                }.collect { }
-            }
-            qrCodeEncoder.getQrCodeBitMatrix()?.let {
+            mailbox.waitForTorPublished()
+            mailbox.getQrCode()?.let {
                 println(QrCodeRenderer.getQrString(it))
             }
         }
diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle
index f26a8013c7b01af2bbcc7e039b63c23f1b9a4d7d..4395acef1da32b74275837c741a5ad04e94a9a01 100644
--- a/mailbox-core/build.gradle
+++ b/mailbox-core/build.gradle
@@ -7,6 +7,10 @@ plugins {
     id 'checkstyle'
 }
 
+checkstyle {
+    configFile = new File('../config/checkstyle/checkstyle.xml')
+}
+
 sourceCompatibility = 1.8
 targetCompatibility = 1.8
 
@@ -67,4 +71,4 @@ idea {
     }
 }
 
-apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
+apply from: "../gradle/ktlint.gradle"
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
index 1a76a857ec0ac89251afd7ea148309f8f52c569c..a668fa64ad7579d762c072bc0281a1e934bda9ae 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
@@ -37,6 +37,7 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.LIFE
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS
 import org.briarproject.mailbox.core.setup.WipeManager
+import org.briarproject.mailbox.core.system.System
 import org.briarproject.mailbox.core.util.LogUtils.info
 import org.briarproject.mailbox.core.util.LogUtils.logDuration
 import org.briarproject.mailbox.core.util.LogUtils.logException
@@ -52,12 +53,12 @@ import javax.annotation.concurrent.GuardedBy
 import javax.annotation.concurrent.ThreadSafe
 import javax.inject.Inject
 import kotlin.concurrent.thread
-import kotlin.system.exitProcess
 
 @ThreadSafe
 internal class LifecycleManagerImpl @Inject constructor(
     private val db: Database,
     private val wipeManager: WipeManager,
+    private val system: System,
 ) :
     LifecycleManager, MigrationListener {
 
@@ -219,7 +220,7 @@ internal class LifecycleManagerImpl @Inject constructor(
             // deadlock by itself.
             if (stopped && exitAfterStopping) {
                 LOG.info("Exiting")
-                exitProcess(0)
+                system.exit(0)
             }
         }
     }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/System.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/System.java
new file mode 100644
index 0000000000000000000000000000000000000000..527e6d0667d47b069143e6ec2746d7294131cab7
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/System.java
@@ -0,0 +1,24 @@
+/*
+ *     Briar Mailbox
+ *     Copyright (C) 2021-2022  The Briar Project
+ *
+ *     This program is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU Affero General Public License as
+ *     published by the Free Software Foundation, either version 3 of the
+ *     License, or (at your option) any later version.
+ *
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU Affero General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Affero General Public License
+ *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.briarproject.mailbox.core.system;
+
+public interface System {
+	void exit(int code);
+}
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
index a9d9d98a97103d6b2e00a2c4b7f3fc8de946fa18..0aaf46a163f8be9b637b074dc7f2ecd4da084576 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
@@ -14,6 +14,7 @@ import org.briarproject.mailbox.core.server.WebServerModule
 import org.briarproject.mailbox.core.settings.SettingsModule
 import org.briarproject.mailbox.core.setup.SetupModule
 import org.briarproject.mailbox.core.system.Clock
+import org.briarproject.mailbox.core.system.System
 import org.briarproject.mailbox.core.system.TestTaskSchedulerModule
 import java.io.File
 import java.util.concurrent.Executor
@@ -35,7 +36,11 @@ import javax.inject.Singleton
 internal class TestModule(private val tempDir: File) {
     @Singleton
     @Provides
-    fun provideClock() = Clock { System.currentTimeMillis() }
+    fun provideClock() = Clock { java.lang.System.currentTimeMillis() }
+
+    @Singleton
+    @Provides
+    fun provideSystem() = System { /* do nothing on exit */ }
 
     @Singleton
     @Provides
diff --git a/mailbox-cli/.gitignore b/mailbox-lib/.gitignore
similarity index 100%
rename from mailbox-cli/.gitignore
rename to mailbox-lib/.gitignore
diff --git a/mailbox-lib/build.gradle b/mailbox-lib/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..3e200c658032afad89975ebec3cc155316cfb90b
--- /dev/null
+++ b/mailbox-lib/build.gradle
@@ -0,0 +1,86 @@
+plugins {
+    id 'java-library'
+    id 'idea'
+    id 'org.jetbrains.kotlin.jvm'
+    id 'org.jetbrains.kotlin.kapt'
+    id "org.jlleitschuh.gradle.ktlint" version "$ktlint_plugin_version"
+    id 'checkstyle'
+}
+
+checkstyle {
+    configFile = new File('../config/checkstyle/checkstyle.xml')
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+configurations {
+    tor
+}
+
+dependencies {
+    implementation project(path: ':mailbox-core', configuration: 'default')
+
+    def jna_version = '5.8.0'
+    implementation "net.java.dev.jna:jna:$jna_version"
+    implementation "net.java.dev.jna:jna-platform:$jna_version"
+    tor "org.briarproject:tor-linux:$tor_version"
+    tor "org.briarproject:obfs4proxy-linux:$obfs4_version"
+
+    implementation "com.google.dagger:hilt-core:$hilt_version"
+    kapt "com.google.dagger:hilt-compiler:$hilt_version"
+
+    testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version"
+    testImplementation "org.junit.jupiter:junit-jupiter-params:$junit_version"
+    testImplementation "org.junit.jupiter:junit-jupiter-engine:$junit_version"
+    testImplementation "io.mockk:mockk:$mockk_version"
+    testImplementation "ch.qos.logback:logback-classic:1.2.10"
+}
+
+test {
+    useJUnitPlatform()
+    testLogging {
+        events "passed", "skipped", "failed"
+    }
+}
+
+apply from: "../gradle/ktlint.gradle"
+
+// At the moment for non-Android projects we need to explicitly mark the code generated by kapt
+// as 'generated source code' for correct highlighting and resolve in IDE.
+idea {
+    module {
+        sourceDirs += file('build/generated/source/kapt/main')
+        testSourceDirs += file('build/generated/source/kapt/test')
+        generatedSourceDirs += file('build/generated/source/kapt/main')
+    }
+}
+
+def torBinariesDir = 'src/main/resources'
+
+task cleanTorBinaries {
+    doLast {
+        delete fileTree(torBinariesDir) { include '*.zip' }
+    }
+}
+
+clean.dependsOn cleanTorBinaries
+
+task unpackTorBinaries {
+    doLast {
+        copy {
+            from configurations.tor.collect { zipTree(it) }
+            into torBinariesDir
+        }
+    }
+    dependsOn cleanTorBinaries
+}
+
+processResources {
+    inputs.dir torBinariesDir
+    dependsOn unpackTorBinaries
+}
+
+tasks.withType(Test) {
+    systemProperty 'java.library.path', 'libs'
+}
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/core/MailboxLibEagerSingletons.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/MailboxLibEagerSingletons.kt
new file mode 100644
index 0000000000000000000000000000000000000000..079af827749d824257f9f92d98a6b58843f57e8c
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/MailboxLibEagerSingletons.kt
@@ -0,0 +1,30 @@
+/*
+ *     Briar Mailbox
+ *     Copyright (C) 2021-2022  The Briar Project
+ *
+ *     This program is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU Affero General Public License as
+ *     published by the Free Software Foundation, either version 3 of the
+ *     License, or (at your option) any later version.
+ *
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU Affero General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Affero General Public License
+ *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.briarproject.mailbox.core
+
+import org.briarproject.mailbox.core.system.TaskScheduler
+import org.briarproject.mailbox.core.tor.TorPlugin
+import javax.inject.Inject
+
+@Suppress("unused")
+internal class MailboxLibEagerSingletons @Inject constructor(
+    val taskScheduler: TaskScheduler,
+    val javaTorPlugin: TorPlugin,
+)
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
similarity index 96%
rename from mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
rename to mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
index c2fec04fd94e3c8aaec06292d8dc2f49ecb2838c..904f84f13efc5ea4849783c2db2233ebd80ed8a1 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
@@ -41,7 +41,7 @@ import javax.inject.Singleton
 
 @Module
 @InstallIn(SingletonComponent::class)
-internal class JavaTorModule {
+class JavaTorModule {
 
     companion object {
         private val LOG: Logger = getLogger(JavaTorModule::class.java)
@@ -109,7 +109,7 @@ internal class JavaTorModule {
 
     @Provides
     @Singleton
-    fun provideNetworkManager(networkManager: JavaCliNetworkManager): NetworkManager {
+    fun provideNetworkManager(networkManager: MailboxLibNetworkManager): NetworkManager {
         return networkManager
     }
 
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java
similarity index 100%
rename from mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java
rename to mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaCliNetworkManager.java b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/MailboxLibNetworkManager.java
similarity index 91%
rename from mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaCliNetworkManager.java
rename to mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/MailboxLibNetworkManager.java
index bb32ed6ccbeb5975d23bfbf29d72a01048c820cb..d7484cb13f8aab4a3d334d4b5930496da3f7b9c2 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaCliNetworkManager.java
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/MailboxLibNetworkManager.java
@@ -33,12 +33,12 @@ import static org.briarproject.mailbox.core.util.LogUtils.logException;
 import static org.briarproject.mailbox.core.util.NetworkUtils.getNetworkInterfaces;
 import static org.slf4j.LoggerFactory.getLogger;
 
-class JavaCliNetworkManager implements NetworkManager {
+public class MailboxLibNetworkManager implements NetworkManager {
 
-	private static final Logger LOG = getLogger(JavaCliNetworkManager.class);
+	private static final Logger LOG = getLogger(MailboxLibNetworkManager.class);
 
 	@Inject
-	JavaCliNetworkManager() {
+	MailboxLibNetworkManager() {
 	}
 
 	@Override
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/AbstractMailbox.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/AbstractMailbox.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c07974ee1378df1ca6c30b1e0d4196edb94c5bc7
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/AbstractMailbox.kt
@@ -0,0 +1,135 @@
+/*
+ *     Briar Mailbox
+ *     Copyright (C) 2021-2022  The Briar Project
+ *
+ *     This program is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU Affero General Public License as
+ *     published by the Free Software Foundation, either version 3 of the
+ *     License, or (at your option) any later version.
+ *
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU Affero General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Affero General Public License
+ *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.briarproject.mailbox.lib
+
+import com.google.zxing.common.BitMatrix
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.runBlocking
+import org.briarproject.mailbox.core.CoreEagerSingletons
+import org.briarproject.mailbox.core.MailboxLibEagerSingletons
+import org.briarproject.mailbox.core.db.TransactionManager
+import org.briarproject.mailbox.core.lifecycle.LifecycleManager
+import org.briarproject.mailbox.core.setup.QrCodeEncoder
+import org.briarproject.mailbox.core.setup.SetupManager
+import org.briarproject.mailbox.core.setup.WipeManager
+import org.briarproject.mailbox.core.system.System
+import org.briarproject.mailbox.core.tor.TorPlugin
+import org.briarproject.mailbox.core.tor.TorState
+import org.briarproject.mailbox.core.util.LogUtils.info
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory.getLogger
+import java.io.File
+import javax.inject.Inject
+
+abstract class AbstractMailbox(protected val customDataDir: File? = null) {
+
+    companion object {
+        val LOG: Logger = getLogger(AbstractMailbox::class.java)
+    }
+
+    @Inject
+    internal lateinit var coreEagerSingletons: CoreEagerSingletons
+
+    @Inject
+    internal lateinit var mailboxLibEagerSingletons: MailboxLibEagerSingletons
+
+    @Inject
+    internal lateinit var lifecycleManager: LifecycleManager
+
+    @Inject
+    internal lateinit var db: TransactionManager
+
+    @Inject
+    internal lateinit var setupManager: SetupManager
+
+    @Inject
+    internal lateinit var wipeManager: WipeManager
+
+    @Inject
+    internal lateinit var torPlugin: TorPlugin
+
+    @Inject
+    internal lateinit var qrCodeEncoder: QrCodeEncoder
+
+    @Inject
+    internal lateinit var system: System
+
+    fun wipeFilesOnly() {
+        wipeManager.wipeFilesOnly()
+        LOG.info { "Mailbox wiped successfully \\o/" }
+    }
+
+    fun startLifecycle() {
+        LOG.info { "Starting lifecycle" }
+        lifecycleManager.startServices()
+        LOG.info { "Waiting for startup" }
+        lifecycleManager.waitForStartup()
+        LOG.info { "Startup finished" }
+    }
+
+    fun stopLifecycle(exitAfterStopping: Boolean) {
+        LOG.info { "Stopping lifecycle" }
+        lifecycleManager.stopServices(exitAfterStopping)
+        LOG.info { "Waiting for shutdown" }
+        lifecycleManager.waitForShutdown()
+        LOG.info { "Shutdown finished" }
+    }
+
+    fun setSetupToken(token: String) {
+        setupManager.setToken(token, null)
+    }
+
+    fun setOwnerToken(token: String) {
+        setupManager.setToken(null, token)
+    }
+
+    fun waitForTorPublished() {
+        LOG.info { "Waiting for Tor to publish hidden service" }
+        runBlocking {
+            // wait until Tor becomes active and published the onion service
+            torPlugin.state.takeWhile { state ->
+                state != TorState.Published
+            }.collect { }
+        }
+        LOG.info { "Hidden service published" }
+    }
+
+    fun waitForShutdown() {
+        lifecycleManager.waitForShutdown()
+    }
+
+    fun getSetupToken(): String? {
+        return db.read { txn ->
+            setupManager.getSetupToken(txn)
+        }
+    }
+
+    fun getOwnerToken(): String? {
+        return db.read { txn ->
+            setupManager.getOwnerToken(txn)
+        }
+    }
+
+    fun getQrCode(): BitMatrix? {
+        return qrCodeEncoder.getQrCodeBitMatrix()
+    }
+
+    fun getSystem(): System = system
+}
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/Mailbox.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/Mailbox.kt
new file mode 100644
index 0000000000000000000000000000000000000000..115bb0717779cf5e3447358d70f9587b68b100ed
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/Mailbox.kt
@@ -0,0 +1,14 @@
+package org.briarproject.mailbox.lib
+
+import org.briarproject.mailbox.core.util.LogUtils.info
+import java.io.File
+
+class Mailbox(mailboxDir: File? = null) : AbstractMailbox(mailboxDir) {
+
+    init {
+        LOG.info { "Hello Mailbox" }
+        val mailboxLibComponent = DaggerMailboxLibComponent.builder()
+            .mailboxLibModule(MailboxLibModule(customDataDir)).build()
+        mailboxLibComponent.inject(this)
+    }
+}
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibComponent.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibComponent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..03ef11d02906328a201284370f86bea3c6988321
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibComponent.kt
@@ -0,0 +1,35 @@
+/*
+ *     Briar Mailbox
+ *     Copyright (C) 2021-2022  The Briar Project
+ *
+ *     This program is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU Affero General Public License as
+ *     published by the Free Software Foundation, either version 3 of the
+ *     License, or (at your option) any later version.
+ *
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU Affero General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Affero General Public License
+ *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.briarproject.mailbox.lib
+
+import dagger.Component
+import org.briarproject.mailbox.system.ProductionSystemModule
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+    modules = [
+        MailboxLibModule::class,
+        ProductionSystemModule::class,
+    ]
+)
+interface MailboxLibComponent {
+    fun inject(mailbox: Mailbox)
+}
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibModule.kt
similarity index 84%
rename from mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt
rename to mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibModule.kt
index 1e90dcadd2f9929bea369ae47a054aeb263a2685..7b8a068cb9fc90f0f3440e321471b3c6ab52a20a 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibModule.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.cli
+package org.briarproject.mailbox.lib
 
 import dagger.Module
 import dagger.Provides
@@ -51,10 +51,10 @@ import javax.inject.Singleton
     ]
 )
 @InstallIn(SingletonComponent::class)
-internal class JavaCliModule {
+open class MailboxLibModule(private val customDataDir: File? = null) {
 
     companion object {
-        private val LOG: Logger = getLogger(JavaCliModule::class.java)
+        private val LOG: Logger = getLogger(MailboxLibModule::class.java)
 
         private val DEFAULT_DATAHOME = System.getProperty("user.home") +
             separator + ".local" + separator + "share"
@@ -67,15 +67,19 @@ internal class JavaCliModule {
      * and otherwise it returns "~/.local/share/briar-mailbox".
      */
     private val dataDir: File by lazy {
-        val dataHome = when (val custom = System.getenv("XDG_DATA_HOME").orEmpty()) {
-            "" -> File(DEFAULT_DATAHOME)
-            else -> File(custom)
-        }
-        if (!dataHome.exists() || !dataHome.isDirectory) {
-            throw IOException("datahome missing or not a directory: ${dataHome.absolutePath}")
+        val dataDir = if (customDataDir != null) {
+            customDataDir
+        } else {
+            val dataHome = when (val custom = System.getenv("XDG_DATA_HOME").orEmpty()) {
+                "" -> File(DEFAULT_DATAHOME)
+                else -> File(custom)
+            }
+            if (!dataHome.exists() || !dataHome.isDirectory) {
+                throw IOException("datahome missing or not a directory: ${dataHome.absolutePath}")
+            }
+            File(dataHome.absolutePath + separator + DATAHOME_SUBDIR)
         }
 
-        val dataDir = File(dataHome.absolutePath + separator + DATAHOME_SUBDIR)
         if (!dataDir.exists() && !dataDir.mkdirs()) {
             throw IOException("datadir could not be created: ${dataDir.absolutePath}")
         } else if (!dataDir.isDirectory) {
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliComponent.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibTestComponent.kt
similarity index 79%
rename from mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliComponent.kt
rename to mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibTestComponent.kt
index 34a4cb42feaa370f761dcc85c892e29bf251afd6..98bbba5297d4da5533b50b20a17231c9dbba7562 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliComponent.kt
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/MailboxLibTestComponent.kt
@@ -17,17 +17,19 @@
  *
  */
 
-package org.briarproject.mailbox.cli
+package org.briarproject.mailbox.lib
 
 import dagger.Component
+import org.briarproject.mailbox.system.TestSystemModule
 import javax.inject.Singleton
 
 @Singleton
 @Component(
     modules = [
-        JavaCliModule::class,
+        MailboxLibModule::class,
+        TestSystemModule::class,
     ]
 )
-interface JavaCliComponent {
-    fun inject(main: Main)
+interface MailboxLibTestComponent {
+    fun inject(mailbox: TestMailbox)
 }
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/TestMailbox.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/TestMailbox.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3d1b0784c30237bb57efb24d3d6b06b80fa5f563
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/lib/TestMailbox.kt
@@ -0,0 +1,27 @@
+package org.briarproject.mailbox.lib
+
+import org.briarproject.mailbox.core.util.LogUtils.info
+import org.briarproject.mailbox.system.TestSystem
+import java.io.File
+import javax.inject.Inject
+
+class TestMailbox(mailboxDir: File? = null) : AbstractMailbox(mailboxDir) {
+
+    init {
+        LOG.info { "Hello Mailbox" }
+        val mailboxLibComponent = DaggerMailboxLibTestComponent.builder()
+            .mailboxLibModule(MailboxLibModule(customDataDir)).build()
+        mailboxLibComponent.inject(this)
+    }
+
+    @Inject
+    internal lateinit var testSystem: TestSystem
+
+    fun hasExited(): Boolean {
+        return testSystem.hasExited()
+    }
+
+    fun getExitCode(): Int {
+        return testSystem.getExitCode()
+    }
+}
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/system/ProductionSystem.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/ProductionSystem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2adc532201e5f22eaf226862f755ca837003355c
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/ProductionSystem.kt
@@ -0,0 +1,11 @@
+package org.briarproject.mailbox.system
+
+import org.briarproject.mailbox.core.system.System
+import kotlin.system.exitProcess
+
+internal class ProductionSystem : System {
+
+    override fun exit(code: Int) {
+        exitProcess(code)
+    }
+}
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/system/ProductionSystemModule.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/ProductionSystemModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8137fa1967ecd183bcccbd8bca9ca6ae4d892b63
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/ProductionSystemModule.kt
@@ -0,0 +1,18 @@
+package org.briarproject.mailbox.system
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.briarproject.mailbox.core.system.System
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal class ProductionSystemModule {
+
+    @Singleton
+    @Provides
+    fun provideSystem(): System = ProductionSystem()
+
+}
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/system/TestSystem.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/TestSystem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ea34f6a3cedf0ddebf1a30d74f1f7ec0d470e8b8
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/TestSystem.kt
@@ -0,0 +1,23 @@
+package org.briarproject.mailbox.system
+
+import org.briarproject.mailbox.core.system.System
+
+sealed interface Exited
+internal object NotExited : Exited
+internal class DidExit(val code: Int) : Exited
+
+internal class TestSystem(private var exited: Exited = NotExited) : System {
+
+    override fun exit(code: Int) {
+        exited = DidExit(code)
+    }
+
+    fun hasExited(): Boolean {
+        return exited is DidExit
+    }
+
+    fun getExitCode(): Int {
+        check(exited is DidExit)
+        return (exited as DidExit).code
+    }
+}
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/system/TestSystemModule.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/TestSystemModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b03e0166844129fc48f551c5c80ceda7b7efe9c5
--- /dev/null
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/system/TestSystemModule.kt
@@ -0,0 +1,22 @@
+package org.briarproject.mailbox.system
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.briarproject.mailbox.core.system.System
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal class TestSystemModule {
+
+    @Singleton
+    @Provides
+    fun provideSystem(system: TestSystem): System = system
+
+    @Singleton
+    @Provides
+    fun provideTestSystem(): TestSystem = TestSystem()
+
+}
diff --git a/mailbox-lib/src/test/java/org/briarproject/mailbox/lib/MailboxLibTest.kt b/mailbox-lib/src/test/java/org/briarproject/mailbox/lib/MailboxLibTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f602617ee2f1fc2d0d2a6b8dc2d9b4b9e37d5cc7
--- /dev/null
+++ b/mailbox-lib/src/test/java/org/briarproject/mailbox/lib/MailboxLibTest.kt
@@ -0,0 +1,42 @@
+/*
+ *     Briar Mailbox
+ *     Copyright (C) 2021-2022  The Briar Project
+ *
+ *     This program is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU Affero General Public License as
+ *     published by the Free Software Foundation, either version 3 of the
+ *     License, or (at your option) any later version.
+ *
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU Affero General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Affero General Public License
+ *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.briarproject.mailbox.lib
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+
+class MailboxLibTest {
+
+    @TempDir
+    lateinit var mailboxDataDirectory: File
+
+    @Test
+    fun testStartStopMailbox() {
+        val mailbox = TestMailbox(mailboxDataDirectory)
+        mailbox.startLifecycle()
+        mailbox.waitForTorPublished()
+        mailbox.stopLifecycle(true)
+        assertTrue(mailbox.hasExited())
+        assertEquals(0, mailbox.getExitCode())
+    }
+}
diff --git a/mailbox-lib/src/test/resources/logback.xml b/mailbox-lib/src/test/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2693dcc4b722ef1e36170866d3fbaf4d662639c9
--- /dev/null
+++ b/mailbox-lib/src/test/resources/logback.xml
@@ -0,0 +1,12 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="trace">
+        <appender-ref ref="STDOUT" />
+    </root>
+    <logger name="org.eclipse.jetty" level="INFO" />
+    <logger name="io.netty" level="INFO" />
+</configuration>
diff --git a/settings.gradle b/settings.gradle
index 8ef7b26e7968e306807f269b864b9a0fb096fee9..072e94e0a70507ae928b0a6220cd318236ab05d7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
 include ':mailbox-core'
 include ':mailbox-android'
 include ':mailbox-cli'
+include ':mailbox-lib'