Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,20 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
android {
namespace = "app.k9mail.legacy.mailstore"
}
dependencies {
implementation(projects.legacy.di)
implementation(projects.legacy.message)
implementation(projects.core.common)
implementation(projects.core.android.account)
implementation(projects.feature.mail.account.api)
implementation(projects.feature.mail.folder.api)
implementation(projects.feature.search.implLegacy)
implementation(projects.mail.common)
}

View file

@ -0,0 +1,10 @@
package app.k9mail.legacy.mailstore
import com.fsck.k9.mail.FolderType
data class CreateFolderInfo(
val serverId: String,
val name: String,
val type: FolderType,
val settings: FolderSettings,
)

View file

@ -0,0 +1,28 @@
package app.k9mail.legacy.mailstore
import com.fsck.k9.mail.FolderType
fun interface FolderMapper<T> {
fun map(folder: FolderDetailsAccessor): T
}
interface FolderDetailsAccessor {
val id: Long
val serverId: String?
val name: String
val type: FolderType
val isLocalOnly: Boolean
val isInTopGroup: Boolean
val isIntegrate: Boolean
val isSyncEnabled: Boolean
val isVisible: Boolean
val isNotificationsEnabled: Boolean
val isPushEnabled: Boolean
val visibleLimit: Int
val moreMessages: MoreMessages
val lastChecked: Long?
val unreadMessageCount: Int
val starredMessageCount: Int
fun serverIdOrThrow(): String
}

View file

