Add onboarding screen to ask for Android permissions

This removes the old onboarding module and creates several onboarding sub modules.
- `:feature:onboarding:main` contains the navigation logic and binds the various onboarding modules together
- `:feature:onboarding:welcome` contains the welcome screen (`strings.xml` files have moved; so we need to update Weblate to point to the new location)
- `:feature:onboarding:permission` contains the new permission screen (needs a new component on Weblate)

<img src="https://user-images.githubusercontent.com/218061/278472767-127df045-6cdd-4d57-b222-658ae5e170ec.png" alt="image" width=400>

Closes #6262

Ask for contacts permission during onboarding
Instead of always requesting the contacts permission in the message list screen, ask during initial app setup (after the first account was successfully created). We want to avoid running into the problem where with broken Android versions repeatedly denying the permission prompt won't get rid of the prompt.

See #6261

this is really annoying
We're currently rewriting the account setup code and are not looking for external contributions implementing this feature.

diff --git a/app-feature-preview/build.gradle.kts b/app-feature-preview/build.gradle.kts
index 2d1beda7e45..6566d944c12 100644
--- a/app-feature-preview/build.gradle.kts
+++ b/app-feature-preview/build.gradle.kts
@@ -60,7 +60,7 @@ dependencies {
     implementation(projects.core.common)
     implementation(projects.mail.common)

-    implementation(projects.feature.onboarding)
+    implementation(projects.feature.onboarding.main)
     implementation(projects.feature.account.setup)
     implementation(projects.feature.account.edit)

diff --git a/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt b/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt
index aa80e7390e6..a5b1186ff2d 100644
--- a/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt
+++ b/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt
@@ -6,6 +6,7 @@ import app.k9mail.feature.account.edit.AccountEditExternalContract
 import app.k9mail.feature.account.edit.featureAccountEditModule
 import app.k9mail.feature.account.setup.AccountSetupExternalContract
 import app.k9mail.feature.account.setup.featureAccountSetupModule
+import app.k9mail.feature.onboarding.main.featureOnboardingModule
 import app.k9mail.feature.preview.account.AccountOwnerNameProvider
 import app.k9mail.feature.preview.account.InMemoryAccountStore
 import app.k9mail.feature.preview.auth.AndroidKeyStoreDirectoryProvider
@@ -18,6 +19,7 @@ import com.fsck.k9.mail.ssl.LocalKeyStore
 import com.fsck.k9.mail.ssl.TrustManagerFactory
 import com.fsck.k9.mail.ssl.TrustedSocketFactory
 import org.koin.core.module.Module
+import org.koin.core.qualifier.named
 import org.koin.dsl.binds
 import org.koin.dsl.module

@@ -41,9 +43,12 @@ val featureModule: Module = module {
     single { TrustManagerFactory.createInstance(get()) }
     single<TrustedSocketFactory> { DefaultTrustedSocketFactory(get(), get()) }
     single<OAuth2TokenProviderFactory> { RealOAuth2TokenProviderFactory(context = get()) }
+    single(named("ClientIdAppName")) { "App Name" }
+    single(named("ClientIdAppVersion")) { "App Version" }

     includes(
         accountModule,
+        featureOnboardingModule,
         featureAccountSetupModule,
         featureAccountEditModule,
     )
diff --git a/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt b/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt
index 5f1a99e19c4..20e0e70181b 100644
--- a/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt
+++ b/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt
@@ -7,10 +7,9 @@ import androidx.navigation.compose.NavHost
 import app.k9mail.feature.account.edit.navigation.accountEditRoute
 import app.k9mail.feature.account.edit.navigation.navigateToAccountEditIncomingServerSettings
 import app.k9mail.feature.account.setup.navigation.accountSetupRoute
-import app.k9mail.feature.account.setup.navigation.navigateToAccountSetup
-import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING
-import app.k9mail.feature.onboarding.navigation.navigateToOnboarding
-import app.k9mail.feature.onboarding.navigation.onboardingRoute
+import app.k9mail.feature.onboarding.main.navigation.NAVIGATION_ROUTE_ONBOARDING
+import app.k9mail.feature.onboarding.main.navigation.navigateToOnboarding
+import app.k9mail.feature.onboarding.main.navigation.onboardingRoute

 @Composable
 fun FeatureNavHost(
@@ -24,8 +23,11 @@ fun FeatureNavHost(
         modifier = modifier,
     ) {
         onboardingRoute(
-            onStart = { navController.navigateToAccountSetup() },
             onImport = { /* TODO */ },
+            onBack = navController::popBackStack,
+            onFinish = { accountUuid ->
+                navController.navigateToAccountEditIncomingServerSettings(accountUuid)
+            },
         )
         accountSetupRoute(
             onBack = navController::popBackStack,
diff --git a/core/android/permissions/build.gradle.kts b/core/android/permissions/build.gradle.kts
new file mode 100644
index 00000000000..41bb4a224c7
--- /dev/null
+++ b/core/android/permissions/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+    id(ThunderbirdPlugins.Library.android)
+}
+
+android {
+    namespace = "app.k9mail.core.android.permissions"
+}
+
+dependencies {
+    testImplementation(libs.androidx.test.core)
+    testImplementation(libs.robolectric)
+    testImplementation(libs.assertk)
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/AndroidPermissionChecker.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/AndroidPermissionChecker.kt
new file mode 100644
index 00000000000..371bf2db34d
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/AndroidPermissionChecker.kt
@@ -0,0 +1,38 @@
+package app.k9mail.core.android.permissions
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.core.content.ContextCompat
+
+/**
+ * Checks if a [Permission] has been granted to the app.
+ */
+class AndroidPermissionChecker(
+    private val context: Context,
+) : PermissionChecker {
+
+    override fun checkPermission(permission: Permission): PermissionState {
+        return when (permission) {
+            Permission.Contacts -> {
+                checkSelfPermission(Manifest.permission.READ_CONTACTS)
+            }
+            Permission.Notifications -> {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                    checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+                } else {
+                    PermissionState.GrantedImplicitly
+                }
+            }
+        }
+    }
+
+    private fun checkSelfPermission(permission: String): PermissionState {
+        return if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
+            PermissionState.Granted
+        } else {
+            PermissionState.Denied
+        }
+    }
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/CorePermissionsAndroidModule.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/CorePermissionsAndroidModule.kt
new file mode 100644
index 00000000000..dc1885345f1
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/CorePermissionsAndroidModule.kt
@@ -0,0 +1,8 @@
+package app.k9mail.core.android.permissions
+
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+val corePermissionsAndroidModule: Module = module {
+    factory<PermissionChecker> { AndroidPermissionChecker(context = get()) }
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/Permission.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/Permission.kt
new file mode 100644
index 00000000000..4ffaa43c3b1
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/Permission.kt
@@ -0,0 +1,9 @@
+package app.k9mail.core.android.permissions
+
+/**
+ * System permissions we ask for during onboarding.
+ */
+enum class Permission {
+    Contacts,
+    Notifications,
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionChecker.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionChecker.kt
new file mode 100644
index 00000000000..6713b34a53b
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionChecker.kt
@@ -0,0 +1,8 @@
+package app.k9mail.core.android.permissions
+
+/**
+ * Checks if a [Permission] has been granted to the app.
+ */
+interface PermissionChecker {
+    fun checkPermission(permission: Permission): PermissionState
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionState.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionState.kt
new file mode 100644
index 00000000000..fc123ae9f78
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionState.kt
@@ -0,0 +1,10 @@
+package app.k9mail.core.android.permissions
+
+enum class PermissionState {
+    /**
+     * The permission is not a runtime permission in the Android version we're running on.
+     */
+    GrantedImplicitly,
+    Granted,
+    Denied,
+}
diff --git a/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/image/ImageWithOverlayCoordinate.kt b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/image/ImageWithOverlayCoordinate.kt
new file mode 100644
index 00000000000..e64bf2b39df
--- /dev/null
+++ b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/image/ImageWithOverlayCoordinate.kt
@@ -0,0 +1,18 @@
+package app.k9mail.core.ui.compose.common.image
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.Dp
+
+/**
+ * An image with a coordinate to draw a (smaller) overlay icon on top of it.
+ *
+ * Example: An icon representing an Android permission with an overlay icon to indicate whether the permission has been
+ * granted.
+ */
+@Immutable
+data class ImageWithOverlayCoordinate(
+    val image: ImageVector,
+    val overlayOffsetX: Dp,
+    val overlayOffsetY: Dp,
+)
diff --git a/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/visibility/VisibilityModifiers.kt b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/visibility/VisibilityModifiers.kt
new file mode 100644
index 00000000000..2425ecc4f74
--- /dev/null
+++ b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/visibility/VisibilityModifiers.kt
@@ -0,0 +1,9 @@
+package app.k9mail.core.ui.compose.common.visibility
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+
+/**
+ * Sets a composable to be fully transparent when it should be hidden (but still present for layout purposes).
+ */
+fun Modifier.hide(hide: Boolean) = alpha(if (hide) 0f else 1f)
diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/DelayedCircularProgressIndicator.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/DelayedCircularProgressIndicator.kt
new file mode 100644
index 00000000000..2a84d192224
--- /dev/null
+++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/DelayedCircularProgressIndicator.kt
@@ -0,0 +1,43 @@
+package app.k9mail.core.ui.compose.designsystem.atom
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import app.k9mail.core.ui.compose.common.visibility.hide
+import app.k9mail.core.ui.compose.theme.MainTheme
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private const val LOADING_INDICATOR_DELAY = 500L
+
+/**
+ * Only show a [CircularProgressIndicator] after [LOADING_INDICATOR_DELAY] ms.
+ *
+ * Use this to avoid flashing a loading indicator for loads that are usually very fast.
+ */
+@Composable
+fun DelayedCircularProgressIndicator(
+    modifier: Modifier = Modifier,
+    color: Color = MainTheme.colors.secondary,
+) {
+    var progressIndicatorVisible by remember { mutableStateOf(false) }
+
+    LaunchedEffect(key1 = Unit) {
+        launch {
+            delay(LOADING_INDICATOR_DELAY)
+            progressIndicatorVisible = true
+        }
+    }
+
+    CircularProgressIndicator(
+        modifier = Modifier
+            .hide(!progressIndicatorVisible)
+            .then(modifier),
+        color = color,
+    )
+}
diff --git a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt
index 5ff0bf0464f..c20a75a8d06 100644
--- a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt
+++ b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt
@@ -1,6 +1,9 @@
 package app.k9mail.core.ui.compose.theme

 import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Cancel
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.CheckCircle
 import androidx.compose.material.icons.filled.Error
 import androidx.compose.material.icons.filled.MoveToInbox
 import androidx.compose.material.icons.filled.Notifications
@@ -48,6 +51,12 @@ object Icons {

         val user: ImageVector
             get() = MaterialIcons.Filled.AccountCircle
+
+        val check: ImageVector
+            get() = MaterialIcons.Filled.CheckCircle
+
+        val cancel: ImageVector
+            get() = MaterialIcons.Filled.Cancel
     }

     object Outlined {
diff --git a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/IconsWithBottomRightOverlay.kt b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/IconsWithBottomRightOverlay.kt
new file mode 100644
index 00000000000..220d926c2e3
--- /dev/null
+++ b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/IconsWithBottomRightOverlay.kt
@@ -0,0 +1,27 @@
+package app.k9mail.core.ui.compose.theme
+
+import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.ui.unit.dp
+import app.k9mail.core.ui.compose.common.image.ImageWithOverlayCoordinate
+import androidx.compose.material.icons.Icons as MaterialIcons
+
+// We're using "by lazy" so not all icons are loaded into memory as soon as the object is accessed. But once a property
+// is accessed we want to retain the `ImageWithOverlayCoordinate` instance.
+object IconsWithBottomRightOverlay {
+    val person: ImageWithOverlayCoordinate by lazy {
+        ImageWithOverlayCoordinate(
+            image = MaterialIcons.Filled.Person,
+            overlayOffsetX = 24.dp,
+            overlayOffsetY = 20.dp,
+        )
+    }
+
+    val notification: ImageWithOverlayCoordinate by lazy {
+        ImageWithOverlayCoordinate(
+            image = MaterialIcons.Filled.Notifications,
+            overlayOffsetX = 23.dp,
+            overlayOffsetY = 19.dp,
+        )
+    }
+}
diff --git a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt
index 148270cf7b2..fc5b60c70cc 100644
--- a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt
+++ b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt
@@ -16,6 +16,7 @@ data class Sizes(
     val huger: Dp = 384.dp,

     val icon: Dp = 24.dp,
+    val largeIcon: Dp = 32.dp,

     val topBarHeight: Dp = 56.dp,
     val bottomBarHeight: Dp = 56.dp,
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt
index 5d4eb2ce88c..74083567529 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt
@@ -3,6 +3,7 @@ package app.k9mail.feature.account.setup.navigation
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
 import app.k9mail.core.ui.compose.common.navigation.deepLinkComposable
 import app.k9mail.feature.account.setup.ui.AccountSetupScreen

@@ -23,3 +24,16 @@ fun NavGraphBuilder.accountSetupRoute(
         )
     }
 }
+
+fun NavGraphBuilder.nestedAccountSetupRoute(
+    route: String,
+    onBack: () -> Unit,
+    onFinish: (String) -> Unit,
+) {
+    composable(route) {
+        AccountSetupScreen(
+            onBack = onBack,
+            onFinish = onFinish,
+        )
+    }
+}
diff --git a/feature/launcher/build.gradle.kts b/feature/launcher/build.gradle.kts
index c7d08c7ade1..72ce81fb321 100644
--- a/feature/launcher/build.gradle.kts
+++ b/feature/launcher/build.gradle.kts
@@ -19,7 +19,7 @@ android {
 dependencies {
     implementation(projects.core.ui.compose.designsystem)
     implementation(projects.app.ui.base)
-    implementation(projects.feature.onboarding)
+    implementation(projects.feature.onboarding.main)
     implementation(projects.feature.account.setup)
     implementation(projects.feature.account.edit)

diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt
index af2050bb921..0b9dd7106b1 100644
--- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt
+++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt
@@ -11,7 +11,7 @@ import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_
 import app.k9mail.feature.account.edit.navigation.withAccountUuid
 import app.k9mail.feature.account.setup.navigation.NAVIGATION_ROUTE_ACCOUNT_SETUP
 import app.k9mail.feature.launcher.ui.FeatureLauncherApp
-import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING
+import app.k9mail.feature.onboarding.main.navigation.NAVIGATION_ROUTE_ONBOARDING
 import com.fsck.k9.ui.base.K9Activity

 class FeatureLauncherActivity : K9Activity() {
diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt
index 1e5a5e47355..ddab819657b 100644
--- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt
+++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt
@@ -2,10 +2,12 @@ package app.k9mail.feature.launcher.di

 import app.k9mail.feature.account.edit.featureAccountEditModule
 import app.k9mail.feature.account.setup.featureAccountSetupModule
+import app.k9mail.feature.onboarding.main.featureOnboardingModule
 import org.koin.dsl.module

 val featureLauncherModule = module {
     includes(
+        featureOnboardingModule,
         featureAccountSetupModule,
         featureAccountEditModule,
     )
diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt
index 5de11098ba7..0ca3213dbe3 100644
--- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt
+++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt
@@ -7,11 +7,10 @@ import androidx.navigation.compose.NavHost
 import app.k9mail.core.ui.compose.common.activity.LocalActivity
 import app.k9mail.feature.account.edit.navigation.accountEditRoute
 import app.k9mail.feature.account.setup.navigation.accountSetupRoute
-import app.k9mail.feature.account.setup.navigation.navigateToAccountSetup
 import app.k9mail.feature.launcher.FeatureLauncherExternalContract.AccountSetupFinishedLauncher
 import app.k9mail.feature.launcher.FeatureLauncherExternalContract.ImportSettingsLauncher
-import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING
-import app.k9mail.feature.onboarding.navigation.onboardingRoute
+import app.k9mail.feature.onboarding.main.navigation.NAVIGATION_ROUTE_ONBOARDING
+import app.k9mail.feature.onboarding.main.navigation.onboardingRoute
 import org.koin.compose.koinInject

 @Composable
@@ -30,8 +29,9 @@ fun FeatureLauncherNavHost(
         modifier = modifier,
     ) {
         onboardingRoute(
-            onStart = { navController.navigateToAccountSetup() },
             onImport = { importSettingsLauncher.launch() },
+            onBack = onBack,
+            onFinish = { accountUuid -> accountSetupFinishedLauncher.launch(accountUuid) },
         )
         accountSetupRoute(
             onBack = onBack,
diff --git a/feature/onboarding/main/build.gradle.kts b/feature/onboarding/main/build.gradle.kts
new file mode 100644
index 00000000000..9b807013746
--- /dev/null
+++ b/feature/onboarding/main/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+    id(ThunderbirdPlugins.Library.androidCompose)
+}
+
+android {
+    namespace = "app.k9mail.feature.onboarding.main"
+    resourcePrefix = "onboarding_main_"
+
+    buildTypes {
+        debug {
+            manifestPlaceholders["appAuthRedirectScheme"] = "FIXME: override this in your app project"
+        }
+        release {
+            manifestPlaceholders["appAuthRedirectScheme"] = "FIXME: override this in your app project"
+        }
+    }
+}
+
+dependencies {
+    implementation(projects.core.ui.compose.designsystem)
+    implementation(projects.feature.onboarding.welcome)
+    implementation(projects.feature.account.setup)
+    implementation(projects.feature.onboarding.permissions)
+}
diff --git a/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/OnboardingModule.kt b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/OnboardingModule.kt
new file mode 100644
index 00000000000..fc02cc33380
--- /dev/null
+++ b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/OnboardingModule.kt
@@ -0,0 +1,11 @@
+package app.k9mail.feature.onboarding.main
+
+import app.k9mail.feature.onboarding.permissions.featureOnboardingPermissionsModule
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+val featureOnboardingModule: Module = module {
+    includes(
+        featureOnboardingPermissionsModule,
+    )
+}
diff --git a/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavHost.kt b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavHost.kt
new file mode 100644
index 00000000000..3d49d1da3c3
--- /dev/null
+++ b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavHost.kt
@@ -0,0 +1,63 @@
+package app.k9mail.feature.onboarding.main.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.navigation.NavController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import app.k9mail.feature.account.setup.navigation.nestedAccountSetupRoute
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsScreen
+import app.k9mail.feature.onboarding.welcome.ui.OnboardingScreen
+
+private const val NESTED_NAVIGATION_ROUTE_WELCOME = "welcome"
+private const val NESTED_NAVIGATION_ROUTE_ACCOUNT_SETUP = "account_setup"
+private const val NESTED_NAVIGATION_ROUTE_PERMISSIONS = "permissions"
+
+private fun NavController.navigateToAccountSetup() {
+    navigate(NESTED_NAVIGATION_ROUTE_ACCOUNT_SETUP)
+}
+
+private fun NavController.navigateToPermissions() {
+    navigate(NESTED_NAVIGATION_ROUTE_PERMISSIONS)
+}
+
+@Composable
+fun OnboardingNavHost(
+    onImport: () -> Unit,
+    onBack: () -> Unit,
+    onFinish: (String) -> Unit,
+) {
+    val navController = rememberNavController()
+    var accountUuid by rememberSaveable { mutableStateOf<String?>(null) }
+
+    NavHost(
+        navController = navController,
+        startDestination = NESTED_NAVIGATION_ROUTE_WELCOME,
+    ) {
+        composable(route = NESTED_NAVIGATION_ROUTE_WELCOME) {
+            OnboardingScreen(
+                onStartClick = { navController.navigateToAccountSetup() },
+                onImportClick = onImport,
+            )
+        }
+
+        nestedAccountSetupRoute(
+            route = NESTED_NAVIGATION_ROUTE_ACCOUNT_SETUP,
+            onBack = onBack,
+            onFinish = { createdAccountUuid ->
+                accountUuid = createdAccountUuid
+                navController.navigateToPermissions()
+            },
+        )
+
+        composable(route = NESTED_NAVIGATION_ROUTE_PERMISSIONS) {
+            PermissionsScreen(
+                onNext = { onFinish(requireNotNull(accountUuid)) },
+            )
+        }
+    }
+}
diff --git a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/navigation/OnboardingNavigation.kt b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavigation.kt
similarity index 69%
rename from feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/navigation/OnboardingNavigation.kt
rename to feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavigation.kt
index a9e2263afbb..16cd0b3e78b 100644
--- a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/navigation/OnboardingNavigation.kt
+++ b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavigation.kt
@@ -1,10 +1,9 @@
-package app.k9mail.feature.onboarding.navigation
+package app.k9mail.feature.onboarding.main.navigation

 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.NavOptions
 import app.k9mail.core.ui.compose.common.navigation.deepLinkComposable
-import app.k9mail.feature.onboarding.ui.OnboardingScreen

 const val NAVIGATION_ROUTE_ONBOARDING = "onboarding"

@@ -15,13 +14,15 @@ fun NavController.navigateToOnboarding(
 }

 fun NavGraphBuilder.onboardingRoute(
-    onStart: () -> Unit,
     onImport: () -> Unit,
+    onBack: () -> Unit,
+    onFinish: (String) -> Unit,
 ) {
     deepLinkComposable(route = NAVIGATION_ROUTE_ONBOARDING) {
-        OnboardingScreen(
-            onStartClick = onStart,
-            onImportClick = onImport,
+        OnboardingNavHost(
+            onImport = onImport,
+            onBack = onBack,
+            onFinish = onFinish,
         )
     }
 }
diff --git a/feature/onboarding/permissions/build.gradle.kts b/feature/onboarding/permissions/build.gradle.kts
new file mode 100644
index 00000000000..240d12f9f46
--- /dev/null
+++ b/feature/onboarding/permissions/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+    id(ThunderbirdPlugins.Library.androidCompose)
+}
+
+android {
+    namespace = "app.k9mail.feature.onboarding.permissions"
+    resourcePrefix = "onboarding_permissions_"
+}
+
+dependencies {
+    implementation(projects.core.ui.compose.designsystem)
+    implementation(projects.core.android.permissions)
+    implementation(projects.feature.account.common)
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/PermissionsModule.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/PermissionsModule.kt
new file mode 100644
index 00000000000..7a4b1b7e773
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/PermissionsModule.kt
@@ -0,0 +1,21 @@
+package app.k9mail.feature.onboarding.permissions
+
+import app.k9mail.core.android.permissions.corePermissionsAndroidModule
+import app.k9mail.feature.onboarding.permissions.domain.PermissionsDomainContract.UseCase
+import app.k9mail.feature.onboarding.permissions.domain.usecase.CheckPermission
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+val featureOnboardingPermissionsModule: Module = module {
+    includes(corePermissionsAndroidModule)
+
+    factory<UseCase.CheckPermission> { CheckPermission(permissionChecker = get()) }
+
+    viewModel {
+        PermissionsViewModel(
+            checkPermission = get(),
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/PermissionsDomainContract.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/PermissionsDomainContract.kt
new file mode 100644
index 00000000000..ef1fb950bcb
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/PermissionsDomainContract.kt
@@ -0,0 +1,14 @@
+package app.k9mail.feature.onboarding.permissions.domain
+
+import app.k9mail.core.android.permissions.Permission
+import app.k9mail.core.android.permissions.PermissionState
+
+interface PermissionsDomainContract {
+
+    interface UseCase {
+
+        fun interface CheckPermission {
+            operator fun invoke(permission: Permission): PermissionState
+        }
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/usecase/CheckPermission.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/usecase/CheckPermission.kt
new file mode 100644
index 00000000000..5f53d40fdd5
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/usecase/CheckPermission.kt
@@ -0,0 +1,18 @@
+package app.k9mail.feature.onboarding.permissions.domain.usecase
+
+import app.k9mail.core.android.permissions.Permission
+import app.k9mail.core.android.permissions.PermissionChecker
+import app.k9mail.core.android.permissions.PermissionState
+import app.k9mail.feature.onboarding.permissions.domain.PermissionsDomainContract.UseCase
+
+/**
+ * Checks if a [Permission] has been granted to the app.
+ */
+class CheckPermission(
+    private val permissionChecker: PermissionChecker,
+) : UseCase.CheckPermission {
+
+    override fun invoke(permission: Permission): PermissionState {
+        return permissionChecker.checkPermission(permission)
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt
new file mode 100644
index 00000000000..e16efd40658
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt
@@ -0,0 +1,174 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import app.k9mail.core.ui.compose.common.image.ImageWithOverlayCoordinate
+import app.k9mail.core.ui.compose.designsystem.atom.Icon
+import app.k9mail.core.ui.compose.designsystem.atom.button.Button
+import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody2
+import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline6
+import app.k9mail.core.ui.compose.theme.Icons
+import app.k9mail.core.ui.compose.theme.IconsWithBottomRightOverlay
+import app.k9mail.core.ui.compose.theme.K9Theme
+import app.k9mail.core.ui.compose.theme.MainTheme
+import app.k9mail.feature.onboarding.permissions.R
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.UiPermissionState
+
+private val MAX_WIDTH = 500.dp
+
+@Composable
+internal fun PermissionBox(
+    icon: ImageWithOverlayCoordinate,
+    permissionState: UiPermissionState,
+    title: String,
+    description: String,
+    onAllowClick: () -> Unit,
+) {
+    Column(
+        modifier = Modifier
+            .width(MAX_WIDTH)
+            .padding(horizontal = MainTheme.spacings.double),
+    ) {
+        Row {
+            Box(
+                modifier = Modifier.padding(
+                    end = MainTheme.spacings.double,
+                    top = MainTheme.spacings.default,
+                    bottom = MainTheme.spacings.default,
+                ),
+            ) {
+                IconWithPermissionStateOverlay(icon, permissionState)
+            }
+            Column {
+                TextHeadline6(text = title)
+                TextBody2(text = description)
+            }
+        }
+
+        val buttonAlpha by animateFloatAsState(
+            targetValue = if (permissionState == UiPermissionState.Granted) 0f else 1f,
+            label = "AllowButtonAlpha",
+        )
+
+        Column(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalAlignment = Alignment.End,
+        ) {
+            Spacer(modifier = Modifier.height(MainTheme.spacings.default))
+
+            Button(
+                text = stringResource(R.string.onboarding_permissions_allow_button),
+                onClick = onAllowClick,
+                modifier = Modifier.alpha(buttonAlpha),
+            )
+        }
+    }
+}
+
+@Composable
+private fun IconWithPermissionStateOverlay(
+    icon: ImageWithOverlayCoordinate,
+    permissionState: UiPermissionState,
+) {
+    Box {
+        val iconSize = MainTheme.sizes.largeIcon
+        val overlayIconSize = iconSize / 2
+        val overlayIconOffset = overlayIconSize / 2
+        val scalingFactor = iconSize / icon.image.defaultHeight
+        val overlayOffsetX = (icon.overlayOffsetX * scalingFactor) - overlayIconOffset
+        val overlayOffsetY = (icon.overlayOffsetY * scalingFactor) - overlayIconOffset
+
+        Icon(
+            imageVector = icon.image,
+            modifier = Modifier.size(iconSize),
+        )
+
+        when (permissionState) {
+            UiPermissionState.Unknown -> Unit
+            UiPermissionState.Granted -> {
+                Icon(
+                    imageVector = Icons.Filled.check,
+                    tint = MainTheme.colors.success,
+                    modifier = Modifier
+                        .size(overlayIconSize)
+                        .offset(
+                            x = overlayOffsetX,
+                            y = overlayOffsetY,
+                        ),
+                )
+            }
+
+            UiPermissionState.Denied -> {
+                Icon(
+                    imageVector = Icons.Filled.cancel,
+                    tint = MainTheme.colors.warning,
+                    modifier = Modifier
+                        .size(overlayIconSize)
+                        .offset(
+                            x = overlayOffsetX,
+                            y = overlayOffsetY,
+                        ),
+                )
+            }
+        }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun PermissionBoxUnknownStatePreview() {
+    K9Theme {
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.person,
+            permissionState = UiPermissionState.Unknown,
+            title = "Contacts",
+            description = "Allow access to be able to display contact names and photos.",
+            onAllowClick = {},
+        )
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun PermissionBoxGrantedStatePreview() {
+    K9Theme {
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.person,
+            permissionState = UiPermissionState.Granted,
+            title = "Contacts",
+            description = "Allow access to be able to display contact names and photos.",
+            onAllowClick = {},
+        )
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun PermissionBoxDeniedStatePreview() {
+    K9Theme {
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.person,
+            permissionState = UiPermissionState.Denied,
+            title = "Contacts",
+            description = "Allow access to be able to display contact names and photos.",
+            onAllowClick = {},
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContent.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContent.kt
new file mode 100644
index 00000000000..127331b1aa5
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContent.kt
@@ -0,0 +1,198 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import app.k9mail.core.ui.compose.common.PreviewDevices
+import app.k9mail.core.ui.compose.common.visibility.hide
+import app.k9mail.core.ui.compose.designsystem.atom.DelayedCircularProgressIndicator
+import app.k9mail.core.ui.compose.designsystem.atom.Surface
+import app.k9mail.core.ui.compose.designsystem.atom.button.Button
+import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonText
+import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline5
+import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
+import app.k9mail.core.ui.compose.designsystem.template.Scaffold
+import app.k9mail.core.ui.compose.theme.IconsWithBottomRightOverlay
+import app.k9mail.core.ui.compose.theme.K9Theme
+import app.k9mail.core.ui.compose.theme.MainTheme
+import app.k9mail.feature.account.common.ui.AppTitleTopHeader
+import app.k9mail.feature.onboarding.permissions.R
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Event
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.State
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.UiPermissionState
+import app.k9mail.feature.account.common.R as CommonR
+
+@Composable
+internal fun PermissionsContent(
+    state: State,
+    onEvent: (Event) -> Unit,
+) {
+    val scrollState = rememberScrollState()
+
+    Scaffold(
+        bottomBar = {
+            BottomBar(state, onEvent, scrollState)
+        },
+    ) { innerPadding ->
+        ResponsiveWidthContainer(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(innerPadding),
+        ) {
+            Column(
+                verticalArrangement = Arrangement.SpaceBetween,
+                horizontalAlignment = Alignment.CenterHorizontally,
+                modifier = Modifier
+                    .fillMaxHeight()
+                    .verticalScroll(state = scrollState),
+            ) {
+                HeaderArea()
+
+                ContentArea(state, onEvent)
+
+                // This provides some bottom padding but is also necessary to make the vertical arrangement have the
+                // desired effect of putting ContentArea() in the middle.
+                Spacer(modifier = Modifier.height(MainTheme.spacings.double))
+            }
+        }
+    }
+}
+
+@Composable
+private fun HeaderArea() {
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        AppTitleTopHeader()
+
+        TextHeadline5(
+            text = stringResource(R.string.onboarding_permissions_screen_title),
+            modifier = Modifier.padding(horizontal = MainTheme.spacings.double),
+        )
+
+        Spacer(modifier = Modifier.height(MainTheme.spacings.double))
+    }
+}
+
+@Composable
+private fun ContentArea(state: State, onEvent: (Event) -> Unit) {
+    Column(
+        verticalArrangement = Arrangement.Center,
+        modifier = Modifier
+            .fillMaxHeight()
+            .padding(MainTheme.spacings.double),
+    ) {
+        if (state.isLoading) {
+            DelayedCircularProgressIndicator()
+        } else {
+            PermissionBoxes(state, onEvent)
+        }
+    }
+}
+
+@Composable
+private fun PermissionBoxes(
+    state: State,
+    onEvent: (Event) -> Unit,
+) {
+    PermissionBox(
+        icon = IconsWithBottomRightOverlay.person,
+        permissionState = state.contactsPermissionState,
+        title = stringResource(R.string.onboarding_permissions_contacts_title),
+        description = stringResource(R.string.onboarding_permissions_contacts_description),
+        onAllowClick = { onEvent(Event.AllowContactsPermissionClicked) },
+    )
+
+    if (state.isNotificationsPermissionVisible) {
+        Spacer(modifier = Modifier.height(MainTheme.spacings.quadruple))
+
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.notification,
+            permissionState = state.notificationsPermissionState,
+            title = stringResource(R.string.onboarding_permissions_notifications_title),
+            description = stringResource(R.string.onboarding_permissions_notifications_description),
+            onAllowClick = { onEvent(Event.AllowNotificationsPermissionClicked) },
+        )
+    }
+}
+
+@Composable
+private fun BottomBar(
+    state: State,
+    onEvent: (Event) -> Unit,
+    scrollState: ScrollState,
+) {
+    // Elevate the bottom bar when some scrollable content is "underneath" it
+    val elevation by animateDpAsState(
+        targetValue = if (scrollState.canScrollForward) 8.dp else 0.dp,
+        label = "BottomBarElevation",
+    )
+
+    Surface(elevation = elevation) {
+        ResponsiveWidthContainer(
+            modifier = Modifier.fillMaxWidth(),
+        ) {
+            Row(
+                modifier = Modifier
+                    .padding(
+                        start = MainTheme.spacings.double,
+                        end = MainTheme.spacings.double,
+                        top = MainTheme.spacings.half,
+                        bottom = MainTheme.spacings.half,
+                    )
+                    .fillMaxWidth(),
+                horizontalArrangement = Arrangement.End,
+            ) {
+                Crossfade(
+                    targetState = state.isNextButtonVisible,
+                    label = "NextButton",
+                ) { isNextButtonVisible ->
+                    Button(
+                        text = stringResource(CommonR.string.account_common_button_next),
+                        onClick = { onEvent(Event.NextClicked) },
+                        modifier = Modifier.hide(!isNextButtonVisible),
+                    )
+
+                    ButtonText(
+                        text = stringResource(R.string.onboarding_permissions_skip_button),
+                        onClick = { onEvent(Event.NextClicked) },
+                        modifier = Modifier.hide(isNextButtonVisible),
+                    )
+                }
+            }
+        }
+    }
+}
+
+@PreviewDevices
+@Composable
+internal fun PermissionContentPreview() {
+    K9Theme {
+        PermissionsContent(
+            state = State(
+                isLoading = false,
+                contactsPermissionState = UiPermissionState.Granted,
+                notificationsPermissionState = UiPermissionState.Denied,
+                isNotificationsPermissionVisible = true,
+                isNextButtonVisible = false,
+            ),
+            onEvent = {},
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContract.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContract.kt
new file mode 100644
index 00000000000..51b84771cb9
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContract.kt
@@ -0,0 +1,40 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
+
+interface PermissionsContract {
+    interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
+
+    data class State(
+        val isLoading: Boolean = true,
+        val contactsPermissionState: UiPermissionState = UiPermissionState.Unknown,
+        val notificationsPermissionState: UiPermissionState = UiPermissionState.Unknown,
+        val isNotificationsPermissionVisible: Boolean = false,
+        val isNextButtonVisible: Boolean = false,
+    )
+
+    sealed interface Event {
+        data object LoadPermissionState : Event
+
+        data object AllowContactsPermissionClicked : Event
+        data object AllowNotificationsPermissionClicked : Event
+
+        data class ContactsPermissionResult(val success: Boolean) : Event
+        data class NotificationsPermissionResult(val success: Boolean) : Event
+
+        data object NextClicked : Event
+    }
+
+    sealed interface Effect {
+        data object RequestContactsPermission : Effect
+        data object RequestNotificationsPermission : Effect
+
+        data object NavigateNext : Effect
+    }
+
+    enum class UiPermissionState {
+        Unknown,
+        Granted,
+        Denied,
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsScreen.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsScreen.kt
new file mode 100644
index 00000000000..4b8423987be
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsScreen.kt
@@ -0,0 +1,72 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import android.Manifest
+import android.os.Build
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import app.k9mail.core.android.permissions.PermissionState
+import app.k9mail.core.ui.compose.common.PreviewDevices
+import app.k9mail.core.ui.compose.common.mvi.observe
+import app.k9mail.core.ui.compose.theme.K9Theme
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Effect
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Event
+import kotlinx.coroutines.Dispatchers
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun PermissionsScreen(
+    viewModel: PermissionsContract.ViewModel = koinViewModel<PermissionsViewModel>(),
+    onNext: () -> Unit,
+) {
+    val contactsPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) { success ->
+        viewModel.event(Event.ContactsPermissionResult(success))
+    }
+
+    val notificationsPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) { success ->
+        viewModel.event(Event.NotificationsPermissionResult(success))
+    }
+
+    val (state, dispatch) = viewModel.observe { effect ->
+        when (effect) {
+            Effect.RequestContactsPermission -> contactsPermissionLauncher.requestContactsPermission()
+            Effect.RequestNotificationsPermission -> notificationsPermissionLauncher.requestNotificationsPermission()
+            Effect.NavigateNext -> onNext()
+        }
+    }
+
+    LaunchedEffect(key1 = Unit) {
+        dispatch(Event.LoadPermissionState)
+    }
+
+    PermissionsContent(
+        state = state.value,
+        onEvent = dispatch,
+    )
+}
+
+private fun ManagedActivityResultLauncher<String, Boolean>.requestContactsPermission() {
+    launch(Manifest.permission.READ_CONTACTS)
+}
+
+private fun ManagedActivityResultLauncher<String, Boolean>.requestNotificationsPermission() {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+        launch(Manifest.permission.POST_NOTIFICATIONS)
+    }
+}
+
+@PreviewDevices
+@Composable
+internal fun PermissionScreenPreview() {
+    K9Theme {
+        PermissionsScreen(
+            viewModel = PermissionsViewModel(
+                checkPermission = { PermissionState.Denied },
+                backgroundDispatcher = Dispatchers.Main.immediate,
+            ),
+            onNext = {},
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsViewModel.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsViewModel.kt
new file mode 100644
index 00000000000..7803a127aac
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsViewModel.kt
@@ -0,0 +1,108 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import androidx.lifecycle.viewModelScope
+import app.k9mail.core.android.permissions.Permission
+import app.k9mail.core.android.permissions.PermissionState
+import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
+import app.k9mail.feature.onboarding.permissions.domain.PermissionsDomainContract.UseCase
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Effect
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Event
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.State
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.UiPermissionState
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.ViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class PermissionsViewModel(
+    private val checkPermission: UseCase.CheckPermission,
+    private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : BaseViewModel<State, Event, Effect>(initialState = State(isLoading = true)), ViewModel {
+
+    override fun event(event: Event) {
+        when (event) {
+            Event.LoadPermissionState -> handleOneTimeEvent(event, ::loadPermissionState)
+            Event.AllowContactsPermissionClicked -> handleAllowContactsPermissionClicked()
+            Event.AllowNotificationsPermissionClicked -> handleAllowNotificationsPermissionClicked()
+            is Event.ContactsPermissionResult -> handleContactsPermissionResult(event.success)
+            is Event.NotificationsPermissionResult -> handleNotificationsPermissionResult(event.success)
+            Event.NextClicked -> handleNextClicked()
+        }
+    }
+
+    private fun loadPermissionState() {
+        viewModelScope.launch {
+            val (contactsPermissionState, notificationsPermissionState) = withContext(backgroundDispatcher) {
+                arrayOf(
+                    checkPermission(Permission.Contacts),
+                    checkPermission(Permission.Notifications),
+                )
+            }
+
+            val contactsUiPermissionState = when (contactsPermissionState) {
+                PermissionState.GrantedImplicitly -> error("Unexpected case")
+                PermissionState.Granted -> UiPermissionState.Granted
+                PermissionState.Denied -> UiPermissionState.Unknown
+            }
+            val notificationsUiPermissionState = when (notificationsPermissionState) {
+                PermissionState.GrantedImplicitly -> UiPermissionState.Unknown
+                PermissionState.Granted -> UiPermissionState.Granted
+                PermissionState.Denied -> UiPermissionState.Unknown
+            }
+            val isNotificationsPermissionVisible = notificationsPermissionState != PermissionState.GrantedImplicitly
+
+            updateState { state ->
+                state.copy(
+                    isLoading = false,
+                    contactsPermissionState = contactsUiPermissionState,
+                    notificationsPermissionState = notificationsUiPermissionState,
+                    isNotificationsPermissionVisible = isNotificationsPermissionVisible,
+                )
+            }
+            updateNextButtonState()
+        }
+    }
+
+    private fun handleAllowContactsPermissionClicked() {
+        emitEffect(Effect.RequestContactsPermission)
+    }
+
+    private fun handleAllowNotificationsPermissionClicked() {
+        emitEffect(Effect.RequestNotificationsPermission)
+    }
+
+    private fun handleContactsPermissionResult(success: Boolean) {
+        updateState { state ->
+            state.copy(
+                contactsPermissionState = if (success) UiPermissionState.Granted else UiPermissionState.Denied,
+            )
+        }
+        updateNextButtonState()
+    }
+
+    private fun handleNotificationsPermissionResult(success: Boolean) {
+        updateState { state ->
+            state.copy(
+                notificationsPermissionState = if (success) UiPermissionState.Granted else UiPermissionState.Denied,
+            )
+        }
+        updateNextButtonState()
+    }
+
+    private fun updateNextButtonState() {
+        updateState { state ->
+            val isContactsPermissionGranted = state.contactsPermissionState == UiPermissionState.Granted
+            val isNotificationsPermissionGrantedOrHidden = !state.isNotificationsPermissionVisible ||
+                state.notificationsPermissionState == UiPermissionState.Granted
+
+            state.copy(
+                isNextButtonVisible = isContactsPermissionGranted && isNotificationsPermissionGrantedOrHidden,
+            )
+        }
+    }
+
+    private fun handleNextClicked() {
+        emitEffect(Effect.NavigateNext)
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/res/values/strings.xml b/feature/onboarding/permissions/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..096560516ea
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Title of the onboarding screen to ask for Android permissions -->
+    <string name="onboarding_permissions_screen_title">Improve the experience</string>
+    <!-- Title of the section asking for the permission to read contacts -->
+    <string name="onboarding_permissions_contacts_title">Contacts</string>
+    <!-- Description of why we ask for the permission to read contacts -->
+    <string name="onboarding_permissions_contacts_description">Allow access to be able to provide contact suggestions and to display contact names and photos.</string>
+    <!-- Title of the section asking for permission to post notifications -->
+    <string name="onboarding_permissions_notifications_title">Notifications</string>
+    <!-- Description of why we ask the permission to post notifications -->
+    <string name="onboarding_permissions_notifications_description">Turn on notifications so you don\'t miss any emails.</string>
+    <!-- Text of the button that will trigger the system dialog to ask for an Android permission -->
+    <string name="onboarding_permissions_allow_button">Allow</string>
+    <!-- Text of the button to skip the onboarding screen to ask for Android permissions -->
+    <string name="onboarding_permissions_skip_button">Skip</string>
+</resources>
diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/welcome/build.gradle.kts
similarity index 60%
rename from feature/onboarding/build.gradle.kts
rename to feature/onboarding/welcome/build.gradle.kts
index fd113b5223c..e620724e191 100644
--- a/feature/onboarding/build.gradle.kts
+++ b/feature/onboarding/welcome/build.gradle.kts
@@ -3,8 +3,8 @@ plugins {
 }

 android {
-    namespace = "app.k9mail.feature.onboarding"
-    resourcePrefix = "onboarding_"
+    namespace = "app.k9mail.feature.onboarding.welcome"
+    resourcePrefix = "onboarding_welcome_"
 }

 dependencies {
diff --git a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingContent.kt b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingContent.kt
similarity index 98%
rename from feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingContent.kt
rename to feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingContent.kt
index d008a2bad21..1da819eec69 100644
--- a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingContent.kt
+++ b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingContent.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.onboarding.ui
+package app.k9mail.feature.onboarding.welcome.ui

 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
@@ -23,7 +23,7 @@ import app.k9mail.core.ui.compose.designsystem.template.ResponsiveContent
 import app.k9mail.core.ui.compose.theme.K9Theme
 import app.k9mail.core.ui.compose.theme.MainTheme
 import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
-import app.k9mail.feature.onboarding.R
+import app.k9mail.feature.onboarding.welcome.R

 @Composable
 internal fun OnboardingContent(
diff --git a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingScreen.kt b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingScreen.kt
similarity index 90%
rename from feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingScreen.kt
rename to feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingScreen.kt
index e1972a069e7..a274d9506fe 100644
--- a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingScreen.kt
+++ b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingScreen.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.onboarding.ui
+package app.k9mail.feature.onboarding.welcome.ui

 import androidx.compose.runtime.Composable
 import androidx.compose.ui.tooling.preview.Preview
diff --git a/feature/onboarding/src/main/res/drawable/onboarding_welcome_logo.xml b/feature/onboarding/welcome/src/main/res/drawable/onboarding_welcome_logo.xml
similarity index 100%
rename from feature/onboarding/src/main/res/drawable/onboarding_welcome_logo.xml
rename to feature/onboarding/welcome/src/main/res/drawable/onboarding_welcome_logo.xml
diff --git a/feature/onboarding/src/main/res/values-cs/strings.xml b/feature/onboarding/welcome/src/main/res/values-ar/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-cs/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ar/strings.xml
diff --git a/feature/onboarding/src/main/res/values-bg/strings.xml b/feature/onboarding/welcome/src/main/res/values-az/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-bg/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-az/strings.xml
diff --git a/feature/onboarding/src/main/res/values-es/strings.xml b/feature/onboarding/welcome/src/main/res/values-be/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-es/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-be/strings.xml
diff --git a/feature/onboarding/src/main/res/values-et/strings.xml b/feature/onboarding/welcome/src/main/res/values-bg/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-et/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-bg/strings.xml
diff --git a/feature/onboarding/src/main/res/values-eu/strings.xml b/feature/onboarding/welcome/src/main/res/values-br/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-eu/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-br/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ca/strings.xml b/feature/onboarding/welcome/src/main/res/values-ca/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ca/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ca/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fi/strings.xml b/feature/onboarding/welcome/src/main/res/values-cs/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fi/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-cs/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fr/strings.xml b/feature/onboarding/welcome/src/main/res/values-cy/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-cy/strings.xml
diff --git a/feature/onboarding/src/main/res/values-da/strings.xml b/feature/onboarding/welcome/src/main/res/values-da/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-da/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-da/strings.xml
diff --git a/feature/onboarding/src/main/res/values-de/strings.xml b/feature/onboarding/welcome/src/main/res/values-de/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-de/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-de/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fy/strings.xml b/feature/onboarding/welcome/src/main/res/values-el/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fy/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-el/strings.xml
diff --git a/feature/onboarding/src/main/res/values-it/strings.xml b/feature/onboarding/welcome/src/main/res/values-en-rGB/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-it/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-en-rGB/strings.xml
diff --git a/feature/onboarding/src/main/res/values-nl/strings.xml b/feature/onboarding/welcome/src/main/res/values-eo/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-nl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-eo/strings.xml
diff --git a/feature/onboarding/src/main/res/values-pt-rBR/strings.xml b/feature/onboarding/welcome/src/main/res/values-es/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-pt-rBR/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-es/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sv/strings.xml b/feature/onboarding/welcome/src/main/res/values-et/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sv/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-et/strings.xml
diff --git a/feature/onboarding/src/main/res/values-zh-rTW/strings.xml b/feature/onboarding/welcome/src/main/res/values-eu/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-zh-rTW/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-eu/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ar/strings.xml b/feature/onboarding/welcome/src/main/res/values-fa/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ar/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fa/strings.xml
diff --git a/feature/onboarding/src/main/res/values-az/strings.xml b/feature/onboarding/welcome/src/main/res/values-fi/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-az/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fi/strings.xml
diff --git a/feature/onboarding/src/main/res/values-be/strings.xml b/feature/onboarding/welcome/src/main/res/values-fr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-be/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-br/strings.xml b/feature/onboarding/welcome/src/main/res/values-fy/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-br/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fy/strings.xml
diff --git a/feature/onboarding/src/main/res/values-cy/strings.xml b/feature/onboarding/welcome/src/main/res/values-gd/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-cy/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-gd/strings.xml
diff --git a/feature/onboarding/src/main/res/values-el/strings.xml b/feature/onboarding/welcome/src/main/res/values-gl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-el/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-gl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hi/strings.xml b/feature/onboarding/welcome/src/main/res/values-hi/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hi/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hi/strings.xml
diff --git a/feature/onboarding/src/main/res/values-en-rGB/strings.xml b/feature/onboarding/welcome/src/main/res/values-hr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-en-rGB/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hu/strings.xml b/feature/onboarding/welcome/src/main/res/values-hu/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hu/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hu/strings.xml
diff --git a/feature/onboarding/src/main/res/values-eo/strings.xml b/feature/onboarding/welcome/src/main/res/values-hy/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-eo/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hy/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fa/strings.xml b/feature/onboarding/welcome/src/main/res/values-in/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fa/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-in/strings.xml
diff --git a/feature/onboarding/src/main/res/values-is/strings.xml b/feature/onboarding/welcome/src/main/res/values-is/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-is/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-is/strings.xml
diff --git a/feature/onboarding/src/main/res/values-gd/strings.xml b/feature/onboarding/welcome/src/main/res/values-it/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-gd/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-it/strings.xml
diff --git a/feature/onboarding/src/main/res/values-gl/strings.xml b/feature/onboarding/welcome/src/main/res/values-iw/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-gl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-iw/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ja/strings.xml b/feature/onboarding/welcome/src/main/res/values-ja/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ja/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ja/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hr/strings.xml b/feature/onboarding/welcome/src/main/res/values-ka/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ka/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hy/strings.xml b/feature/onboarding/welcome/src/main/res/values-ko/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hy/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ko/strings.xml
diff --git a/feature/onboarding/src/main/res/values-in/strings.xml b/feature/onboarding/welcome/src/main/res/values-lt/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-in/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-lt/strings.xml
diff --git a/feature/onboarding/src/main/res/values-iw/strings.xml b/feature/onboarding/welcome/src/main/res/values-lv/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-iw/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-lv/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ka/strings.xml b/feature/onboarding/welcome/src/main/res/values-ml/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ka/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ml/strings.xml
diff --git a/feature/onboarding/src/main/res/values-nb-rNO/strings.xml b/feature/onboarding/welcome/src/main/res/values-nb-rNO/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-nb-rNO/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-nb-rNO/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ko/strings.xml b/feature/onboarding/welcome/src/main/res/values-nl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ko/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-nl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-lt/strings.xml b/feature/onboarding/welcome/src/main/res/values-nn/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-lt/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-nn/strings.xml
diff --git a/feature/onboarding/src/main/res/values-pl/strings.xml b/feature/onboarding/welcome/src/main/res/values-pl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-pl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-pl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-lv/strings.xml b/feature/onboarding/welcome/src/main/res/values-pt-rBR/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-lv/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-pt-rBR/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ml/strings.xml b/feature/onboarding/welcome/src/main/res/values-pt-rPT/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ml/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-pt-rPT/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ro/strings.xml b/feature/onboarding/welcome/src/main/res/values-ro/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ro/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ro/strings.xml
diff --git a/feature/onboarding/src/main/res/values-nn/strings.xml b/feature/onboarding/welcome/src/main/res/values-ru/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-nn/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ru/strings.xml
diff --git a/feature/onboarding/src/main/res/values-pt-rPT/strings.xml b/feature/onboarding/welcome/src/main/res/values-sk/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-pt-rPT/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sk/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ru/strings.xml b/feature/onboarding/welcome/src/main/res/values-sl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ru/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sq/strings.xml b/feature/onboarding/welcome/src/main/res/values-sq/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sq/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sq/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sk/strings.xml b/feature/onboarding/welcome/src/main/res/values-sr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sk/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sl/strings.xml b/feature/onboarding/welcome/src/main/res/values-sv/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sv/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sr/strings.xml b/feature/onboarding/welcome/src/main/res/values-ta/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ta/strings.xml
diff --git a/feature/onboarding/src/main/res/values-tr/strings.xml b/feature/onboarding/welcome/src/main/res/values-tr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-tr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-tr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ta/strings.xml b/feature/onboarding/welcome/src/main/res/values-uk/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ta/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-uk/strings.xml
diff --git a/feature/onboarding/src/main/res/values-uk/strings.xml b/feature/onboarding/welcome/src/main/res/values-vi/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-uk/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-vi/strings.xml
diff --git a/feature/onboarding/src/main/res/values-zh-rCN/strings.xml b/feature/onboarding/welcome/src/main/res/values-zh-rCN/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-zh-rCN/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-zh-rCN/strings.xml
diff --git a/feature/onboarding/src/main/res/values-vi/strings.xml b/feature/onboarding/welcome/src/main/res/values-zh-rTW/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-vi/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-zh-rTW/strings.xml
diff --git a/feature/onboarding/src/main/res/values/strings.xml b/feature/onboarding/welcome/src/main/res/values/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values/strings.xml
rename to feature/onboarding/welcome/src/main/res/values/strings.xml
diff --git a/settings.gradle.kts b/settings.gradle.kts
index eb170d99c42..1969a84ab70 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -39,7 +39,12 @@ include(

 include(
     ":feature:launcher",
-    ":feature:onboarding",
+)
+
+include(
+    ":feature:onboarding:main",
+    ":feature:onboarding:welcome",
+    ":feature:onboarding:permissions",
 )

 include(
@@ -64,6 +69,7 @@ include(
     ":core:featureflags",
     ":core:testing",
     ":core:android:common",
+    ":core:android:permissions",
     ":core:android:testing",
     ":core:ui:compose:common",
     ":core:ui:compose:designsystem",

diff --git a/core/android/permissions/src/test/kotlin/app/k9mail/core/android/permissions/AndroidPermissionCheckerTest.kt b/core/android/permissions/src/test/kotlin/app/k9mail/core/android/permissions/AndroidPermissionCheckerTest.kt
new file mode 100644
index 00000000000..af2ec34c383
--- /dev/null
+++ b/core/android/permissions/src/test/kotlin/app/k9mail/core/android/permissions/AndroidPermissionCheckerTest.kt
@@ -0,0 +1,66 @@
+package app.k9mail.core.android.permissions
+
+import android.Manifest
+import android.app.Application
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+class AndroidPermissionCheckerTest {
+    private val application: Application = ApplicationProvider.getApplicationContext()
+    private val shadowApplication = Shadows.shadowOf(application)
+
+    private val permissionChecker = AndroidPermissionChecker(application)
+
+    @Test
+    fun `granted READ_CONTACTS permission`() {
+        shadowApplication.grantPermissions(Manifest.permission.READ_CONTACTS)
+
+        val result = permissionChecker.checkPermission(Permission.Contacts)
+
+        assertThat(result).isEqualTo(PermissionState.Granted)
+    }
+
+    @Test
+    fun `denied READ_CONTACTS permission`() {
+        shadowApplication.denyPermissions(Manifest.permission.READ_CONTACTS)
+
+        val result = permissionChecker.checkPermission(Permission.Contacts)
+
+        assertThat(result).isEqualTo(PermissionState.Denied)
+    }
+
+    @Test
+    fun `granted POST_NOTIFICATIONS permission`() {
+        shadowApplication.grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
+
+        val result = permissionChecker.checkPermission(Permission.Notifications)
+
+        assertThat(result).isEqualTo(PermissionState.Granted)
+    }
+
+    @Test
+    fun `denied POST_NOTIFICATIONS permission`() {
+        shadowApplication.denyPermissions(Manifest.permission.POST_NOTIFICATIONS)
+
+        val result = permissionChecker.checkPermission(Permission.Notifications)
+
+        assertThat(result).isEqualTo(PermissionState.Denied)
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.S_V2, maxSdk = Build.VERSION_CODES.S_V2)
+    fun `POST_NOTIFICATIONS permission not available`() {
+        val result = permissionChecker.checkPermission(Permission.Notifications)
+
+        assertThat(result).isEqualTo(PermissionState.GrantedImplicitly)
+    }
+}


Allow `<style>` tags inside the HTML body

Fixes #7163

MS Xbox Mail issue - Sanitizer deletes stylesheet
### Checklist

- [X] I have used the search function to see if someone else has already submitted the same bug report.
- [X] I will describe the problem with as much detail as possible.

### App version

6.7.10

### Where did you get the app from?

Other

### Android version

13

### Device model

Pixel 7 Pro

### Steps to reproduce

[eml.zip](https://github.com/thundernest/k-9/files/12558805/eml.zip)

### Expected behavior

The stylesheets get used for rendering (not deleted).
The Header is readable (displayed in separate lines not overlapping itself)

### Actual behavior

The `<style>` elements get deleted by the sanitizer, because they are wrapped within `<table>s` (for whatever reason).
These tables are within the `<head>` tag which in return also makes the sanitizer create the body (too) early. (compared to the actual body tag in the mail)

![grafik](https://github.com/thundernest/k-9/assets/5685512/fda1ed62-9482-45ee-82ca-079a444243b3)

### Logs

_No response_

> These tables are within the <head> tag which in return also makes the sanitizer create the body (too) early. (compared to the actual body tag in the mail)

This works as intended. The HTML parser creates an `html`,  a `head`, and a `body` element when it encounters the first `<table>` tag.

`<style>` tags are supported inside `<body>`. So we'll add them to our allow list.

./gradlew :app:html-cleaner:test --tests "app.k9mail.html.cleaner.HtmlSanitizerTest"

diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt
index 4fb58c28522..9102f3a5ec7 100644
--- a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt
+++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt
@@ -13,7 +13,7 @@ internal class BodyCleaner {

     init {
         val allowList = Safelist.relaxed()
-            .addTags("font", "hr", "ins", "del", "center", "map", "area", "title", "tt", "kbd", "samp", "var")
+            .addTags("font", "hr", "ins", "del", "center", "map", "area", "title", "tt", "kbd", "samp", "var", "style")
             .addAttributes("font", "color", "face", "size")
             .addAttributes("a", "name")
             .addAttributes("div", "align")

diff --git a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt
index 51f92121374..bd818122e86 100644
--- a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt
+++ b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt
@@ -485,6 +485,23 @@ class HtmlSanitizerTest {
         assertThat(result.toCompactString()).isEqualTo(html)
     }

+    @Test
+    fun `should keep 'style' element in body`() {
+        val html =
+            """
+            <html>
+              <head></head>
+              <body>
+                <style>.test { color: #000 }</style>
+              </body>
+            </html>
+            """.compactHtml()
+
+        val result = htmlSanitizer.sanitize(html)
+
+        assertThat(result.toCompactString()).isEqualTo(html)
+    }
+
     private fun assertTagsNotStripped(element: String) {
         val html = """<$element>some text</$element>"""


HTML-encode header field values when displaying message/rfc822 parts

When displaying `message/rfc822` parts, the app displays certain header fields (From, To, Subject, etc). But we forgot to HTML-encode these values. So an email address with a display name, e.g. `Sender <sender@domain.example>` was rendered as `Sender` (because everything inside `<>` is treated as an HTML tag).

This pull request changes the code to use `TextUtils.htmlEncode()` to add the proper HTML encoding.

Fixes #7165

handling of embedded .eml files - inline vs attachment
### Checklist

- [X] I have used the search function to see if someone else has already submitted the same bug report.
- [X] I will describe the problem with as much detail as possible.

### App version

6.7.10

### Where did you get the app from?

Other

### Android version

13

### Device model

Pixel 7 Pro

### Steps to reproduce

[dmarc-embedded-as-eml.zip](https://github.com/thundernest/k-9/files/12558942/dmarc-embedded-as-eml.zip)

### Expected behavior

K-9 shows embedded mail like a quoted mail below the actual mail text.
K-9 allows me to see Names and Mailaddresses of all recipients.

### Actual behavior

I can't view/see the Mailaddresses of recipients of the quoted mail.

Ok in this case they are the same due to it being a dmarc protected mail, but you get the point.
If people forward you any email as embedded, then you should be able to read the addresses in it :)
(maybe even view the headers at some point but that's low prio to me)

Also there is the issue, that K-9 just drops the .eml on regular forward.
Only forwarding the .eml as an attachment works.

Though K-9 forwards mails as attachment like this:
```
------CRKHS6K8IK0190Y3APDX7NBAFZT2CK
Content-Type: message/rfc822;
 name="Re: Implementing bluetooth speakers on OpenWrt?.eml"
Content-Disposition: attachment;
 filename="Re: Implementing bluetooth speakers on OpenWrt?.eml";
 size=11297
```

For comparison, what the mailing list did:
```
--===============2691111782005021451==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
```

What do you think about forwarding mails as an attachment using `Content-Disposition: inline` instead of `Content-Disposition: attachment;`?

### Logs

_No response_

Interesting fact: Thunderbird shows embedded .eml files whether they are inline or attachments
Thunderbird also allows me to save the .eml file even when it's inline (K-9 does not)

EDIT:
I just realized that mixing two html mails (inlined) will also make them share their stylesheets which might mess up the layout?
Maybe there should still be a difference (don't inline attached .eml files by default) or we would need an extra viewport for each embedded mail? (separate DOMs)
or those separate DOMs could be realized via frames or something.

./gradlew :app:core:testDebugUnitTest --tests "com.fsck.k9.mailstore.MessageViewInfoExtractorTest"

diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java
index b98c801464f..8b142e594be 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java
@@ -6,6 +6,8 @@
 import java.util.Date;
 import java.util.List;

+import android.text.TextUtils;
+
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
@@ -347,7 +349,7 @@ private void addHtmlDivider(StringBuilder html, Part part, boolean prependDivide
             String filename = getPartName(part);

             html.append("
");
-            html.append(filename);
+            html.append(TextUtils.htmlEncode(filename));
             html.append("
");
         }
     }
@@ -540,10 +542,10 @@ private void addMessageHeaderHtml(StringBuilder html, Message message)
      */
     private static void addTableRow(StringBuilder html, String header, String value) {
         html.append("
");
-        html.append(header);
+        html.append(TextUtils.htmlEncode(header));
         html.append("	");
         html.append("");
-        html.append(value);
+        html.append(TextUtils.htmlEncode(value));
         html.append("
");
     }

diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java b/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java
index 6de424caa23..8d5a057a8a3 100644
--- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java
+++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java
@@ -251,16 +251,16 @@ public void testTextPlusRfc822Message() throws MessagingException {
         // Create message/rfc822 body
         MimeMessage innerMessage = new MimeMessage();
         innerMessage.addSentDate(new Date(112, 2, 17), false);
-        innerMessage.setHeader("To", "to@example.com");
-        innerMessage.setSubject("Subject");
-        innerMessage.setFrom(new Address("from@example.com"));
+        innerMessage.setHeader("To", "Recipient <to@example.com>");
+        innerMessage.setSubject("Subject with characters that need HTML encoding: \"&<>");
+        innerMessage.setFrom(new Address("from@example.com", "Display name & email address"));
         MimeMessageHelper.setBody(innerMessage, innerBody);

         // Create multipart/mixed part
         MimeMultipart multipart = MimeMultipart.newInstance();
         MimeBodyPart bodyPart1 = new MimeBodyPart(textBody, "text/plain");
         MimeBodyPart bodyPart2 = new MimeBodyPart(innerMessage, "message/rfc822");
-        bodyPart2.setHeader("Content-Disposition", "inline; filename=\"message.eml\"");
+        bodyPart2.setHeader("Content-Disposition", "inline; filename=\"message&special_char.eml\"");
         multipart.addBodyPart(bodyPart1);
         multipart.addBodyPart(bodyPart2);

@@ -277,12 +277,12 @@ public void testTextPlusRfc822Message() throws MessagingException {
         String expectedText =
                 BODY_TEXT +
                 "\r\n\r\n" +
-                "----- message.eml ------------------------------------------------------" +
+                "----- message&special_char.eml -----------------------------------------" +
                 "\r\n\r\n" +
-                "From: from@example.com" + "\r\n" +
-                "To: to@example.com" + "\r\n" +
+                "From: Display name & email address <from@example.com>" + "\r\n" +
+                "To: Recipient <to@example.com>" + "\r\n" +
                 "Sent: Sat Mar 17 00:00:00 GMT+01:00 2012" + "\r\n" +
-                "Subject: Subject" + "\r\n" +
+                "Subject: Subject with characters that need HTML encoding: \"&<>" + "\r\n" +
                 "\r\n" +
                 innerBodyText;
         String expectedHtml =
@@ -290,20 +290,20 @@ public void testTextPlusRfc822Message() throws MessagingException {
                         BODY_TEXT_HTML +
                 "
" +
                 "
message.eml
" +
+                        "1px solid #000\">message&special_char.eml
" +
                 "
" +
                 "
" +
                 "From:	" +
-                "from@example.com	" +
+                "Display name & email address <from@example.com>	" +
                 "

" +
                 "To:	" +
-                "to@example.com	" +
+                "Recipient <to@example.com>	" +
                 "

" +
                 "Sent:	" +
                 "Sat Mar 17 00:00:00 GMT+01:00 2012	" +
                 "

" +
                 "Subject:	" +
-                "Subject	" +
+                "Subject with characters that need HTML encoding: "&<>	" +
                 "
" +
                 "
" +
                 "
" +