Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Change feature search to Kotlin
The `feature:search` is still written in Java, this needs to be changed to allow further changes to search to finally decouple `MessageList` from `LocalSearch`. This should help to fix the drawer cashes when the local state doesn't match the app state.

Additionally it should be split into api/implementation and changed to a Kotlin Multiplatform module.


Discussion hints:
nan

Pull request: Refactor search to kotlin - part 3
This finishes the Java to Kotlin rewrite of the search functionality.

The remaining `LocalMessageSearch` was converted to Kotlin and the most important change is replacing `Parcelable` with `Serializable`.

The search module is now a full kmp module and the implemetation has been moved under `impl-legacy` to prepare for a better api design in the future.

Resolves #9229

Test command: ./gradlew :feature:widget:unread:testDebugUnitTest --tests "app.k9mail.feature.widget.unread.UnreadWidgetDataProviderTest" :feature:navigation:drawer:siderail:testDebugUnitTest --tests "net.thunderbird.feature.navigation.drawer.siderail.data.UnifiedFolderRepositoryTest" :feature:search:impl-legacy:testDebugUnitTest --tests "net.thunderbird.feature.search.legacy.sql.SqlWhereClauseTest" --tests "net.thunderbird.feature.search.legacy.LocalMessageSearchTest" --tests "net.thunderbird.feature.search.legacy.serialization.LocalMessageSearchSerializerTest" --tests "net.thunderbird.feature.search.legacy.SearchConditionTreeNodeTest" :feature:navigation:drawer:dropdown:testDebugUnitTest --tests "net.thunderbird.feature.navigation.drawer.dropdown.data.UnifiedFolderRepositoryTest" :legacy:storage:testDebugUnitTest --tests "com.fsck.k9.storage.messages.RetrieveFolderOperationsTest" :legacy:core:testDebugUnitTest --tests "com.fsck.k9.controller.DefaultMessageCountsProviderTest"