@ -0,0 +1,212 @@
package app.k9mail.legacy.mailstore
import app.k9mail.legacy.mailstore.FolderTypeMapper.folderTypeOf
import app.k9mail.legacy.mailstore.RemoteFolderTypeMapper.toFolderType
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.common.exception.MessagingException
import net.thunderbird.feature.mail.folder.api.Folder
import net.thunderbird.feature.mail.folder.api.FolderDetails
import net.thunderbird.feature.mail.folder.api.FolderType
import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
import net.thunderbird.feature.mail.folder.api.RemoteFolder
@Suppress("TooManyFunctions")
class FolderRepository(
private val messageStoreManager: MessageStoreManager,
private val outboxFolderManager: OutboxFolderManager,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
suspend fun getFolder(account: LegacyAccount, folderId: Long): Folder? {
val messageStore = messageStoreManager.getMessageStore(account)
val outboxFolderId = outboxFolderManager.getOutboxFolderId(account.id)
return messageStore.getFolder(folderId) { folder ->
Folder(
id = folder.id,
name = folder.name,
type = folder.getFolderType(account, outboxFolderId),
isLocalOnly = folder.isLocalOnly,
)
}
}
suspend fun getFolderDetails(account: LegacyAccount, folderId: Long): FolderDetails? {
val messageStore = messageStoreManager.getMessageStore(account)
val outboxFolderId = outboxFolderManager.getOutboxFolderId(account.id)
return messageStore.getFolder(folderId) { folder ->
FolderDetails(
folder = Folder(
id = folder.id,
name = folder.name,
type = folder.getFolderType(account, outboxFolderId),
isLocalOnly = folder.isLocalOnly,
),
isInTopGroup = folder.isInTopGroup,
isIntegrate = folder.isIntegrate,
isSyncEnabled = folder.isSyncEnabled,
isVisible = folder.isVisible,
isNotificationsEnabled = folder.isNotificationsEnabled,
isPushEnabled = folder.isPushEnabled,
)
}
}
@Throws(MessagingException::class)
fun getRemoteFolders(accountUuid: String): List<RemoteFolder> {
val messageStore = messageStoreManager.getMessageStore(accountUuid)
return messageStore.getFolders(excludeLocalOnly = true) { folder ->
RemoteFolder(
id = folder.id,
serverId = folder.serverIdOrThrow(),
name = folder.name,
type = folder.type.toFolderType(),
)
}
}
@Throws(MessagingException::class)
fun getRemoteFolders(account: LegacyAccount): List<RemoteFolder> =
getRemoteFolders(account.uuid)
fun getRemoteFolderDetails(account: LegacyAccount): List<RemoteFolderDetails> {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolders(excludeLocalOnly = true) { folder ->
RemoteFolderDetails(
folder = RemoteFolder(
id = folder.id,
serverId = folder.serverIdOrThrow(),
name = folder.name,
type = folder.type.toFolderType(),
),
isInTopGroup = folder.isInTopGroup,
isIntegrate = folder.isIntegrate,
isSyncEnabled = folder.isSyncEnabled,
isVisible = folder.isVisible,
isNotificationsEnabled = folder.isNotificationsEnabled,
isPushEnabled = folder.isPushEnabled,
)
}
}
fun getPushFoldersFlow(account: LegacyAccount): Flow<List<RemoteFolder>> {
val messageStore = messageStoreManager.getMessageStore(account)
return callbackFlow {
send(getPushFolders(account))
val listener = FolderSettingsChangedListener {
trySendBlocking(getPushFolders(account))
}
messageStore.addFolderSettingsChangedListener(listener)
awaitClose {
messageStore.removeFolderSettingsChangedListener(listener)
}
}.buffer(capacity = Channel.CONFLATED)
.distinctUntilChanged()
.flowOn(ioDispatcher)
}
private fun getPushFolders(account: LegacyAccount): List<RemoteFolder> {
return getRemoteFolderDetails(account)
.asSequence()
.filter { folderDetails -> folderDetails.isPushEnabled }
.map { folderDetails -> folderDetails.folder }
.toList()
}
fun getFolderServerId(account: LegacyAccount, folderId: Long): String? {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolder(folderId) { folder ->
folder.serverId
}
}
fun getFolderId(account: LegacyAccount, folderServerId: String): Long? {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolderId(folderServerId)
}
fun isFolderPresent(account: LegacyAccount, folderId: Long): Boolean {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolder(folderId) { true } ?: false
}
fun updateFolderDetails(account: LegacyAccount, folderDetails: FolderDetails) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.updateFolderSettings(folderDetails)
}
fun setIncludeInUnifiedInbox(account: LegacyAccount, folderId: Long, includeInUnifiedInbox: Boolean) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setIncludeInUnifiedInbox(folderId, includeInUnifiedInbox)
}
fun setVisible(account: LegacyAccount, folderId: Long, visible: Boolean) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setVisible(folderId, visible)
}
fun setSyncEnabled(account: LegacyAccount, folderId: Long, enable: Boolean) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setSyncEnabled(folderId, enable)
}
fun setNotificationsEnabled(account: LegacyAccount, folderId: Long, enable: Boolean) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setNotificationsEnabled(folderId, enable)
}
fun setPushDisabled(account: LegacyAccount) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setPushDisabled()
}
fun hasPushEnabledFolder(account: LegacyAccount): Boolean {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.hasPushEnabledFolder()
}
fun hasPushEnabledFolderFlow(account: LegacyAccount): Flow<Boolean> {
val messageStore = messageStoreManager.getMessageStore(account)
return callbackFlow {
send(hasPushEnabledFolder(account))
val listener = FolderSettingsChangedListener {
trySendBlocking(hasPushEnabledFolder(account))
}
messageStore.addFolderSettingsChangedListener(listener)
awaitClose {
messageStore.removeFolderSettingsChangedListener(listener)
}
}.buffer(capacity = Channel.CONFLATED)
.distinctUntilChanged()
.flowOn(ioDispatcher)
}
private fun FolderDetailsAccessor.getFolderType(account: LegacyAccount, outboxFolderId: Long): FolderType =
if (id == outboxFolderId) {
FolderType.OUTBOX
} else {
folderTypeOf(account, id)
}
}
data class RemoteFolderDetails(
val folder: RemoteFolder,
val isInTopGroup: Boolean,
val isIntegrate: Boolean,
val isSyncEnabled: Boolean,
val isVisible: Boolean,
val isNotificationsEnabled: Boolean,
val isPushEnabled: Boolean,
)

View file

@ -0,0 +1,11 @@
package app.k9mail.legacy.mailstore
data class FolderSettings(
val visibleLimit: Int,
val isVisible: Boolean,
val isSyncEnabled: Boolean,
val isNotificationsEnabled: Boolean,
val isPushEnabled: Boolean,
val inTopGroup: Boolean,
val integrate: Boolean,
)

