Repository: thundernest/k-9
Issue: NPE when trying to change folder settings of outbox folder

**Describe the bug**
I tried to change my folder settings for outbox folder but app crashes.

**To Reproduce**
Steps to reproduce the behavior:
1. Tab on sandwich icon to open side menu
2. Tab on 'Manage folders'
3. Tab on 'Outbox' (2nd list item)
4. App crashes

**Expected behavior**
App should show folder settings

**Environment (please complete the following information):**
 - K-9 Mail version: actual main (1a68adc8)
 - Android version: 10

**Logs**
FATAL EXCEPTION: main
    Process: com.fsck.k9.debug, PID: 9972
    java.lang.NullPointerException: cursor.getString(7) must not be null
        at com.fsck.k9.storage.messages.CursorFolderAccessor.getSyncClass(RetrieveFolderOperations.kt:151)
        at com.fsck.k9.mailstore.FolderRepository$getFolderDetails$1.map(FolderRepository.kt:62)
        at com.fsck.k9.mailstore.FolderRepository$getFolderDetails$1.map(FolderRepository.kt:8)
        at com.fsck.k9.storage.messages.RetrieveFolderOperations$getFolder$1.doDbWork(RetrieveFolderOperations.kt:44)
        at com.fsck.k9.mailstore.LockableDatabase.execute(LockableDatabase.java:273)
        at com.fsck.k9.storage.messages.RetrieveFolderOperations.getFolder(RetrieveFolderOperations.kt:32)
        at com.fsck.k9.storage.messages.RetrieveFolderOperations.getFolder(RetrieveFolderOperations.kt:16)
        at com.fsck.k9.storage.messages.K9MessageStore.getFolder(K9MessageStore.kt:94)
        at com.fsck.k9.mailstore.FolderRepository.getFolderDetails(FolderRepository.kt:52)
        at com.fsck.k9.ui.managefolders.FolderSettingsViewModel$loadFolderDetails$2.invokeSuspend(FolderSettingsViewModel.kt:74)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

Changed files:
app/ui/legacy/src/main/java/com/fsck/k9/ui/managefolders/FolderSettingsFragment.kt
app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt
app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt
app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt
app/storage/src/test/java/com/fsck/k9/storage/messages/FolderHelpers.kt
app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt
app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt

diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt b/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt
index cb80505de..f283618e8 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt
@@ -9,7 +9,7 @@ fun interface FolderMapper<T> {
 
 interface FolderDetailsAccessor {
     val id: Long
-    val serverId: String
+    val serverId: String?
     val name: String
     val type: FolderType
     val isLocalOnly: Boolean
@@ -22,4 +22,6 @@ interface FolderDetailsAccessor {
     val visibleLimit: Int
     val moreMessages: MoreMessages
     val messageCount: Int
+
+    fun serverIdOrThrow(): String
 }
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt
index 35972dd2c..cd18b0a85 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt
@@ -72,7 +72,7 @@ class FolderRepository(
         return messageStore.getFolders(excludeLocalOnly = true) { folder ->
             RemoteFolder(
                 id = folder.id,
-                serverId = folder.serverId,
+                serverId = folder.serverIdOrThrow(),
                 name = folder.name,
                 type = folder.type.toFolderType()
             )
@@ -85,7 +85,7 @@ class FolderRepository(
             RemoteFolderDetails(
                 folder = RemoteFolder(
                     id = folder.id,
-                    serverId = folder.serverId,
+                    serverId = folder.serverIdOrThrow(),
                     name = folder.name,
                     type = folder.type.toFolderType()
                 ),
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt
index 4c86055e3..5fc159cba 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt
@@ -17,7 +17,7 @@ class K9BackendStorage(
     }
 
     override fun getFolderServerIds(): List<String> {
-        return messageStore.getFolders(excludeLocalOnly = true) { folder -> folder.serverId }
+        return messageStore.getFolders(excludeLocalOnly = true) { folder -> folder.serverIdOrThrow() }
     }
 
     override fun createFolderUpdater(): BackendFolderUpdater {
diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt
index 3b3593291..abe16112b 100644
--- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt
+++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt
@@ -135,7 +135,7 @@ private class CursorFolderAccessor(val cursor: Cursor) : FolderDetailsAccessor {
     override val type: FolderType
         get() = cursor.getString(2).toFolderType()
 
-    override val serverId: String
+    override val serverId: String?
         get() = cursor.getString(3)
 
     override val isLocalOnly: Boolean
@@ -148,16 +148,16 @@ private class CursorFolderAccessor(val cursor: Cursor) : FolderDetailsAccessor {
         get() = cursor.getInt(6) == 1
 
     override val syncClass: FolderClass
-        get() = FolderClass.valueOf(cursor.getString(7))
+        get() = cursor.getString(7).toFolderClass(FolderClass.INHERITED)
 
     override val displayClass: FolderClass
-        get() = FolderClass.valueOf(cursor.getString(8))
+        get() = cursor.getString(8).toFolderClass(FolderClass.NO_CLASS)
 
     override val notifyClass: FolderClass
-        get() = FolderClass.valueOf(cursor.getString(9))
+        get() = cursor.getString(9).toFolderClass(FolderClass.INHERITED)
 
     override val pushClass: FolderClass
-        get() = FolderClass.valueOf(cursor.getString(10))
+        get() = cursor.getString(10).toFolderClass(FolderClass.SECOND_CLASS)
 
     override val visibleLimit: Int
         get() = cursor.getInt(11)
@@ -167,6 +167,14 @@ private class CursorFolderAccessor(val cursor: Cursor) : FolderDetailsAccessor {
 
     override val messageCount: Int
         get() = cursor.getInt(13)
+
+    override fun serverIdOrThrow(): String {
+        return serverId ?: error("No server ID found for folder '$name' ($id)")
+    }
+}
+
+private fun String?.toFolderClass(defaultValue: FolderClass): FolderClass {
+    return if (this == null) defaultValue else FolderClass.valueOf(this)
 }
 
 private val FOLDER_COLUMNS = arrayOf(
diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/FolderHelpers.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/FolderHelpers.kt
index e4942f9f8..b2ea71b55 100644
--- a/app/storage/src/test/java/com/fsck/k9/storage/messages/FolderHelpers.kt
+++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/FolderHelpers.kt
@@ -15,9 +15,9 @@ fun SQLiteDatabase.createFolder(
     integrate: Boolean = false,
     inTopGroup: Boolean = false,
     displayClass: String = "NO_CLASS",
-    syncClass: String = "INHERITED",
-    notifyClass: String = "INHERITED",
-    pushClass: String = "SECOND_CLASS",
+    syncClass: String? = "INHERITED",
+    notifyClass: String? = "INHERITED",
+    pushClass: String? = "SECOND_CLASS",
     lastUpdated: Long = 0L,
     unreadCount: Int = 0,
     visibleLimit: Int = 25,
diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt
index 4b08dafe7..4ab153ec2 100644
--- a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt
+++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt
@@ -3,6 +3,7 @@ package com.fsck.k9.storage.messages
 import com.fsck.k9.Account.FolderMode
 import com.fsck.k9.mail.FolderClass
 import com.fsck.k9.mail.FolderType
+import com.fsck.k9.mailstore.toDatabaseFolderType
 import com.fsck.k9.storage.RobolectricTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -45,6 +46,40 @@ class RetrieveFolderOperationsTest : RobolectricTest() {
         assertThat(result).isTrue()
     }
 
+    @Test
+    fun `get local folder`() {
+        val name = "Local outbox"
+        val folderId = sqliteDatabase.createFolder(
+            name = name,
+            type = FolderType.OUTBOX.toDatabaseFolderType(),
+            serverId = null,
+            isLocalOnly = true,
+            integrate = true,
+            inTopGroup = true,
+            displayClass = FolderClass.FIRST_CLASS.name,
+            syncClass = null,
+            notifyClass = null,
+            pushClass = null
+        )
+
+        val result = retrieveFolderOperations.getFolder(folderId) { folder ->
+            assertThat(folder.id).isEqualTo(folderId)
+            assertThat(folder.name).isEqualTo(name)
+            assertThat(folder.type).isEqualTo(FolderType.OUTBOX)
+            assertThat(folder.serverId).isNull()
+            assertThat(folder.isLocalOnly).isEqualTo(true)
+            assertThat(folder.isIntegrate).isEqualTo(true)
+            assertThat(folder.isInTopGroup).isEqualTo(true)
+            assertThat(folder.displayClass).isEqualTo(FolderClass.FIRST_CLASS)
+            assertThat(folder.syncClass).isEqualTo(FolderClass.INHERITED)
+            assertThat(folder.notifyClass).isEqualTo(FolderClass.INHERITED)
+            assertThat(folder.pushClass).isEqualTo(FolderClass.SECOND_CLASS)
+            true
+        }
+
+        assertThat(result).isTrue()
+    }
+
     @Test
     fun `get non-existent folder should return null`() {
         val result = retrieveFolderOperations.getFolder(1) { "failed" }
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/managefolders/FolderSettingsFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/managefolders/FolderSettingsFragment.kt
index 362382e61..6e78838d3 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/managefolders/FolderSettingsFragment.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/managefolders/FolderSettingsFragment.kt
@@ -77,6 +77,7 @@ class FolderSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFra
 
         setCategoryTitle(folderSettings)
         updateMenu()
+        setPreferenceVisibility(folderSettings)
     }
 
     private fun updateMenu() {
@@ -106,6 +107,13 @@ class FolderSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFra
         dialogFragment.show(requireFragmentManager(), TAG_CLEAR_FOLDER_CONFIRMATION)
     }
 
+    private fun setPreferenceVisibility(folderSettings: FolderSettingsData) {
+        if (folderSettings.folder.isLocalOnly) {
+            requirePreference<Preference>(PREFERENCE_POLL_CLASS).isVisible = false
+            requirePreference<Preference>(PREFERENCE_NOTIFICATION_CLASS).isVisible = false
+        }
+    }
+
     override fun doPositiveClick(dialogId: Int) {
         when (dialogId) {
             DIALOG_CLEAR_FOLDER -> {
@@ -118,6 +126,10 @@ class FolderSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFra
 
     override fun dialogCancelled(dialogId: Int) = Unit
 
+    private fun <T : Preference> requirePreference(key: String): T {
+        return findPreference(key) ?: error("Preference '$key' not found")
+    }
+
     companion object {
         const val EXTRA_ACCOUNT = "account"
         const val EXTRA_FOLDER_ID = "folderId"
@@ -127,5 +139,7 @@ class FolderSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFra
         private const val TAG_CLEAR_FOLDER_CONFIRMATION = "clear_folder_confirmation"
 
         private const val PREFERENCE_TOP_CATEGORY = "folder_settings"
+        private const val PREFERENCE_POLL_CLASS = "folder_settings_folder_sync_mode"
+        private const val PREFERENCE_NOTIFICATION_CLASS = "folder_settings_folder_notify_mode"
     }
 }