Fix patch:
diff --git a/feature/navigation/drawer/dropdown/build.gradle.kts b/feature/navigation/drawer/dropdown/build.gradle.kts
index b88f3c7eb8a..77d5f86ff48 100644
--- a/feature/navigation/drawer/dropdown/build.gradle.kts
+++ b/feature/navigation/drawer/dropdown/build.gradle.kts
@@ -18,7 +18,7 @@ dependencies {
     implementation(projects.feature.mail.account.api)
     implementation(projects.feature.mail.folder.api)
 
-    implementation(projects.feature.search)
+    implementation(projects.feature.search.implLegacy)
     implementation(projects.legacy.mailstore)
     implementation(projects.legacy.message)
     implementation(projects.legacy.ui.folder)
diff --git a/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepository.kt b/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepository.kt
index 707f4828d10..decc9581653 100644
--- a/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepository.kt
+++ b/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepository.kt
@@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.map
 import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract
 import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
 import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
 
 internal class UnifiedFolderRepository(
     private val messageCountsProvider: MessageCountsProvider,
diff --git a/feature/navigation/drawer/siderail/build.gradle.kts b/feature/navigation/drawer/siderail/build.gradle.kts
index a84ee9d27da..347d1fe96af 100644
--- a/feature/navigation/drawer/siderail/build.gradle.kts
+++ b/feature/navigation/drawer/siderail/build.gradle.kts
@@ -20,7 +20,7 @@ dependencies {
     implementation(projects.core.android.account)
     implementation(projects.legacy.mailstore)
     implementation(projects.legacy.message)
-    implementation(projects.feature.search)
+    implementation(projects.feature.search.implLegacy)
     implementation(projects.legacy.ui.folder)
     implementation(projects.core.featureflag)
 
diff --git a/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepository.kt b/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepository.kt
index 5c8c94b2cc1..8acc7a57d67 100644
--- a/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepository.kt
+++ b/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepository.kt
@@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.map
 import net.thunderbird.feature.navigation.drawer.siderail.domain.DomainContract
 import net.thunderbird.feature.navigation.drawer.siderail.domain.entity.DisplayUnifiedFolder
 import net.thunderbird.feature.navigation.drawer.siderail.domain.entity.DisplayUnifiedFolderType
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
 
 internal class UnifiedFolderRepository(
     private val messageCountsProvider: MessageCountsProvider,
diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts
deleted file mode 100644
index b5318143117..00000000000
--- a/feature/search/build.gradle.kts
+++ /dev/null
@@ -1,12 +0,0 @@
-plugins {
-    id(ThunderbirdPlugins.Library.android)
-    alias(libs.plugins.kotlin.parcelize)
-}
-
-android {
-    namespace = "net.thunderbird.feature.search"
-}
-
-dependencies {
-    implementation(projects.feature.mail.account.api)
-}
diff --git a/feature/search/impl-legacy/build.gradle.kts b/feature/search/impl-legacy/build.gradle.kts
new file mode 100644
index 00000000000..14ac8e7e3a8
--- /dev/null
+++ b/feature/search/impl-legacy/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+    id(ThunderbirdPlugins.Library.kmp)
+    alias(libs.plugins.kotlin.serialization)
+}
+
+android {
+    namespace = "net.thunderbird.feature.search.legacy"
+}
+
+kotlin {
+    sourceSets {
+        commonMain.dependencies {
+            implementation(projects.feature.mail.account.api)
+
+            implementation(libs.kotlinx.serialization.json)
+        }
+    }
+}
diff --git a/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/LocalMessageSearch.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/LocalMessageSearch.kt
new file mode 100644
index 00000000000..62dbeae61da
--- /dev/null
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/LocalMessageSearch.kt
@@ -0,0 +1,207 @@
+package net.thunderbird.feature.search.legacy
+
+import kotlinx.serialization.Serializable
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.MessageSearchSpecification
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
+
+/**
+ * This class represents a local search.
+ *
+ * Removing conditions could be done through matching there unique id in the leafset and then
+ * removing them from the tree.
+ *
+ * TODO implement a complete addAllowedFolder method
+ * TODO conflicting conditions check on add
+ * TODO duplicate condition checking?
+ * TODO assign each node a unique id that's used to retrieve it from the leafset and remove.
+ */
+@Serializable
+@Suppress("TooManyFunctions")
+class LocalMessageSearch : MessageSearchSpecification {
+    var id: String = ""
+    var isManualSearch: Boolean = false
+
+    // since the uuid isn't in the message table it's not in the tree neither
+    private val accountUuidSet: MutableSet<String> = HashSet()
+    private var conditionsRoot: SearchConditionTreeNode? = null
+
+    /**
+     * Gets the leafset of the related condition tree.
+     *
+     * @return All the leaf conditions as a set.
+     */
+    var leafSet: MutableSet<SearchConditionTreeNode> = HashSet()
+        private set
+
+    /**
+     * Add a new account to the search. When no accounts are
+     * added manually we search all accounts on the device.
+     *
+     * @param uuid Uuid of the account to be added.
+     */
+    fun addAccountUuid(uuid: String) {
+        accountUuidSet.add(uuid)
+    }
+
+    /**
+     * Adds the provided node as the second argument of an AND
+     * clause to this node.
+     *
+     * @param field Message table field to match against.
+     * @param value Value to look for.
+     * @param attribute Attribute to use when matching.
+     */
+    fun and(field: MessageSearchField, value: String, attribute: SearchAttribute) {
+        and(SearchCondition(field, attribute, value))
+    }
+
+    /**
+     * Adds the provided condition as the second argument of an AND
+     * clause to this node.
+     *
+     * @param condition Condition to 'AND' with.
+     * @return New top AND node, new root.
+     */
+    fun and(condition: SearchCondition): SearchConditionTreeNode {
+        val node = SearchConditionTreeNode.Builder(condition).build()
+        return and(node)
+    }
+
+    /**
+     * Adds the provided node as the second argument of an AND
+     * clause to this node.
+     *
+     * @param node Node to 'AND' with.
+     * @return New top AND node, new root.
+     */
+    fun and(node: SearchConditionTreeNode): SearchConditionTreeNode {
+        leafSet.addAll(node.getLeafSet())
+
+        conditionsRoot = conditionsRoot?.let {
+            SearchConditionTreeNode.Builder(it)
+                .and(node)
+                .build()
+        } ?: node
+
+        return conditionsRoot!!
+    }
+
+    /**
+     * Adds the provided condition as the second argument of an OR
+     * clause to this node.
+     *
+     * @param condition Condition to 'OR' with.
+     * @return New top OR node, new root.
+     */
+    fun or(condition: SearchCondition): SearchConditionTreeNode {
+        val node = SearchConditionTreeNode.Builder(condition).build()
+        return or(node)
+    }
+
+    /**
+     * Adds the provided node as the second argument of an OR
+     * clause to this node.
+     *
+     * @param node Node to 'OR' with.
+     * @return New top OR node, new root.
+     */
+    fun or(node: SearchConditionTreeNode): SearchConditionTreeNode {
+        leafSet.addAll(node.getLeafSet())
+
+        conditionsRoot = conditionsRoot?.let {
+            SearchConditionTreeNode.Builder(it)
+                .or(node)
+                .build()
+        } ?: node
+
+        return conditionsRoot!!
+    }
+
+    /**
+     * TODO
+     * FOR NOW: And the folder with the root.
+     *
+     * Add the folder as another folder to search in. The folder
+     * will be added AND to the root if no 'folder subtree' was found.
+     * Otherwise the folder will be added OR to that tree.
+     */
+    fun addAllowedFolder(folderId: Long) {
+        /*
+         *  TODO find folder sub-tree
+         *          - do and on root of it & rest of search
+         *          - do or between folder nodes
+         */
+        conditionsRoot = and(SearchCondition(MessageSearchField.FOLDER, SearchAttribute.EQUALS, folderId.toString()))
+    }
+
+    /**
+     * TODO make this more advanced!
+     * This is a temporary solution that does NOT WORK for
+     * real searches because of possible extra conditions to a folder requirement.
+     */
+    val folderIds: List<Long>
+        get() = leafSet
+            .mapNotNull { node ->
+                node.condition?.takeIf {
+                    it.field == MessageSearchField.FOLDER && it.attribute == SearchAttribute.EQUALS
+                }?.value?.toLong()
+            }
+
+    /**
+     * Safely gets a folder ID at the specified index, returning null if the index is out of bounds.
+     * This helps prevent IndexOutOfBoundsException when accessing folder IDs.
+     *
+     * @param index The index of the folder ID to get
+     * @return The folder ID at the specified index, or null if the index is out of bounds
+     */
+    fun getFolderIdAtIndexOrNull(index: Int): Long? {
+        return folderIds.getOrNull(index)
+    }
+
+    /**
+     * TODO THIS HAS TO GO!!!!
+     * very dirty fix for remotesearch support atm
+     */
+    val remoteSearchArguments: String?
+        get() = leafSet.firstNotNullOfOrNull { node ->
+            node.condition?.takeIf {
+                it.field == MessageSearchField.SUBJECT || it.field == MessageSearchField.SENDER
+            }?.value
+        }
+
+    /**
+     * Returns all the account uuids that this search will try to match against. Might be an empty array, in which
+     * case all accounts should be included in the search.
+     */
+    override val accountUuids: Set<String>
+        get() = accountUuidSet.toSet()
+
+    /**
+     * Returns whether or not to search all accounts.
+     *
+     * @return `true` if all accounts should be searched.
+     */
+    fun searchAllAccounts(): Boolean = accountUuidSet.isEmpty()
+
+    /**
+     * Get the condition tree.
+     *
+     * @return The root node of the related conditions tree.
+     */
+    override val conditions: SearchConditionTreeNode
+        get() = conditionsRoot ?: SearchConditionTreeNode.Builder(
+            SearchCondition(MessageSearchField.SUBJECT, SearchAttribute.CONTAINS, ""),
+        ).build()
+
+    override fun toString(): String = buildString {
+        append("LocalSearch(")
+        append("id='").append(id).append("', ")
+        append("isManualSearch=").append(isManualSearch).append(", ")
+        append("accountUuidSet=").append(accountUuidSet).append(", ")
+        append("conditionsRoot=").append(conditionsRoot).append(", ")
+        append("leafSet=").append(leafSet)
+        append(")")
+    }
+}
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/SearchAccount.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/SearchAccount.kt
similarity index 89%
rename from feature/search/src/main/java/net/thunderbird/feature/search/SearchAccount.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/SearchAccount.kt
index 5268d4b15ce..f96f79dda2e 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/SearchAccount.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/SearchAccount.kt
@@ -1,8 +1,8 @@
-package net.thunderbird.feature.search
+package net.thunderbird.feature.search.legacy
 
 import net.thunderbird.feature.mail.account.api.BaseAccount
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
 
 /**
  * This class is basically a wrapper around a LocalSearch. It allows to expose it as an account.
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/SearchConditionTreeNode.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/SearchConditionTreeNode.kt
similarity index 94%
rename from feature/search/src/main/java/net/thunderbird/feature/search/SearchConditionTreeNode.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/SearchConditionTreeNode.kt
index 74e5bca292d..29f5f4fb8e3 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/SearchConditionTreeNode.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/SearchConditionTreeNode.kt
@@ -1,10 +1,9 @@
-package net.thunderbird.feature.search
+package net.thunderbird.feature.search.legacy
 
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-import net.thunderbird.feature.search.api.SearchAttribute
-import net.thunderbird.feature.search.api.SearchCondition
-import net.thunderbird.feature.search.api.SearchFieldType
+import kotlinx.serialization.Serializable
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
+import net.thunderbird.feature.search.legacy.api.SearchFieldType
 
 /**
  * Represents a node in a boolean expression tree for evaluating search conditions.
@@ -54,13 +53,15 @@ import net.thunderbird.feature.search.api.SearchFieldType
  * @see SearchCondition
  * @see LocalMessageSearch
  */
-@Parcelize
+@Serializable
 class SearchConditionTreeNode private constructor(
     val operator: Operator,
     val condition: SearchCondition? = null,
     var left: SearchConditionTreeNode? = null,
     var right: SearchConditionTreeNode? = null,
-) : Parcelable {
+) {
+
+    @Serializable
     enum class Operator {
         AND,
         NOT,
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/api/MessageSearchField.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/MessageSearchField.kt
similarity index 94%
rename from feature/search/src/main/java/net/thunderbird/feature/search/api/MessageSearchField.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/MessageSearchField.kt
index 38afc895606..757e158561b 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/api/MessageSearchField.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/MessageSearchField.kt
@@ -1,6 +1,6 @@
-package net.thunderbird.feature.search.api
+package net.thunderbird.feature.search.legacy.api
 
-import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
 
 /**
  * Represents a field that can be searched in messages.
@@ -10,7 +10,7 @@ import kotlinx.parcelize.Parcelize
  * @property fieldType The type of the search field, which determines how it can be queried.
  * @property customQueryTemplate An optional custom query template for fields that require special handling.
  */
-@Parcelize
+@Serializable
 enum class MessageSearchField(
     override val fieldName: String,
     override val fieldType: SearchFieldType,
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/api/MessageSearchSpecification.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/MessageSearchSpecification.kt
similarity index 77%
rename from feature/search/src/main/java/net/thunderbird/feature/search/api/MessageSearchSpecification.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/MessageSearchSpecification.kt
index f434d989a10..08dcf5f03da 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/api/MessageSearchSpecification.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/MessageSearchSpecification.kt
@@ -1,7 +1,6 @@
-package net.thunderbird.feature.search.api
+package net.thunderbird.feature.search.legacy.api
 
-import android.os.Parcelable
-import net.thunderbird.feature.search.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
 
 /**
  * Represents a search specification that defines the accounts and conditions
@@ -11,7 +10,7 @@ import net.thunderbird.feature.search.SearchConditionTreeNode
  * including which accounts to search and the conditions that must be met
  * for messages to be included in the search results.
  */
-interface MessageSearchSpecification : Parcelable {
+interface MessageSearchSpecification {
     /**
      * Get all the uuids of accounts this search acts on.
      * @return Set of uuids.
diff --git a/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/PolymorphicSearchFieldSerializer.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/PolymorphicSearchFieldSerializer.kt
new file mode 100644
index 00000000000..7cd58177092
--- /dev/null
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/PolymorphicSearchFieldSerializer.kt
@@ -0,0 +1,37 @@
+package net.thunderbird.feature.search.legacy.api
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.decodeStructure
+import kotlinx.serialization.encoding.encodeStructure
+import kotlinx.serialization.modules.SerializersModule
+import kotlinx.serialization.modules.polymorphic
+
+val searchFieldSerializerModule = SerializersModule {
+    polymorphic(SearchField::class) {
+        // Register MessageSearchField as a subclass
+        subclass(MessageSearchField::class, PolymorphicSearchFieldSerializer(MessageSearchField.serializer()))
+    }
+}
+
+class PolymorphicSearchFieldSerializer<T : SearchField>(
+    private val searchFieldSerializer: KSerializer<T>,
+) : KSerializer<T> {
+    override val descriptor: SerialDescriptor = buildClassSerialDescriptor(
+        serialName = searchFieldSerializer.descriptor.serialName,
+    ) {
+        element("value", searchFieldSerializer.descriptor)
+    }
+
+    override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): T =
+        decoder.decodeStructure(descriptor) {
+            decodeElementIndex(descriptor)
+            decodeSerializableElement(descriptor, 0, searchFieldSerializer)
+        }
+
+    override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: T) =
+        encoder.encodeStructure(descriptor) {
+            encodeSerializableElement(descriptor, 0, searchFieldSerializer, value)
+        }
+}
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchAttribute.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchAttribute.kt
similarity index 76%
rename from feature/search/src/main/java/net/thunderbird/feature/search/api/SearchAttribute.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchAttribute.kt
index 1ac64ea106b..676ca5b3412 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchAttribute.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchAttribute.kt
@@ -1,8 +1,11 @@
-package net.thunderbird.feature.search.api
+package net.thunderbird.feature.search.legacy.api
+
+import kotlinx.serialization.Serializable
 
 /**
  * Represents the attributes that can be used to match search conditions.
  */
+@Serializable
 enum class SearchAttribute {
     /**
      * The value must be contained within the field.
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchCondition.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchCondition.kt
similarity index 76%
rename from feature/search/src/main/java/net/thunderbird/feature/search/api/SearchCondition.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchCondition.kt
index 8cc713b8703..659a7678e46 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchCondition.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchCondition.kt
@@ -1,7 +1,7 @@
-package net.thunderbird.feature.search.api
+package net.thunderbird.feature.search.legacy.api
 
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.Serializable
 
 /**
  * Represents a search condition describing what to search for in a specific field
@@ -11,9 +11,10 @@ import kotlinx.parcelize.Parcelize
  * @param attribute The attribute to apply to the field (e.g., contains, equals, starts with)
  * @param value The value to match against the field and attribute
  */
-@Parcelize
+@Serializable
 data class SearchCondition(
     @JvmField
+    @Contextual
     val field: SearchField,
 
     @JvmField
@@ -21,4 +22,4 @@ data class SearchCondition(
 
     @JvmField
     val value: String,
-) : Parcelable
+)
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchField.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchField.kt
similarity index 81%
rename from feature/search/src/main/java/net/thunderbird/feature/search/api/SearchField.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchField.kt
index 3fc9e855185..02244e2859f 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchField.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchField.kt
@@ -1,11 +1,9 @@
-package net.thunderbird.feature.search.api
-
-import android.os.Parcelable
+package net.thunderbird.feature.search.legacy.api
 
 /**
  * Represents a field that can be searched.
  */
-interface SearchField : Parcelable {
+interface SearchField {
     /**
      * The name of the field.
      */
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchFieldType.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchFieldType.kt
similarity index 82%
rename from feature/search/src/main/java/net/thunderbird/feature/search/api/SearchFieldType.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchFieldType.kt
index 49ef6d8404c..7250db6cacb 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/api/SearchFieldType.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/api/SearchFieldType.kt
@@ -1,4 +1,6 @@
-package net.thunderbird.feature.search.api
+package net.thunderbird.feature.search.legacy.api
+
+import kotlinx.serialization.Serializable
 
 /**
  * Represents the type of a search field.
@@ -6,6 +8,7 @@ package net.thunderbird.feature.search.api
  * This enum defines the different types of fields that can be used in a search operation.
  * Each type corresponds to a specific kind of data that the field can hold.
  */
+@Serializable
 enum class SearchFieldType {
     /**
      * Represents a field that contains text.
diff --git a/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/serialization/LocalMessageSearchSerializer.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/serialization/LocalMessageSearchSerializer.kt
new file mode 100644
index 00000000000..819a9f7d37d
--- /dev/null
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/serialization/LocalMessageSearchSerializer.kt
@@ -0,0 +1,45 @@
+package net.thunderbird.feature.search.legacy.serialization
+
+import kotlin.text.Charsets
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.api.SearchField
+
+/**
+ * Provides a JSON configuration for serializing and deserializing [LocalMessageSearch] objects.
+ *
+ * It converts [LocalMessageSearch] to and from a ByteArray using the [Json] library.
+ */
+object LocalMessageSearchSerializer {
+    private val json = Json {
+        serializersModule = SerializersModule {
+            // The LocalMessageSearch internally uses a SearchCondition that requires a SearchField which is an
+            // interface. To allow serialization, the SearchCondition needs to mark it's SearchField property as
+            // contextual and we need to provide a serializer for it.
+            contextual(SearchField::class, MessageSearchFieldAsSearchFieldSerializer)
+        }
+    }
+
+    /**
+     * Serializes a [LocalMessageSearch] object to a ByteArray.
+     *
+     * @param search The [LocalMessageSearch] object to serialize.
+     * @return The serialized [LocalMessageSearch] as a ByteArray.
+     */
+    fun serialize(search: LocalMessageSearch): ByteArray {
+        val searchString = json.encodeToString(LocalMessageSearch.serializer(), search)
+        return searchString.toByteArray(Charsets.UTF_8)
+    }
+
+    /**
+     * Deserializes a ByteArray to a [LocalMessageSearch] object.
+     *
+     * @param bytes The ByteArray to deserialize.
+     * @return The deserialized [LocalMessageSearch] object.
+     */
+    fun deserialize(bytes: ByteArray): LocalMessageSearch {
+        val searchString = String(bytes, Charsets.UTF_8)
+        return json.decodeFromString(LocalMessageSearch.serializer(), searchString)
+    }
+}
diff --git a/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/serialization/MessageSearchFieldAsSearchFieldSerializer.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/serialization/MessageSearchFieldAsSearchFieldSerializer.kt
new file mode 100644
index 00000000000..f9375d7a598
--- /dev/null
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/serialization/MessageSearchFieldAsSearchFieldSerializer.kt
@@ -0,0 +1,30 @@
+package net.thunderbird.feature.search.legacy.serialization
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchField
+
+/**
+ * A serializer for [SearchField] that specifically handles [MessageSearchField].
+ * This serializer is used to convert [MessageSearchField] to and from its string representation.
+ */
+object MessageSearchFieldAsSearchFieldSerializer : KSerializer<SearchField> {
+    override val descriptor: SerialDescriptor =
+        PrimitiveSerialDescriptor("SearchField", PrimitiveKind.STRING)
+
+    override fun serialize(encoder: Encoder, value: SearchField) {
+        val enumValue = value as? MessageSearchField
+            ?: throw IllegalArgumentException("Only MessageSearchField is supported")
+        encoder.encodeString(enumValue.name)
+    }
+
+    override fun deserialize(decoder: Decoder): SearchField {
+        val name = decoder.decodeString()
+        return MessageSearchField.valueOf(name)
+    }
+}
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/sql/SqlWhereClause.kt b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/sql/SqlWhereClause.kt
similarity index 94%
rename from feature/search/src/main/java/net/thunderbird/feature/search/sql/SqlWhereClause.kt
rename to feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/sql/SqlWhereClause.kt
index fb42ab06914..109de1ab2ca 100644
--- a/feature/search/src/main/java/net/thunderbird/feature/search/sql/SqlWhereClause.kt
+++ b/feature/search/impl-legacy/src/commonMain/kotlin/net/thunderbird/feature/search/legacy/sql/SqlWhereClause.kt
@@ -1,9 +1,9 @@
-package net.thunderbird.feature.search.sql
+package net.thunderbird.feature.search.legacy.sql
 
-import net.thunderbird.feature.search.SearchConditionTreeNode
-import net.thunderbird.feature.search.api.SearchAttribute
-import net.thunderbird.feature.search.api.SearchCondition
-import net.thunderbird.feature.search.api.SearchFieldType
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
+import net.thunderbird.feature.search.legacy.api.SearchFieldType
 
 /**
  * Builds a SQL query string based on a search condition tree and creates the selection arguments.
diff --git a/feature/search/src/main/java/net/thunderbird/feature/search/LocalMessageSearch.java b/feature/search/src/main/java/net/thunderbird/feature/search/LocalMessageSearch.java
deleted file mode 100644
index af991c78edb..00000000000
--- a/feature/search/src/main/java/net/thunderbird/feature/search/LocalMessageSearch.java
+++ /dev/null
@@ -1,314 +0,0 @@
-package net.thunderbird.feature.search;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import androidx.annotation.NonNull;
-import net.thunderbird.feature.search.api.SearchAttribute;
-import net.thunderbird.feature.search.api.SearchCondition;
-import net.thunderbird.feature.search.api.MessageSearchField;
-import net.thunderbird.feature.search.api.MessageSearchSpecification;
-
-
-/**
- * This class represents a local search.
- *
- * Removing conditions could be done through matching there unique id in the leafset and then
- * removing them from the tree.
- *
- * TODO implement a complete addAllowedFolder method
- * TODO conflicting conditions check on add
- * TODO duplicate condition checking?
- * TODO assign each node a unique id that's used to retrieve it from the leafset and remove.
- *
- */
-
-public class LocalMessageSearch implements MessageSearchSpecification {
-
-    private String id;
-    private boolean mManualSearch = false;
-
-    // since the uuid isn't in the message table it's not in the tree neither
-    private Set<String> mAccountUuids = new HashSet<>();
-    private SearchConditionTreeNode mConditions = null;
-    private Set<SearchConditionTreeNode> mLeafSet = new HashSet<>();
-
-
-    ///////////////////////////////////////////////////////////////
-    // Constructors
-    ///////////////////////////////////////////////////////////////
-    /**
-     * Use this only if the search won't be saved. Saved searches need
-     * a name!
-     */
-    public LocalMessageSearch() {}
-
-    ///////////////////////////////////////////////////////////////
-    // Public manipulation methods
-    ///////////////////////////////////////////////////////////////
-    /**
-     * Set the ID of the search. This is used to identify a unified inbox
-     * search
-     *
-     * @param id ID to set
-     */
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    /**
-     * Add a new account to the search. When no accounts are
-     * added manually we search all accounts on the device.
-     *
-     * @param uuid Uuid of the account to be added.
-     */
-    public void addAccountUuid(String uuid) {
-        mAccountUuids.add(uuid);
-    }
-
-    /**
-     * Adds the provided node as the second argument of an AND
-     * clause to this node.
-     *
-     * @param field Message table field to match against.
-     * @param value Value to look for.
-     * @param attribute Attribute to use when matching.
-     */
-    public void and(MessageSearchField field, String value, SearchAttribute attribute) {
-        and(new SearchCondition(field, attribute, value));
-    }
-
-    /**
-     * Adds the provided condition as the second argument of an AND
-     * clause to this node.
-     *
-     * @param condition Condition to 'AND' with.
-     * @return New top AND node, new root.
-     */
-    public SearchConditionTreeNode and(SearchCondition condition) {
-        SearchConditionTreeNode tmp = new SearchConditionTreeNode.Builder(condition).build();
-        return and(tmp);
-    }
-
-    /**
-     * Adds the provided node as the second argument of an AND
-     * clause to this node.
-     *
-     * @param node Node to 'AND' with.
-     * @return New top AND node, new root.
-     */
-    public SearchConditionTreeNode and(SearchConditionTreeNode node) {
-        mLeafSet.addAll(node.getLeafSet());
-
-        if (mConditions == null) {
-            mConditions = node;
-            return node;
-        }
-
-        mConditions = new SearchConditionTreeNode.Builder(mConditions)
-                .and(node)
-                .build();
-
-        return mConditions;
-    }
-
-    /**
-     * Adds the provided condition as the second argument of an OR
-     * clause to this node.
-     *
-     * @param condition Condition to 'OR' with.
-     * @return New top OR node, new root.
-     */
-    public SearchConditionTreeNode or(SearchCondition condition) {
-        SearchConditionTreeNode tmp = new SearchConditionTreeNode.Builder(condition).build();
-        return or(tmp);
-    }
-
-    /**
-     * Adds the provided node as the second argument of an OR
-     * clause to this node.
-     *
-     * @param node Node to 'OR' with.
-     * @return New top OR node, new root.
-     */
-    public SearchConditionTreeNode or(SearchConditionTreeNode node) {
-        mLeafSet.addAll(node.getLeafSet());
-
-        if (mConditions == null) {
-            mConditions = node;
-            return node;
-        }
-
-        mConditions = new SearchConditionTreeNode.Builder(mConditions)
-                .or(node)
-                .build();
-
-        return mConditions;
-    }
-
-    /**
-     * TODO
-     * FOR NOW: And the folder with the root.
-     *
-     * Add the folder as another folder to search in. The folder
-     * will be added AND to the root if no 'folder subtree' was found.
-     * Otherwise the folder will be added OR to that tree.
-     */
-    public void addAllowedFolder(long folderId) {
-        /*
-         *  TODO find folder sub-tree
-         *          - do and on root of it & rest of search
-         *          - do or between folder nodes
-         */
-        mConditions = and(new SearchCondition(MessageSearchField.FOLDER, SearchAttribute.EQUALS, Long.toString(folderId)));
-    }
-
-    /*
-     * TODO make this more advanced!
-     * This is a temporary solution that does NOT WORK for
-     * real searches because of possible extra conditions to a folder requirement.
-     */
-    public List<Long> getFolderIds() {
-        List<Long> results = new ArrayList<>();
-        for (SearchConditionTreeNode node : mLeafSet) {
-            if (node.getCondition().field == MessageSearchField.FOLDER &&
-                    node.getCondition().attribute == SearchAttribute.EQUALS) {
-                results.add(Long.valueOf(node.getCondition().value));
-            }
-        }
-        return results;
-    }
-
-    /**
-     * Gets the leafset of the related condition tree.
-     *
-     * @return All the leaf conditions as a set.
-     */
-    public Set<SearchConditionTreeNode> getLeafSet() {
-        return mLeafSet;
-    }
-
-    ///////////////////////////////////////////////////////////////
-    // Public accessor methods
-    ///////////////////////////////////////////////////////////////
-    /**
-     * TODO THIS HAS TO GO!!!!
-     * very dirty fix for remotesearch support atm
-     */
-    public String getRemoteSearchArguments() {
-        Set<SearchConditionTreeNode> leafSet = getLeafSet();
-        if (leafSet == null) {
-            return null;
-        }
-
-        for (SearchConditionTreeNode node : leafSet) {
-            if (node.getCondition().field == MessageSearchField.SUBJECT ||
-                    node.getCondition().field == MessageSearchField.SENDER ) {
-                return node.getCondition().value;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Returns the ID of the search
-     *
-     * @return The ID of the search
-     */
-    public String getId() {
-        return (id == null) ? "" : id;
-    }
-
-    public boolean isManualSearch() {
-        return mManualSearch;
-    }
-
-    public void setManualSearch(boolean manualSearch) {
-        mManualSearch = manualSearch;
-    }
-
-    /**
-     * Returns all the account uuids that this search will try to match against. Might be an empty array, in which case
-     * all accounts should be included in the search.
-     */
-    @Override
-    public Set<String> getAccountUuids() {
-        return new HashSet<>(mAccountUuids);
-    }
-
-    /**
-     * Returns whether or not to search all accounts.
-     *
-     * @return {@code true} if all accounts should be searched.
-     */
-    public boolean searchAllAccounts() {
-        return (mAccountUuids.isEmpty());
-    }
-
-    /**
-     * Get the condition tree.
-     *
-     * @return The root node of the related conditions tree.
-     */
-    @Override
-    public SearchConditionTreeNode getConditions() {
-        return mConditions;
-    }
-
-    ///////////////////////////////////////////////////////////////
-    // Parcelable
-    ///////////////////////////////////////////////////////////////
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeString(id);
-        dest.writeByte((byte) (mManualSearch ? 1 : 0));
-        dest.writeStringList(new ArrayList<>(mAccountUuids));
-        dest.writeParcelable(mConditions, flags);
-    }
-
-    public static final Parcelable.Creator<LocalMessageSearch> CREATOR =
-            new Parcelable.Creator<LocalMessageSearch>() {
-
-        @Override
-        public LocalMessageSearch createFromParcel(Parcel in) {
-            return new LocalMessageSearch(in);
-        }
-
-        @Override
-        public LocalMessageSearch[] newArray(int size) {
-            return new LocalMessageSearch[size];
-        }
-    };
-
-    public LocalMessageSearch(Parcel in) {
-        id = in.readString();
-        mManualSearch = (in.readByte() == 1);
-        mAccountUuids.addAll(in.createStringArrayList());
-        mConditions = in.readParcelable(LocalMessageSearch.class.getClassLoader());
-        if (mConditions != null) {
-            mLeafSet = mConditions.getLeafSet();
-        }
-    }
-
-    @NonNull
-    @Override
-    public String toString() {
-        return "LocalSearch(" +
-            "id='" + id + '\'' +
-            ", mManualSearch=" + mManualSearch +
-            ", mAccountUuids=" + mAccountUuids +
-            ", mConditions=" + mConditions +
-            ", mLeafSet=" + mLeafSet +
-            ')';
-    }
-}
diff --git a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListConfig.kt b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListConfig.kt
index 17f6965f917..d7368abb5a9 100644
--- a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListConfig.kt
+++ b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListConfig.kt
@@ -1,7 +1,7 @@
 package net.thunderbird.feature.widget.message.list
 
 import net.thunderbird.core.android.account.SortType
-import net.thunderbird.feature.search.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
 
 internal data class MessageListConfig(
     val search: LocalMessageSearch,
diff --git a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt
index 97b18348952..db4e8f226af 100644
--- a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt
+++ b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt
@@ -9,7 +9,7 @@ import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
-import net.thunderbird.feature.search.sql.SqlWhereClause
+import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
 
 internal class MessageListLoader(
     private val preferences: Preferences,
diff --git a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListWidget.kt b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListWidget.kt
index 5fb80f12111..c98d8ef5d2a 100644
--- a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListWidget.kt
+++ b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListWidget.kt
@@ -20,7 +20,7 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.preference.GeneralSettingsManager
-import net.thunderbird.feature.search.SearchAccount.Companion.createUnifiedInboxAccount
+import net.thunderbird.feature.search.legacy.SearchAccount.Companion.createUnifiedInboxAccount
 import net.thunderbird.feature.widget.message.list.ui.MessageListWidgetContent
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
diff --git a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/BaseMessageListWidgetProvider.kt b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/BaseMessageListWidgetProvider.kt
index fa0aad2b36a..20d0d39420d 100644
--- a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/BaseMessageListWidgetProvider.kt
+++ b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/BaseMessageListWidgetProvider.kt
@@ -11,7 +11,7 @@ import com.fsck.k9.CoreResourceProvider
 import com.fsck.k9.activity.MessageCompose
 import com.fsck.k9.activity.MessageList
 import com.fsck.k9.activity.MessageList.Companion.intentDisplaySearch
-import net.thunderbird.feature.search.SearchAccount.Companion.createUnifiedInboxAccount
+import net.thunderbird.feature.search.legacy.SearchAccount.Companion.createUnifiedInboxAccount
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
 
diff --git a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListConfig.kt b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListConfig.kt
index fd86f3779cc..8864b0bf62d 100644
--- a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListConfig.kt
+++ b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListConfig.kt
@@ -1,7 +1,7 @@
 package app.k9mail.feature.widget.message.list
 
 import net.thunderbird.core.android.account.SortType
-import net.thunderbird.feature.search.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
 
 internal data class MessageListConfig(
     val search: LocalMessageSearch,
diff --git a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt
index 8840828521f..578bed9cd7c 100644
--- a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt
+++ b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt
@@ -9,7 +9,7 @@ import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
-import net.thunderbird.feature.search.sql.SqlWhereClause
+import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
 
 internal class MessageListLoader(
     private val preferences: Preferences,
diff --git a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListRemoteViewFactory.kt b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListRemoteViewFactory.kt
index 73a0a6d8585..3535e89d5d6 100644
--- a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListRemoteViewFactory.kt
+++ b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListRemoteViewFactory.kt
@@ -12,8 +12,8 @@ import com.fsck.k9.CoreResourceProvider
 import com.fsck.k9.activity.MessageList
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.preference.GeneralSettingsManager
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
 
diff --git a/feature/widget/shortcut/src/main/kotlin/app/k9mail/feature/widget/shortcut/LauncherShortcutActivity.kt b/feature/widget/shortcut/src/main/kotlin/app/k9mail/feature/widget/shortcut/LauncherShortcutActivity.kt
index e903df77369..af1728df7cc 100644
--- a/feature/widget/shortcut/src/main/kotlin/app/k9mail/feature/widget/shortcut/LauncherShortcutActivity.kt
+++ b/feature/widget/shortcut/src/main/kotlin/app/k9mail/feature/widget/shortcut/LauncherShortcutActivity.kt
@@ -12,7 +12,7 @@ import androidx.core.graphics.drawable.IconCompat
 import com.fsck.k9.activity.AccountList
 import com.fsck.k9.activity.MessageList
 import net.thunderbird.feature.mail.account.api.BaseAccount
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.SearchAccount
 import app.k9mail.core.ui.legacy.theme2.common.R as CommonR
 
 class LauncherShortcutActivity : AccountList() {
diff --git a/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetConfigurationFragment.kt b/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetConfigurationFragment.kt
index 2b8f29f9ca0..aca06d23b3e 100644
--- a/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetConfigurationFragment.kt
+++ b/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetConfigurationFragment.kt
@@ -16,7 +16,7 @@ import com.fsck.k9.Preferences
 import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
 import com.fsck.k9.ui.choosefolder.ChooseFolderResultContract
 import com.takisoft.preferencex.PreferenceFragmentCompat
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.SearchAccount
 import org.koin.android.ext.android.inject
 
 @Suppress("TooManyFunctions")
diff --git a/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt b/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt
index ba4708d468c..27eab11d220 100644
--- a/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt
+++ b/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt
@@ -11,8 +11,8 @@ import com.fsck.k9.activity.MessageList
 import com.fsck.k9.ui.messagelist.DefaultFolderProvider
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.logging.legacy.Log
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
 
 class UnreadWidgetDataProvider(
     private val context: Context,
diff --git a/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt b/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
index 63ebd5c3c65..f30fcb54447 100644
--- a/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
+++ b/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
@@ -18,7 +18,7 @@ import com.fsck.k9.ui.messagelist.DefaultFolderProvider
 import com.fsck.k9.ui.notification.DeleteConfirmationActivity
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.preference.GeneralSettingsManager
-import net.thunderbird.feature.search.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
 
 /**
  * This class contains methods to create the [PendingIntent]s for the actions of our notifications.
diff --git a/legacy/core/build.gradle.kts b/legacy/core/build.gradle.kts
index 3b16378124b..2417ba6edbc 100644
--- a/legacy/core/build.gradle.kts
+++ b/legacy/core/build.gradle.kts
@@ -18,7 +18,7 @@ dependencies {
     api(projects.feature.mail.folder.api)
     api(projects.feature.account.storage.legacy)
 
-    api(projects.feature.search)
+    api(projects.feature.search.implLegacy)
     api(projects.feature.mail.account.api)
     api(projects.legacy.di)
     api(projects.legacy.mailstore)
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt b/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt
index a98059d88f1..b9cbf320622 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt
@@ -21,9 +21,9 @@ import kotlinx.coroutines.flow.flowOn
 import net.thunderbird.core.android.account.AccountManager
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.logging.legacy.Log
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
-import net.thunderbird.feature.search.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
 
 internal class DefaultMessageCountsProvider(
     private val accountManager: AccountManager,
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
index 7ee8f63db93..3167216e774 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
@@ -85,7 +85,7 @@
 import net.thunderbird.core.android.account.DeletePolicy;
 import net.thunderbird.core.android.account.LegacyAccount;
 import net.thunderbird.core.featureflag.FeatureFlagProvider;
-import net.thunderbird.feature.search.LocalMessageSearch;
+import net.thunderbird.feature.search.legacy.LocalMessageSearch;
 import net.thunderbird.core.logging.Logger;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/NotificationOperations.kt b/legacy/core/src/main/java/com/fsck/k9/controller/NotificationOperations.kt
index 59a5407e31d..c79cf5da2c1 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/NotificationOperations.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/NotificationOperations.kt
@@ -7,7 +7,7 @@ import com.fsck.k9.search.isSingleFolder
 import com.fsck.k9.search.isUnifiedInbox
 import net.thunderbird.core.android.account.AccountManager
 import net.thunderbird.core.android.account.LegacyAccount
-import net.thunderbird.feature.search.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
 
 internal class NotificationOperations(
     private val notificationController: NotificationController,
diff --git a/legacy/core/src/main/java/com/fsck/k9/helper/ParcelableUtil.java b/legacy/core/src/main/java/com/fsck/k9/helper/ParcelableUtil.java
deleted file mode 100644
index 830c9c0c474..00000000000
--- a/legacy/core/src/main/java/com/fsck/k9/helper/ParcelableUtil.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.fsck.k9.helper;
-
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-
-// Source: http://stackoverflow.com/a/18000094
-public class ParcelableUtil {
-    public static byte[] marshall(Parcelable parceable) {
-        Parcel parcel = Parcel.obtain();
-        parceable.writeToParcel(parcel, 0);
-        byte[] bytes = parcel.marshall();
-        parcel.recycle();
-        return bytes;
-    }
-
-    public static <T> T unmarshall(byte[] bytes, Parcelable.Creator<T> creator) {
-        Parcel parcel = unmarshall(bytes);
-        T result = creator.createFromParcel(parcel);
-        parcel.recycle();
-        return result;
-    }
-
-    private static Parcel unmarshall(byte[] bytes) {
-        Parcel parcel = Parcel.obtain();
-        parcel.unmarshall(bytes, 0, bytes.length);
-        parcel.setDataPosition(0);
-        return parcel;
-    }
-}
diff --git a/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java
index da93913cc64..bf3e83c3ca5 100644
--- a/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java
+++ b/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java
@@ -45,12 +45,12 @@
 import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
 import com.fsck.k9.mailstore.LockableDatabase.SchemaDefinition;
 import com.fsck.k9.message.extractors.AttachmentInfoExtractor;
-import net.thunderbird.feature.search.sql.SqlWhereClause;
 import kotlin.time.Clock;
 import net.thunderbird.core.android.account.LegacyAccount;
-import net.thunderbird.feature.search.LocalMessageSearch;
-import net.thunderbird.feature.search.api.SearchAttribute;
-import net.thunderbird.feature.search.api.MessageSearchField;
+import net.thunderbird.feature.search.legacy.LocalMessageSearch;
+import net.thunderbird.feature.search.legacy.api.SearchAttribute;
+import net.thunderbird.feature.search.legacy.api.MessageSearchField;
+import net.thunderbird.feature.search.legacy.sql.SqlWhereClause;
 import org.apache.commons.io.IOUtils;
 import org.apache.james.mime4j.codec.Base64InputStream;
 import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
diff --git a/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt b/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt
index 13d8639f9f9..6f25fc9de9c 100644
--- a/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt
@@ -1,10 +1,10 @@
 package com.fsck.k9.search
 
 import net.thunderbird.core.android.account.LegacyAccount
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
-import net.thunderbird.feature.search.api.SearchCondition
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
 
 /**
  * Modify the supplied [LocalMessageSearch] instance to limit the search to displayable folders.
diff --git a/legacy/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt b/legacy/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt
index d8f2e404b2f..4f2891db1d7 100644
--- a/legacy/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt
@@ -4,8 +4,8 @@ package com.fsck.k9.search
 
 import net.thunderbird.core.android.account.AccountManager
 import net.thunderbird.core.android.account.LegacyAccount
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
 
 val LocalMessageSearch.isUnifiedInbox: Boolean
     get() = id == SearchAccount.UNIFIED_INBOX
diff --git a/legacy/mailstore/build.gradle.kts b/legacy/mailstore/build.gradle.kts
index f20840a2bfa..768228df7ab 100644
--- a/legacy/mailstore/build.gradle.kts
+++ b/legacy/mailstore/build.gradle.kts
@@ -14,7 +14,7 @@ dependencies {
     implementation(projects.core.android.account)
     implementation(projects.feature.mail.account.api)
     implementation(projects.feature.mail.folder.api)
-    implementation(projects.feature.search)
+    implementation(projects.feature.search.implLegacy)
 
     implementation(projects.mail.common)
 }
diff --git a/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/MessageStore.kt b/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/MessageStore.kt
index 6ee1a25ad3a..034a753a216 100644
--- a/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/MessageStore.kt
+++ b/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/MessageStore.kt
@@ -7,7 +7,7 @@ import com.fsck.k9.mail.MessagingException
 import java.util.Date
 import kotlin.jvm.Throws
 import net.thunderbird.feature.mail.folder.api.FolderDetails
-import net.thunderbird.feature.search.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
 
 /**
  * Functions for accessing and modifying locally stored messages.
diff --git a/legacy/message/build.gradle.kts b/legacy/message/build.gradle.kts
index 29ebe3d6706..e389d2df1c5 100644
--- a/legacy/message/build.gradle.kts
+++ b/legacy/message/build.gradle.kts
@@ -8,7 +8,7 @@ android {
 
 dependencies {
     implementation(projects.core.android.account)
-    implementation(projects.feature.search)
+    implementation(projects.feature.search.implLegacy)
 
     implementation(projects.mail.common)
     implementation(projects.backend.api)
diff --git a/legacy/message/src/main/java/app/k9mail/legacy/message/controller/MessageCountsProvider.kt b/legacy/message/src/main/java/app/k9mail/legacy/message/controller/MessageCountsProvider.kt
index 962bf997604..acc89fc85db 100644
--- a/legacy/message/src/main/java/app/k9mail/legacy/message/controller/MessageCountsProvider.kt
+++ b/legacy/message/src/main/java/app/k9mail/legacy/message/controller/MessageCountsProvider.kt
@@ -2,8 +2,8 @@ package app.k9mail.legacy.message.controller
 
 import kotlinx.coroutines.flow.Flow
 import net.thunderbird.core.android.account.LegacyAccount
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
 
 interface MessageCountsProvider {
     fun getMessageCounts(account: LegacyAccount): MessageCounts
diff --git a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt
index a809f46d835..03678fe9869 100644
--- a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt
+++ b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt
@@ -15,7 +15,7 @@ import com.fsck.k9.mailstore.StorageFilesProvider
 import com.fsck.k9.message.extractors.BasicPartInfoExtractor
 import java.util.Date
 import net.thunderbird.feature.mail.folder.api.FolderDetails
-import net.thunderbird.feature.search.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
 
 class K9MessageStore(
     database: LockableDatabase,
diff --git a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt
index 390d0c51d46..76fa90a21ff 100644
--- a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt
+++ b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt
@@ -10,8 +10,8 @@ import com.fsck.k9.mail.FolderType
 import com.fsck.k9.mailstore.FolderNotFoundException
 import com.fsck.k9.mailstore.LockableDatabase
 import com.fsck.k9.mailstore.toFolderType
-import net.thunderbird.feature.search.SearchConditionTreeNode
-import net.thunderbird.feature.search.sql.SqlWhereClause
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
 
 internal class RetrieveFolderOperations(private val lockableDatabase: LockableDatabase) {
     fun <T> getFolder(folderId: Long, mapper: FolderMapper<T>): T? {
diff --git a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt
index 78f02ca83b9..ac9560bcda5 100644
--- a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt
+++ b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt
@@ -7,7 +7,7 @@ import app.k9mail.legacy.message.extractors.PreviewResult
 import com.fsck.k9.mail.Address
 import com.fsck.k9.mailstore.DatabasePreviewType
 import com.fsck.k9.mailstore.LockableDatabase
-import net.thunderbird.feature.search.sql.SqlWhereClause
+import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
 
 internal class RetrieveMessageListOperations(private val lockableDatabase: LockableDatabase) {
 
diff --git a/legacy/ui/legacy/build.gradle.kts b/legacy/ui/legacy/build.gradle.kts
index 428d401e2d3..3caa22f3ebb 100644
--- a/legacy/ui/legacy/build.gradle.kts
+++ b/legacy/ui/legacy/build.gradle.kts
@@ -26,6 +26,7 @@ dependencies {
     // TODO: Remove AccountOauth dependency
     implementation(projects.feature.account.oauth)
     implementation(projects.feature.funding.api)
+    implementation(projects.feature.search.implLegacy)
     implementation(projects.feature.settings.import)
     implementation(projects.feature.telemetry.api)
     implementation(projects.feature.mail.message.list)
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.kt
index b838c5e0fc1..2d34f8ef014 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.kt
@@ -19,7 +19,7 @@ import kotlinx.coroutines.withContext
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.preference.GeneralSettingsManager
 import net.thunderbird.feature.mail.account.api.BaseAccount
-import net.thunderbird.feature.search.SearchAccount.Companion.createUnifiedInboxAccount
+import net.thunderbird.feature.search.legacy.SearchAccount
 import org.koin.android.ext.android.inject
 
 /**
@@ -77,7 +77,7 @@ abstract class AccountList : K9ListActivity(), OnItemClickListener {
         val accounts: MutableList<BaseAccount> = ArrayList()
 
         if (generalSettingsManager.getConfig().display.isShowUnifiedInbox) {
-            val unifiedInboxAccount: BaseAccount = createUnifiedInboxAccount(
+            val unifiedInboxAccount: BaseAccount = SearchAccount.createUnifiedInboxAccount(
                 unifiedInboxTitle = coreResourceProvider.searchUnifiedInboxTitle(),
                 unifiedInboxDetail = coreResourceProvider.searchUnifiedInboxDetail(),
             )
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java
index 625865fa5db..c67be12533f 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java
@@ -120,7 +120,7 @@
 import net.thunderbird.core.android.contact.ContactIntentHelper;
 import net.thunderbird.core.preference.GeneralSettingsManager;
 import net.thunderbird.core.ui.theme.manager.ThemeManager;
-import net.thunderbird.feature.search.LocalMessageSearch;
+import net.thunderbird.feature.search.legacy.LocalMessageSearch;
 import org.openintents.openpgp.OpenPgpApiManager;
 import org.openintents.openpgp.util.OpenPgpIntentStarter;
 import net.thunderbird.core.logging.legacy.Log;
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
index 08849bffdcc..b85bfcf788a 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
@@ -43,7 +43,6 @@ import com.fsck.k9.Preferences
 import com.fsck.k9.account.BackgroundAccountRemover
 import com.fsck.k9.activity.compose.MessageActions
 import com.fsck.k9.controller.MessagingController
-import com.fsck.k9.helper.ParcelableUtil
 import com.fsck.k9.search.isUnifiedInbox
 import com.fsck.k9.ui.BuildConfig
 import com.fsck.k9.ui.R
@@ -71,12 +70,12 @@ import net.thunderbird.feature.navigation.drawer.api.NavigationDrawer
 import net.thunderbird.feature.navigation.drawer.dropdown.DropDownDrawer
 import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayAccount
 import net.thunderbird.feature.navigation.drawer.siderail.SideRailDrawer
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.MessageSearchSpecification
-import net.thunderbird.feature.search.api.SearchAttribute
-import net.thunderbird.feature.search.api.SearchCondition
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
+import net.thunderbird.feature.search.legacy.serialization.LocalMessageSearchSerializer
 import org.koin.android.ext.android.inject
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
@@ -526,7 +525,9 @@ open class MessageList :
 
             if (messageReference != null) {
                 val search = if (intent.hasByteArrayExtra(EXTRA_SEARCH)) {
-                    ParcelableUtil.unmarshall(intent.getByteArrayExtra(EXTRA_SEARCH), LocalMessageSearch.CREATOR)
+                    intent.getByteArrayExtra(EXTRA_SEARCH)?.let {
+                        LocalMessageSearchSerializer.deserialize(it)
+                    } ?: messageReference.toLocalSearch()
                 } else {
                     messageReference.toLocalSearch()
                 }
@@ -539,13 +540,20 @@ open class MessageList :
             }
         } else if (intent.hasByteArrayExtra(EXTRA_SEARCH)) {
             // regular LocalSearch object was passed
-            val search = ParcelableUtil.unmarshall(intent.getByteArrayExtra(EXTRA_SEARCH), LocalMessageSearch.CREATOR)
+            val search = intent.getByteArrayExtra(EXTRA_SEARCH)?.let {
+                LocalMessageSearchSerializer.deserialize(it)
+            }
             val noThreading = intent.getBooleanExtra(EXTRA_NO_THREADING, false)
             val account = intent.getStringExtra(EXTRA_ACCOUNT)?.let { accountUuid ->
                 accountManager.getAccount(accountUuid)
             }
 
-            return LaunchData(search = search, account = account, noThreading = noThreading)
+            return if (search == null) {
+                Log.e("No search data found in intent extras.")
+                LaunchData(createDefaultLocalSearch())
+            } else {
+                LaunchData(search = search, account = account, noThreading = noThreading)
+            }
         }
 
         // Default action
@@ -1227,7 +1235,7 @@ open class MessageList :
         showMessageViewPlaceHolder()
 
         val tmpSearch = LocalMessageSearch().apply {
-            setId(search?.id)
+            id = search?.id ?: "ShowThread-${account.uuid}-$threadRootId"
             addAccountUuid(account.uuid)
             and(MessageSearchField.THREAD_ID, threadRootId.toString(), SearchAttribute.EQUALS)
         }
@@ -1479,10 +1487,12 @@ open class MessageList :
 
         search?.let { search ->
             when {
-                singleFolderMode -> drawer.selectFolder(
-                    accountUuid = search.accountUuids.elementAt(0),
-                    folderId = search.folderIds[0],
-                )
+                singleFolderMode -> search.getFolderIdAtIndexOrNull(0)?.let { folderId ->
+                    drawer.selectFolder(
+                        accountUuid = search.accountUuids.elementAt(0),
+                        folderId = folderId,
+                    )
+                }
 
                 // Don't select any item in the drawer because the Unified Inbox is displayed,
                 // but not listed in the drawer
@@ -1547,7 +1557,7 @@ open class MessageList :
         @JvmOverloads
         fun actionDisplaySearch(
             context: Context,
-            search: MessageSearchSpecification?,
+            search: LocalMessageSearch?,
             noThreading: Boolean,
             newTask: Boolean,
             clearTop: Boolean = true,
@@ -1558,13 +1568,15 @@ open class MessageList :
         @JvmStatic
         fun intentDisplaySearch(
             context: Context?,
-            search: MessageSearchSpecification?,
+            search: LocalMessageSearch?,
             noThreading: Boolean,
             newTask: Boolean,
             clearTop: Boolean,
         ): Intent {
             return Intent(context, MessageList::class.java).apply {
-                putExtra(EXTRA_SEARCH, ParcelableUtil.marshall(search))
+                if (search != null) {
+                    putExtra(EXTRA_SEARCH, LocalMessageSearchSerializer.serialize(search))
+                }
                 putExtra(EXTRA_NO_THREADING, noThreading)
 
                 addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
@@ -1573,7 +1585,6 @@ open class MessageList :
                 if (newTask) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
             }
         }
-
         fun createUnifiedInboxIntent(
             context: Context,
             account: LegacyAccount,
@@ -1585,7 +1596,7 @@ open class MessageList :
                 ).relatedSearch
 
                 putExtra(EXTRA_ACCOUNT, account.uuid)
-                putExtra(EXTRA_SEARCH, ParcelableUtil.marshall(search))
+                putExtra(EXTRA_SEARCH, LocalMessageSearchSerializer.serialize(search))
                 putExtra(EXTRA_NO_THREADING, false)
 
                 addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
@@ -1650,7 +1661,7 @@ open class MessageList :
                         unifiedInboxTitle = coreResourceProvider.searchUnifiedInboxTitle(),
                         unifiedInboxDetail = coreResourceProvider.searchUnifiedInboxDetail(),
                     ).relatedSearch
-                    putExtra(EXTRA_SEARCH, ParcelableUtil.marshall(search))
+                    putExtra(EXTRA_SEARCH, LocalMessageSearchSerializer.serialize(search))
                 }
 
                 putExtra(EXTRA_MESSAGE_VIEW_ONLY, messageViewOnly)
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt
index 8d3b78a0d01..667c14c4a65 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt
@@ -2,7 +2,7 @@ package com.fsck.k9.ui.messagelist
 
 import app.k9mail.legacy.message.controller.MessageReference
 import net.thunderbird.core.android.account.SortType
-import net.thunderbird.feature.search.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
 
 data class MessageListConfig(
     val search: LocalMessageSearch,
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
index 44142c85710..8ee9aa5c924 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
@@ -15,7 +15,6 @@ import android.widget.Toast
 import androidx.activity.result.ActivityResultLauncher
 import androidx.annotation.StringRes
 import androidx.appcompat.view.ActionMode
-import androidx.core.os.BundleCompat
 import androidx.core.os.bundleOf
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
@@ -84,8 +83,9 @@ import net.thunderbird.core.preference.GeneralSettingsManager
 import net.thunderbird.feature.account.storage.legacy.mapper.DefaultLegacyAccountWrapperDataMapper
 import net.thunderbird.feature.mail.message.list.domain.DomainContract
 import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
+import net.thunderbird.feature.search.legacy.serialization.LocalMessageSearchSerializer
 import org.koin.android.ext.android.inject
 import org.koin.androidx.viewmodel.ext.android.viewModel
 import org.koin.core.parameter.parametersOf
@@ -277,7 +277,10 @@ class MessageListFragment :
         val arguments = requireArguments()
         showingThreadedList = arguments.getBoolean(ARG_THREADED_LIST, false)
         isThreadDisplay = arguments.getBoolean(ARG_IS_THREAD_DISPLAY, false)
-        localSearch = BundleCompat.getParcelable(arguments, ARG_SEARCH, LocalMessageSearch::class.java)!!
+
+        localSearch = arguments.getByteArray(ARG_SEARCH)?.let {
+            LocalMessageSearchSerializer.deserialize(it)
+        }!!
 
         allAccounts = localSearch.searchAllAccounts()
         val searchAccounts = localSearch.getAccounts(accountManager).also(::updateAccountList)
@@ -2259,9 +2262,11 @@ class MessageListFragment :
             isThreadDisplay: Boolean,
             threadedList: Boolean,
         ): MessageListFragment {
+            val searchBytes = LocalMessageSearchSerializer.serialize(search)
+
             return MessageListFragment().apply {
                 arguments = bundleOf(
-                    ARG_SEARCH to search,
+                    ARG_SEARCH to searchBytes,
                     ARG_IS_THREAD_DISPLAY to isThreadDisplay,
                     ARG_THREADED_LIST to threadedList,
                 )
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt
index fa997c31508..58b90b66f86 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt
@@ -10,9 +10,9 @@ import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.sql.SqlWhereClause
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
 
 class MessageListLoader(
     private val preferences: Preferences,
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 370de5b6cdf..bdeaca63044 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -129,7 +129,7 @@ include(
 )
 
 include(
-    ":feature:search",
+    ":feature:search:impl-legacy",
 )
 
 include(


Test patch:
diff --git a/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/FakeMessageCountsProvider.kt b/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/FakeMessageCountsProvider.kt
index 2148b31c11f..6e62a2ab1e0 100644
--- a/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/FakeMessageCountsProvider.kt
+++ b/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/FakeMessageCountsProvider.kt
@@ -5,8 +5,8 @@ import app.k9mail.legacy.message.controller.MessageCountsProvider
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flowOf
 import net.thunderbird.core.android.account.LegacyAccount
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
 
 internal class FakeMessageCountsProvider(
     private val messageCounts: MessageCounts,
diff --git a/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepositoryTest.kt b/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepositoryTest.kt
index ae9befce4da..a045870d3d1 100644
--- a/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepositoryTest.kt
+++ b/feature/navigation/drawer/dropdown/src/test/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/data/UnifiedFolderRepositoryTest.kt
@@ -8,8 +8,8 @@ import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
 import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
 import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
 
 internal class UnifiedFolderRepositoryTest {
 
diff --git a/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/FakeMessageCountsProvider.kt b/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/FakeMessageCountsProvider.kt
index f519c92f2eb..6db5170aac1 100644
--- a/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/FakeMessageCountsProvider.kt
+++ b/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/FakeMessageCountsProvider.kt
@@ -5,8 +5,8 @@ import app.k9mail.legacy.message.controller.MessageCountsProvider
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flowOf
 import net.thunderbird.core.android.account.LegacyAccount
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
 
 internal class FakeMessageCountsProvider(
     private val messageCounts: MessageCounts,
diff --git a/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepositoryTest.kt b/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepositoryTest.kt
index 313c78f090e..97640dbbc62 100644
--- a/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepositoryTest.kt
+++ b/feature/navigation/drawer/siderail/src/test/kotlin/net/thunderbird/feature/navigation/drawer/siderail/data/UnifiedFolderRepositoryTest.kt
@@ -8,8 +8,8 @@ import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
 import net.thunderbird.feature.navigation.drawer.siderail.domain.entity.DisplayUnifiedFolder
 import net.thunderbird.feature.navigation.drawer.siderail.domain.entity.DisplayUnifiedFolderType
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
 
 internal class UnifiedFolderRepositoryTest {
 
diff --git a/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/LocalMessageSearchTest.kt b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/LocalMessageSearchTest.kt
new file mode 100644
index 00000000000..9d3fa94507a
--- /dev/null
+++ b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/LocalMessageSearchTest.kt
@@ -0,0 +1,216 @@
+package net.thunderbird.feature.search.legacy
+
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.containsExactly
+import assertk.assertions.hasSize
+import assertk.assertions.isEmpty
+import assertk.assertions.isEqualTo
+import assertk.assertions.isNotNull
+import assertk.assertions.isTrue
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
+import org.junit.Test
+
+class LocalMessageSearchTest {
+
+    @Test
+    fun `should create an empty search`() {
+        // Arrange & Act
+        val testSubject = LocalMessageSearch()
+
+        // Assert
+        assertThat(testSubject.id).isEqualTo("")
+        assertThat(testSubject.isManualSearch).isEqualTo(false)
+        assertThat(testSubject.accountUuids).isEmpty()
+        assertThat(testSubject.leafSet).isEmpty()
+        assertThat(testSubject.conditions).isNotNull()
+    }
+
+    @Test
+    fun `should add account uuid`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val uuid = "test-uuid"
+
+        // Act
+        testSubject.addAccountUuid(uuid)
+
+        // Assert
+        assertThat(testSubject.accountUuids.toList()).containsExactly(uuid)
+    }
+
+    @Test
+    fun `should add condition with AND operator`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val condition = SearchCondition(MessageSearchField.SUBJECT, SearchAttribute.CONTAINS, "test")
+
+        // Act
+        val result = testSubject.and(condition)
+
+        // Assert
+        assertThat(result).isNotNull()
+        assertThat(result.operator).isEqualTo(SearchConditionTreeNode.Operator.CONDITION)
+        assertThat(result.condition).isEqualTo(condition)
+        assertThat(testSubject.leafSet).hasSize(1)
+        assertThat(testSubject.conditions).isEqualTo(result)
+    }
+
+    @Test
+    fun `should add condition with OR operator`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val condition = SearchCondition(MessageSearchField.SUBJECT, SearchAttribute.CONTAINS, "test")
+
+        // Act
+        val result = testSubject.or(condition)
+
+        // Assert
+        assertThat(result).isNotNull()
+        assertThat(result.operator).isEqualTo(SearchConditionTreeNode.Operator.CONDITION)
+        assertThat(result.condition).isEqualTo(condition)
+        assertThat(testSubject.leafSet).hasSize(1)
+        assertThat(testSubject.conditions).isEqualTo(result)
+    }
+
+    @Test
+    fun `should add multiple conditions with AND operator`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val condition1 = SearchCondition(MessageSearchField.SUBJECT, SearchAttribute.CONTAINS, "test1")
+        val condition2 = SearchCondition(MessageSearchField.SENDER, SearchAttribute.CONTAINS, "test2")
+
+        // Act
+        testSubject.and(condition1)
+        val result = testSubject.and(condition2)
+
+        // Assert
+        assertThat(result).isNotNull()
+        assertThat(result.operator).isEqualTo(SearchConditionTreeNode.Operator.AND)
+        assertThat(testSubject.leafSet).hasSize(2)
+
+        // Verify both conditions are in the leaf set
+        val conditions = testSubject.leafSet.mapNotNull { it.condition }
+        assertThat(conditions).contains(condition1)
+        assertThat(conditions).contains(condition2)
+    }
+
+    @Test
+    fun `should add multiple conditions with OR operator`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val condition1 = SearchCondition(MessageSearchField.SUBJECT, SearchAttribute.CONTAINS, "test1")
+        val condition2 = SearchCondition(MessageSearchField.SENDER, SearchAttribute.CONTAINS, "test2")
+
+        // Act
+        testSubject.or(condition1)
+        val result = testSubject.or(condition2)
+
+        // Assert
+        assertThat(result).isNotNull()
+        assertThat(result.operator).isEqualTo(SearchConditionTreeNode.Operator.OR)
+        assertThat(testSubject.leafSet).hasSize(2)
+
+        // Verify both conditions are in the leaf set
+        val conditions = testSubject.leafSet.mapNotNull { it.condition }
+        assertThat(conditions).contains(condition1)
+        assertThat(conditions).contains(condition2)
+    }
+
+    @Test
+    fun `should add allowed folder`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val folderId = 123L
+
+        // Act
+        testSubject.addAllowedFolder(folderId)
+
+        // Assert
+        assertThat(testSubject.folderIds.toList()).containsExactly(folderId)
+    }
+
+    @Test
+    fun `should return empty list when no folder conditions exist`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+
+        // Act
+        val result = testSubject.folderIds
+
+        // Assert
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun `should return remote search arguments when subject condition exists`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val searchValue = "test query"
+        testSubject.and(MessageSearchField.SUBJECT, searchValue, SearchAttribute.CONTAINS)
+
+        // Act
+        val result = testSubject.remoteSearchArguments
+
+        // Assert
+        assertThat(result).isEqualTo(searchValue)
+    }
+
+    @Test
+    fun `should return remote search arguments when sender condition exists`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        val searchValue = "test@example.com"
+        testSubject.and(MessageSearchField.SENDER, searchValue, SearchAttribute.CONTAINS)
+
+        // Act
+        val result = testSubject.remoteSearchArguments
+
+        // Assert
+        assertThat(result).isEqualTo(searchValue)
+    }
+
+    @Test
+    fun `should return null for remote search arguments when no relevant conditions exist`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+        testSubject.and(MessageSearchField.FLAGGED, "1", SearchAttribute.EQUALS)
+
+        // Act
+        val result = testSubject.remoteSearchArguments
+
+        // Assert
+        assertThat(result).isEqualTo(null)
+    }
+
+    @Test
+    fun `should return true for searchAllAccounts when no accounts are added`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+
+        // Act
+        val result = testSubject.searchAllAccounts()
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun `should return default condition when conditions is null`() {
+        // Arrange
+        val testSubject = LocalMessageSearch()
+
+        // Act
+        val result = testSubject.conditions
+
+        // Assert
+        assertThat(result).isNotNull()
+        assertThat(result.operator).isEqualTo(SearchConditionTreeNode.Operator.CONDITION)
+        assertThat(result.condition).isNotNull()
+        assertThat(result.condition?.field).isEqualTo(MessageSearchField.SUBJECT)
+        assertThat(result.condition?.attribute).isEqualTo(SearchAttribute.CONTAINS)
+        assertThat(result.condition?.value).isEqualTo("")
+    }
+}
diff --git a/feature/search/src/test/java/net/thunderbird/feature/search/SearchConditionTreeNodeTest.kt b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/SearchConditionTreeNodeTest.kt
similarity index 95%
rename from feature/search/src/test/java/net/thunderbird/feature/search/SearchConditionTreeNodeTest.kt
rename to feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/SearchConditionTreeNodeTest.kt
index 3eab01bfad0..038f95c1979 100644
--- a/feature/search/src/test/java/net/thunderbird/feature/search/SearchConditionTreeNodeTest.kt
+++ b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/SearchConditionTreeNodeTest.kt
@@ -1,4 +1,4 @@
-package net.thunderbird.feature.search
+package net.thunderbird.feature.search.legacy
 
 import assertk.assertFailure
 import assertk.assertThat
@@ -6,17 +6,15 @@ import assertk.assertions.contains
 import assertk.assertions.isEqualTo
 import assertk.assertions.isInstanceOf
 import assertk.assertions.isNotNull
-import kotlinx.parcelize.Parcelize
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
-import net.thunderbird.feature.search.api.SearchCondition
-import net.thunderbird.feature.search.api.SearchField
-import net.thunderbird.feature.search.api.SearchFieldType
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
+import net.thunderbird.feature.search.legacy.api.SearchField
+import net.thunderbird.feature.search.legacy.api.SearchFieldType
 import org.junit.Test
 
 class SearchConditionTreeNodeTest {
 
-    @Parcelize
     data class TestSearchField(
         override val fieldName: String,
         override val fieldType: SearchFieldType,
diff --git a/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/serialization/LocalMessageSearchSerializerTest.kt b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/serialization/LocalMessageSearchSerializerTest.kt
new file mode 100644
index 00000000000..799f8f08b86
--- /dev/null
+++ b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/serialization/LocalMessageSearchSerializerTest.kt
@@ -0,0 +1,143 @@
+package net.thunderbird.feature.search.legacy.serialization
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isNotNull
+import kotlin.text.Charsets
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import org.junit.Test
+
+class LocalMessageSearchSerializerTest {
+
+    @Test
+    fun `should serialize empty search`() {
+        // Arrange
+        val search = LocalMessageSearch()
+
+        // Act
+        val result = LocalMessageSearchSerializer.serialize(search)
+
+        // Assert
+        assertThat(result).isNotNull()
+    }
+
+    @Test
+    fun `should deserialize empty search`() {
+        // Arrange
+        val search = LocalMessageSearch()
+        val bytes = LocalMessageSearchSerializer.serialize(search)
+
+        // Act
+        val result = LocalMessageSearchSerializer.deserialize(bytes)
+
+        // Assert
+        assertThat(result).isNotNull()
+        assertThat(result.id).isEqualTo("")
+        assertThat(result.isManualSearch).isEqualTo(false)
+    }
+
+    @Test
+    fun `should round-trip serialize and deserialize empty search`() {
+        // Arrange
+        val search = LocalMessageSearch()
+
+        // Act
+        val bytes = LocalMessageSearchSerializer.serialize(search)
+        val result = LocalMessageSearchSerializer.deserialize(bytes)
+
+        // Assert
+        assertThat(result.id).isEqualTo(search.id)
+        assertThat(result.isManualSearch).isEqualTo(search.isManualSearch)
+        assertThat(result.accountUuids).isEqualTo(search.accountUuids)
+    }
+
+    @Test
+    fun `should round-trip serialize and deserialize search with account uuid`() {
+        // Arrange
+        val search = LocalMessageSearch()
+        search.addAccountUuid("test-account-uuid")
+
+        // Act
+        val bytes = LocalMessageSearchSerializer.serialize(search)
+        val result = LocalMessageSearchSerializer.deserialize(bytes)
+
+        // Assert
+        assertThat(result.accountUuids).isEqualTo(search.accountUuids)
+    }
+
+    @Test
+    fun `should round-trip serialize and deserialize search with condition`() {
+        // Arrange
+        val search = LocalMessageSearch()
+        search.and(MessageSearchField.SUBJECT, "test subject", SearchAttribute.CONTAINS)
+
+        // Act
+        val bytes = LocalMessageSearchSerializer.serialize(search)
+        val result = LocalMessageSearchSerializer.deserialize(bytes)
+
+        // Assert
+        val originalCondition = search.conditions.condition
+        val resultCondition = result.conditions.condition
+
+        assertThat(resultCondition).isNotNull()
+        assertThat(originalCondition).isNotNull()
+        assertThat(resultCondition!!.field).isEqualTo(originalCondition!!.field)
+        assertThat(resultCondition.attribute).isEqualTo(originalCondition.attribute)
+        assertThat(resultCondition.value).isEqualTo(originalCondition.value)
+    }
+
+    @Test
+    fun `should round-trip serialize and deserialize search with multiple conditions`() {
+        // Arrange
+        val search = LocalMessageSearch()
+        search.and(MessageSearchField.SUBJECT, "test subject", SearchAttribute.CONTAINS)
+        search.and(MessageSearchField.SENDER, "test sender", SearchAttribute.CONTAINS)
+
+        // Act
+        val bytes = LocalMessageSearchSerializer.serialize(search)
+        val result = LocalMessageSearchSerializer.deserialize(bytes)
+
+        // Assert
+        // Since the conditions are in a tree structure, we'll just verify the leaf set size
+        assertThat(result.leafSet.size).isEqualTo(search.leafSet.size)
+    }
+
+    @Test
+    fun `should handle special characters correctly`() {
+        // Arrange
+        val search = LocalMessageSearch()
+        val specialChars = "Special characters: äöüß@€$%&*()[]{}|<>?/\\=+"
+        search.and(MessageSearchField.SUBJECT, specialChars, SearchAttribute.CONTAINS)
+
+        // Act
+        val bytes = LocalMessageSearchSerializer.serialize(search)
+        val result = LocalMessageSearchSerializer.deserialize(bytes)
+
+        // Assert
+        val originalCondition = search.conditions.condition
+        val resultCondition = result.conditions.condition
+
+        assertThat(resultCondition).isNotNull()
+        assertThat(originalCondition).isNotNull()
+        assertThat(resultCondition!!.value).isEqualTo(originalCondition!!.value)
+        assertThat(resultCondition.value).isEqualTo(specialChars)
+    }
+
+    @Test
+    fun `should use UTF-8 encoding for serialization`() {
+        // Arrange
+        val search = LocalMessageSearch()
+        val utf8String = "UTF-8 characters: 你好, こんにちは, 안녕하세요"
+        search.and(MessageSearchField.SUBJECT, utf8String, SearchAttribute.CONTAINS)
+
+        // Act
+        val bytes = LocalMessageSearchSerializer.serialize(search)
+
+        // Assert
+        // Convert bytes back to string using UTF-8 and verify it contains the original string
+        val jsonString = String(bytes, Charsets.UTF_8)
+        assertThat(jsonString.contains(utf8String)).isEqualTo(true)
+    }
+}
diff --git a/feature/search/src/test/java/net/thunderbird/feature/search/sql/SqlWhereClauseTest.kt b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/sql/SqlWhereClauseTest.kt
similarity index 96%
rename from feature/search/src/test/java/net/thunderbird/feature/search/sql/SqlWhereClauseTest.kt
rename to feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/sql/SqlWhereClauseTest.kt
index 471b65eb97c..45bcff82ff7 100644
--- a/feature/search/src/test/java/net/thunderbird/feature/search/sql/SqlWhereClauseTest.kt
+++ b/feature/search/impl-legacy/src/commonTest/java/net/thunderbird/feature/search/legacy/sql/SqlWhereClauseTest.kt
@@ -1,22 +1,20 @@
-package net.thunderbird.feature.search.sql
+package net.thunderbird.feature.search.legacy.sql
 
 import assertk.assertFailure
 import assertk.assertThat
 import assertk.assertions.hasSize
 import assertk.assertions.isEqualTo
 import assertk.assertions.isInstanceOf
-import kotlinx.parcelize.Parcelize
-import net.thunderbird.feature.search.SearchConditionTreeNode
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
-import net.thunderbird.feature.search.api.SearchCondition
-import net.thunderbird.feature.search.api.SearchField
-import net.thunderbird.feature.search.api.SearchFieldType
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.api.SearchCondition
+import net.thunderbird.feature.search.legacy.api.SearchField
+import net.thunderbird.feature.search.legacy.api.SearchFieldType
 import org.junit.Test
 
 class SqlWhereClauseTest {
 
-    @Parcelize
     data class TestSearchField(
         override val fieldName: String,
         override val fieldType: SearchFieldType,
diff --git a/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt b/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt
index 72fdbaa0a39..22142d1bba8 100644
--- a/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt
+++ b/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt
@@ -15,8 +15,8 @@ import kotlinx.coroutines.flow.Flow
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.feature.mail.folder.api.Folder
 import net.thunderbird.feature.mail.folder.api.FolderType
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchAccount
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchAccount
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt b/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt
index 64361074ea4..709a99d6695 100644
--- a/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt
@@ -13,8 +13,8 @@ import kotlinx.coroutines.test.runTest
 import net.thunderbird.account.fake.FakeAccountData.ACCOUNT_ID_RAW
 import net.thunderbird.core.android.account.AccountManager
 import net.thunderbird.core.android.account.LegacyAccount
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.SearchConditionTreeNode
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
 import org.junit.Test
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
diff --git a/legacy/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt b/legacy/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt
index b3d05f245d9..e89c5865d01 100644
--- a/legacy/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt
+++ b/legacy/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt
@@ -16,9 +16,9 @@ import com.fsck.k9.mailstore.toDatabaseFolderType
 import com.fsck.k9.storage.RobolectricTest
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.logging.testing.TestLogger
-import net.thunderbird.feature.search.LocalMessageSearch
-import net.thunderbird.feature.search.api.MessageSearchField
-import net.thunderbird.feature.search.api.SearchAttribute
+import net.thunderbird.feature.search.legacy.LocalMessageSearch
+import net.thunderbird.feature.search.legacy.api.MessageSearchField
+import net.thunderbird.feature.search.legacy.api.SearchAttribute
 import org.junit.Before
 import org.junit.Test