View file

@ -0,0 +1,17 @@
package app.k9mail.legacy.mailstore
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.mail.folder.api.FolderType
object FolderTypeMapper {
fun folderTypeOf(account: LegacyAccount, folderId: Long) = when (folderId) {
account.inboxFolderId -> FolderType.INBOX
account.sentFolderId -> FolderType.SENT
account.trashFolderId -> FolderType.TRASH
account.draftsFolderId -> FolderType.DRAFTS
account.archiveFolderId -> FolderType.ARCHIVE
account.spamFolderId -> FolderType.SPAM
else -> FolderType.REGULAR
}
}

View file

@ -0,0 +1,73 @@
package app.k9mail.legacy.mailstore
import java.util.concurrent.CopyOnWriteArraySet
import net.thunderbird.feature.mail.folder.api.FolderDetails
@Suppress("TooManyFunctions")
class ListenableMessageStore(private val messageStore: MessageStore) : MessageStore by messageStore {
private val folderSettingsListener = CopyOnWriteArraySet<FolderSettingsChangedListener>()
override fun createFolders(folders: List<CreateFolderInfo>): Set<Long> {
return messageStore.createFolders(folders).also {
notifyFolderSettingsChanged()
}
}
override fun deleteFolders(folderServerIds: List<String>) {
messageStore.deleteFolders(folderServerIds)
notifyFolderSettingsChanged()
}
override fun updateFolderSettings(folderDetails: FolderDetails) {
messageStore.updateFolderSettings(folderDetails)
notifyFolderSettingsChanged()
}
override fun setIncludeInUnifiedInbox(folderId: Long, includeInUnifiedInbox: Boolean) {
messageStore.setIncludeInUnifiedInbox(folderId, includeInUnifiedInbox)
notifyFolderSettingsChanged()
}
override fun setVisible(folderId: Long, visible: Boolean) {
messageStore.setVisible(folderId, visible)
notifyFolderSettingsChanged()
}
override fun setSyncEnabled(folderId: Long, enable: Boolean) {
messageStore.setSyncEnabled(folderId, enable)
notifyFolderSettingsChanged()
}
override fun setPushEnabled(folderId: Long, enable: Boolean) {
messageStore.setPushEnabled(folderId, enable)
notifyFolderSettingsChanged()
}
override fun setNotificationsEnabled(folderId: Long, enable: Boolean) {
messageStore.setNotificationsEnabled(folderId, enable)
notifyFolderSettingsChanged()
}
override fun setPushDisabled() {
messageStore.setPushDisabled()
notifyFolderSettingsChanged()
}
fun addFolderSettingsChangedListener(listener: FolderSettingsChangedListener) {
folderSettingsListener.add(listener)
}
fun removeFolderSettingsChangedListener(listener: FolderSettingsChangedListener) {
folderSettingsListener.remove(listener)
}
private fun notifyFolderSettingsChanged() {
for (listener in folderSettingsListener) {
listener.onFolderSettingsChanged()
}
}
}
fun interface FolderSettingsChangedListener {
fun onFolderSettingsChanged()
}

View file

@ -0,0 +1,5 @@
package app.k9mail.legacy.mailstore
fun interface MessageListChangedListener {
fun onMessageListChanged()
}

View file

@ -0,0 +1,31 @@
package app.k9mail.legacy.mailstore
interface MessageListRepository {
fun addListener(listener: MessageListChangedListener)
fun addListener(accountUuid: String, listener: MessageListChangedListener)
fun removeListener(listener: MessageListChangedListener)
fun notifyMessageListChanged(accountUuid: String)
fun <T> getMessages(
accountUuid: String,
selection: String,
selectionArgs: Array<String>,
sortOrder: String,
messageMapper: MessageMapper<T>,
): List<T>
fun <T> getThreadedMessages(
accountUuid: String,
selection: String,
selectionArgs: Array<String>,
sortOrder: String,
messageMapper: MessageMapper<T>,
): List<T>
fun <T> getThread(
accountUuid: String,
threadId: Long,
sortOrder: String,
messageMapper: MessageMapper<T>,
): List<T>
}

View file

@ -0,0 +1,28 @@
package app.k9mail.legacy.mailstore
import app.k9mail.legacy.message.extractors.PreviewResult
import com.fsck.k9.mail.Address
fun interface MessageMapper<T> {
fun map(message: MessageDetailsAccessor): T
}
interface MessageDetailsAccessor {
val id: Long
val messageServerId: String
val folderId: Long
val fromAddresses: List<Address>
val toAddresses: List<Address>
val ccAddresses: List<Address>
val messageDate: Long
val internalDate: Long
val subject: String?
val preview: PreviewResult
val isRead: Boolean
val isStarred: Boolean
val isAnswered: Boolean
val isForwarded: Boolean
val hasAttachments: Boolean
val threadRoot: Long
val threadCount: Int
}

View file

@ -0,0 +1,373 @@
package app.k9mail.legacy.mailstore
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.FolderType
import com.fsck.k9.mail.Header
import java.util.Date
import kotlin.jvm.Throws
import net.thunderbird.core.common.exception.MessagingException
import net.thunderbird.feature.mail.folder.api.FolderDetails
import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
/**
* Functions for accessing and modifying locally stored messages.
*
* The goal is for this to gradually replace [LocalStore]. Once complete, apps will be able to provide their own
* storage implementation.
*/
@Suppress("TooManyFunctions")
interface MessageStore {
/**
* Save a remote message in this store.
*/
fun saveRemoteMessage(folderId: Long, messageServerId: String, messageData: SaveMessageData)
/**
* Save a local message in this store.
*
* @param existingMessageId The message with this ID is replaced if not `null`.
* @return The message ID of the saved message.
*/
fun saveLocalMessage(folderId: Long, messageData: SaveMessageData, existingMessageId: Long? = null): Long
/**
* Copy a message to another folder.
*
* @return The message's database ID in the destination folder.
*/
fun copyMessage(messageId: Long, destinationFolderId: Long): Long
/**
* Copy messages to another folder.
*
* @return A mapping of the original message database ID to the new message database ID.
*/
fun copyMessages(messageIds: Collection<Long>, destinationFolderId: Long): Map<Long, Long> {
return messageIds
.map { messageId ->
messageId to copyMessage(messageId, destinationFolderId)
}
.toMap()
}
/**
* Move a message to another folder.
*
* @return The message's database ID in the destination folder. This will most likely be different from the
* messageId passed to this function.
*/
fun moveMessage(messageId: Long, destinationFolderId: Long): Long
/**
* Move messages to another folder.
*
* @return A mapping of the original message database ID to the new message database ID.
*/
fun moveMessages(messageIds: Collection<Long>, destinationFolderId: Long): Map<Long, Long> {
return messageIds
.map { messageId ->
messageId to moveMessage(messageId, destinationFolderId)
}
.toMap()
}
/**
* Set message flags.
*/
fun setFlag(messageIds: Collection<Long>, flag: Flag, set: Boolean)
/**
* Set or remove a flag on a message.
*/
fun setMessageFlag(folderId: Long, messageServerId: String, flag: Flag, set: Boolean)
/**
* Set whether a message should be considered as new.
*/
fun setNewMessageState(folderId: Long, messageServerId: String, newMessage: Boolean)
/**
* Clear the new message state for all messages.
*/
fun clearNewMessageState()
/**
* Retrieve the server ID for a given message.
*/
fun getMessageServerId(messageId: Long): String?
/**
* Retrieve the server IDs for the given messages.
*
* @return A mapping of the message database ID to the message server ID.
*/
fun getMessageServerIds(messageIds: Collection<Long>): Map<Long, String>
/**
* Retrieve server IDs for all remote messages in the given folder.
*/
fun getMessageServerIds(folderId: Long): Set<String>
/**
* Check if a message is present in the store.
*/
fun isMessagePresent(folderId: Long, messageServerId: String): Boolean
/**
* Get the flags associated with a message.
*/
fun getMessageFlags(folderId: Long, messageServerId: String): Set<Flag>
/**
* Retrieve server IDs and dates for all remote messages in the given folder.
*/
fun getAllMessagesAndEffectiveDates(folderId: Long): Map<String, Long?>
/**
* Retrieve list of messages.
*/
fun <T> getMessages(
selection: String,
selectionArgs: Array<String>,
sortOrder: String,
messageMapper: MessageMapper<out T?>,
): List<T>
/**
* Retrieve threaded list of messages.
*/
fun <T> getThreadedMessages(
selection: String,
selectionArgs: Array<String>,
sortOrder: String,
messageMapper: MessageMapper<out T?>,
): List<T>
/**
* Retrieve list of messages in a thread.
*/
fun <T> getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper<out T?>): List<T>
/**
* Retrieve the date of the oldest message in the given folder.
*/
fun getOldestMessageDate(folderId: Long): Date?
/**
* Retrieve the header fields of a message.
*/
fun getHeaders(folderId: Long, messageServerId: String): List<Header>
/**
* Retrieve selected header fields of a message.
*/
fun getHeaders(folderId: Long, messageServerId: String, headerNames: Set<String>): List<Header>
/**
* Return the size of this message store in bytes.
*/
fun getSize(): Long
/**
* Remove messages from the store.
*/
fun destroyMessages(folderId: Long, messageServerIds: Collection<String>)
/**
* Create folders.
*/
@Throws(MessagingException::class)
fun createFolders(folders: List<CreateFolderInfo>): Set<Long>
/**
* Retrieve information about a folder.
*
* @param mapper A function to map the values read from the store to a domain-specific object.
* @return The value returned by [mapper] or `null` if the folder wasn't found.
*/
fun <T> getFolder(folderId: Long, mapper: FolderMapper<T>): T?
/**
* Retrieve information about a folder.
*
* @param mapper A function to map the values read from the store to a domain-specific object.
* @return The value returned by [mapper] or `null` if the folder wasn't found.
*/
fun <T> getFolder(folderServerId: String, mapper: FolderMapper<T>): T?
/**
* Retrieve folders.
*
* @param mapper A function to map the values read from the store to a domain-specific object.
* @return A list of values returned by [mapper].
*/
@Throws(MessagingException::class)
fun <T> getFolders(excludeLocalOnly: Boolean, mapper: FolderMapper<T>): List<T>
/**
* Retrieve folders with their unread count.
*
* For the Outbox the total number of messages will be returned.
*/
fun <T> getDisplayFolders(includeHiddenFolders: Boolean, outboxFolderId: Long?, mapper: FolderMapper<T>): List<T>
/**
* Check if all given folders are included in the Unified Inbox.
*/
fun areAllIncludedInUnifiedInbox(folderIds: Collection<Long>): Boolean
/**
* Find a folder with the given server ID and return its store ID.
*/
fun getFolderId(folderServerId: String): Long?
/**
* Find a folder with the given store ID and return its server ID.
*/
fun getFolderServerId(folderId: Long): String?
/**
* Retrieve the number of messages in a folder.
*/
fun getMessageCount(folderId: Long): Int
/**
* Retrieve the number of unread messages in a folder.
*/
fun getUnreadMessageCount(folderId: Long): Int
/**
* Retrieve the number of unread messages matching [conditions].
*/
fun getUnreadMessageCount(conditions: SearchConditionTreeNode?): Int
/**
* Retrieve the number of starred messages matching [conditions].
*/
fun getStarredMessageCount(conditions: SearchConditionTreeNode?): Int
/**
* Update a folder's name and type.
*
* @throws MessagingException in case it fails changing the folder in the local database.
*/
@Throws(MessagingException::class)
fun changeFolder(folderServerId: String, name: String, type: FolderType)
/**
* Update settings of a single folder.
*/
fun updateFolderSettings(folderDetails: FolderDetails)
/**
* Update the "integrate" setting of a folder.
*/
fun setIncludeInUnifiedInbox(folderId: Long, includeInUnifiedInbox: Boolean)
/**
* Update the "visible" setting of a folder.
*/
fun setVisible(folderId: Long, visible: Boolean)
/**
* Update the sync setting of a folder.
*/
fun setSyncEnabled(folderId: Long, enable: Boolean)
/**
* Update the push setting of a folder.
*/
fun setPushEnabled(folderId: Long, enable: Boolean)
/**
* Update the notifications setting of a folder.
*/
fun setNotificationsEnabled(folderId: Long, enable: Boolean)
/**
* Get the 'more messages' state of a folder.
*/
fun hasMoreMessages(folderId: Long): MoreMessages?
/**
* Update the 'more messages' state of a folder.
*/
fun setMoreMessages(folderId: Long, moreMessages: MoreMessages)
/**
* Update the time when the folder was last checked for new messages.
*/
fun setLastChecked(folderId: Long, timestamp: Long)
/**
* Update folder status message.
*/
fun setStatus(folderId: Long, status: String?)
/**
* Update a folder's "visible limit" value.
*/
fun setVisibleLimit(folderId: Long, visibleLimit: Int)
/**
* Disable the push setting of all folders.
*/
fun setPushDisabled()
/**
* Returns `true` if there is at least one folder with the push setting enabled.
*/
fun hasPushEnabledFolder(): Boolean
/**
* Delete folders.
*/
fun deleteFolders(folderServerIds: List<String>)
/**
* Retrieve a string property by name.
*
* For everything that doesn't fit into existing structures this message store offers a generic key/value store.
*/
fun getExtraString(name: String): String?
/**
* Create or update a string property.
*/
fun setExtraString(name: String, value: String)
/**
* Retrieve a number property by name.
*/
fun getExtraNumber(name: String): Long?
/**
* Create or update a number property.
*/
fun setExtraNumber(name: String, value: Long)
/**
* Retrieve a string property associated with the given folder.
*/
fun getFolderExtraString(folderId: Long, name: String): String?
/**
* Create or update a string property associated with the given folder.
*/
fun setFolderExtraString(folderId: Long, name: String, value: String?)
/**
* Retrieve a number property associated with the given folder.
*/
fun getFolderExtraNumber(folderId: Long, name: String): Long?
/**
* Create or update a number property associated with the given folder.
*/
fun setFolderExtraNumber(folderId: Long, name: String, value: Long)
/**
* Optimize the message store with the goal of using the minimal amount of disk space.
*/
fun compact()
}

View file

@ -0,0 +1,7 @@
package app.k9mail.legacy.mailstore
import net.thunderbird.core.android.account.LegacyAccount
interface MessageStoreFactory {
fun create(account: LegacyAccount): ListenableMessageStore
}

View file

@ -0,0 +1,31 @@
package app.k9mail.legacy.mailstore
import java.util.concurrent.ConcurrentHashMap
import net.thunderbird.core.android.account.AccountManager
import net.thunderbird.core.android.account.LegacyAccount
class MessageStoreManager(
private val accountManager: AccountManager,
private val messageStoreFactory: MessageStoreFactory,
) {
private val messageStores = ConcurrentHashMap<String, ListenableMessageStore>()
init {
accountManager.addAccountRemovedListener { account ->
removeMessageStore(account.uuid)
}
}
fun getMessageStore(accountUuid: String): ListenableMessageStore {
val account = accountManager.getAccount(accountUuid) ?: error("Account not found: $accountUuid")
return getMessageStore(account)
}
fun getMessageStore(account: LegacyAccount): ListenableMessageStore {
return messageStores.getOrPut(account.uuid) { messageStoreFactory.create(account) }
}
private fun removeMessageStore(accountUuid: String) {
messageStores.remove(accountUuid)
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.legacy.mailstore;
public enum MoreMessages {
UNKNOWN("unknown"),
FALSE("false"),
TRUE("true");
private final String databaseName;
MoreMessages(String databaseName) {
this.databaseName = databaseName;
}
public static MoreMessages fromDatabaseName(String databaseName) {
for (MoreMessages value : MoreMessages.values()) {
if (value.databaseName.equals(databaseName)) {
return value;
}
}
throw new IllegalArgumentException("Unknown value: " + databaseName);
}
public String getDatabaseName() {
return databaseName;
}
}

View file

@ -0,0 +1,18 @@
package app.k9mail.legacy.mailstore
import net.thunderbird.feature.mail.folder.api.FolderType
import com.fsck.k9.mail.FolderType as RemoteFolderType
object RemoteFolderTypeMapper {
fun RemoteFolderType.toFolderType(): FolderType = when (this) {
RemoteFolderType.REGULAR -> FolderType.REGULAR
RemoteFolderType.INBOX -> FolderType.INBOX
RemoteFolderType.OUTBOX -> FolderType.REGULAR // We currently don't support remote Outbox folders
RemoteFolderType.DRAFTS -> FolderType.DRAFTS
RemoteFolderType.SENT -> FolderType.SENT
RemoteFolderType.TRASH -> FolderType.TRASH
RemoteFolderType.SPAM -> FolderType.SPAM
RemoteFolderType.ARCHIVE -> FolderType.ARCHIVE
}
}

View file

@ -0,0 +1,17 @@
package app.k9mail.legacy.mailstore
import app.k9mail.legacy.message.extractors.PreviewResult
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.MessageDownloadState
data class SaveMessageData(
val message: Message,
val subject: String?,
val date: Long,
val internalDate: Long,
val downloadState: MessageDownloadState,
val attachmentCount: Int,
val previewResult: PreviewResult,
val textForSearchIndex: String? = null,
val encryptionType: String?,
)

View file

@ -0,0 +1,51 @@
package app.k9mail.legacy.mailstore
import assertk.assertThat
import assertk.assertions.isSameInstanceAs
import net.thunderbird.core.android.account.AccountManager
import net.thunderbird.core.android.account.AccountRemovedListener
import net.thunderbird.core.android.account.LegacyAccount
import org.junit.Test
import org.mockito.kotlin.KStubbing
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
class MessageStoreManagerTest {
private val account = LegacyAccount("00000000-0000-4000-0000-000000000000")
private val messageStore1 = mock<ListenableMessageStore>(name = "messageStore1")
private val messageStore2 = mock<ListenableMessageStore>(name = "messageStore2")
private val messageStoreFactory = mock<MessageStoreFactory> {
on { create(account) } doReturn messageStore1 doReturn messageStore2
}
@Test
fun `MessageStore instance is reused`() {
val accountManager = mock<AccountManager>()
val messageStoreManager = MessageStoreManager(accountManager, messageStoreFactory)
assertThat(messageStoreManager.getMessageStore(account)).isSameInstanceAs(messageStore1)
assertThat(messageStoreManager.getMessageStore(account)).isSameInstanceAs(messageStore1)
}
@Test
fun `MessageStore instance is removed when account is removed`() {
val listenerCaptor = argumentCaptor<AccountRemovedListener>()
val accountManager = mock<AccountManager> {
doNothingOn { addAccountRemovedListener(listenerCaptor.capture()) }
}
val messageStoreManager = MessageStoreManager(accountManager, messageStoreFactory)
assertThat(messageStoreManager.getMessageStore(account)).isSameInstanceAs(messageStore1)
listenerCaptor.firstValue.onAccountRemoved(account)
assertThat(messageStoreManager.getMessageStore(account)).isSameInstanceAs(messageStore2)
}
private fun <T : Any> KStubbing<T>.doNothingOn(block: T.() -> Any) {
doNothing().whenever(mock).block()
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.legacy.mailstore;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MoreMessagesTest {
private static final String ERROR_MESSAGE = "The return value of getDatabaseName() is used in the database and " +
"must not be changed without data migration.";
@Test
public void UNKNOWN_getDatabaseName_shouldReturnUnknown() throws Exception {
assertEquals(ERROR_MESSAGE, "unknown", MoreMessages.UNKNOWN.getDatabaseName());
}
@Test
public void TRUE_getDatabaseName_shouldReturnTrue() throws Exception {
assertEquals(ERROR_MESSAGE, "true", MoreMessages.TRUE.getDatabaseName());
}
@Test
public void FALSE_getDatabaseName_shouldReturnFalse() throws Exception {
assertEquals(ERROR_MESSAGE, "false", MoreMessages.FALSE.getDatabaseName());
}
}