diff options
| author | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
|---|---|---|
| committer | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
| commit | 50ce5d5e6cd0d720ff437d35eace545816848428 (patch) | |
| tree | b59af206a36265cf081945e434900b32b0c43edd /backend/imap/src/test/java/com | |
| download | monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2 monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip | |
Inital
Diffstat (limited to 'backend/imap/src/test/java/com')
5 files changed, 745 insertions, 0 deletions
diff --git a/backend/imap/src/test/java/com/fsck/k9/backend/imap/BackendIdleRefreshManagerTest.kt b/backend/imap/src/test/java/com/fsck/k9/backend/imap/BackendIdleRefreshManagerTest.kt new file mode 100644 index 0000000..04fc3e4 --- /dev/null +++ b/backend/imap/src/test/java/com/fsck/k9/backend/imap/BackendIdleRefreshManagerTest.kt @@ -0,0 +1,185 @@ +package com.fsck.k9.backend.imap + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import org.junit.Test + +private const val START_TIME = 100_000_000L + +class BackendIdleRefreshManagerTest { + val alarmManager = MockSystemAlarmManager(START_TIME) + val idleRefreshManager = BackendIdleRefreshManager(alarmManager) + + @Test + fun `single timer`() { + val timeout = 15 * 60 * 1000L + val callback = RecordingCallback() + + idleRefreshManager.startTimer(timeout, callback::alarm) + alarmManager.advanceTime(timeout) + + assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout)) + assertThat(callback.wasCalled).isTrue() + } + + @Test + fun `starting two timers in quick succession`() { + val timeout = 15 * 60 * 1000L + val callback1 = RecordingCallback() + val callback2 = RecordingCallback() + + idleRefreshManager.startTimer(timeout, callback1::alarm) + // Advance clock less than MIN_TIMER_DELTA + alarmManager.advanceTime(100) + idleRefreshManager.startTimer(timeout, callback2::alarm) + alarmManager.advanceTime(timeout) + + assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout)) + assertThat(callback1.wasCalled).isTrue() + assertThat(callback2.wasCalled).isTrue() + } + + @Test + fun `starting second timer some time after first should trigger both at initial trigger time`() { + val timeout = 15 * 60 * 1000L + val waitTime = 10 * 60 * 1000L + val callback1 = RecordingCallback() + val callback2 = RecordingCallback() + + idleRefreshManager.startTimer(timeout, callback1::alarm) + // Advance clock by more than MIN_TIMER_DELTA but less than 'timeout' + alarmManager.advanceTime(waitTime) + + assertThat(callback1.wasCalled).isFalse() + + idleRefreshManager.startTimer(timeout, callback2::alarm) + alarmManager.advanceTime(timeout - waitTime) + + assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout)) + assertThat(callback1.wasCalled).isTrue() + assertThat(callback2.wasCalled).isTrue() + } + + @Test + fun `second timer with lower timeout should reschedule alarm`() { + val timeout1 = 15 * 60 * 1000L + val timeout2 = 10 * 60 * 1000L + val callback1 = RecordingCallback() + val callback2 = RecordingCallback() + + idleRefreshManager.startTimer(timeout1, callback1::alarm) + + assertThat(alarmManager.triggerTime).isEqualTo(START_TIME + timeout1) + + idleRefreshManager.startTimer(timeout2, callback2::alarm) + alarmManager.advanceTime(timeout2) + + assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout1, START_TIME + timeout2)) + assertThat(callback1.wasCalled).isTrue() + assertThat(callback2.wasCalled).isTrue() + } + + @Test + fun `do not trigger timers earlier than necessary`() { + val timeout1 = 10 * 60 * 1000L + val timeout2 = 23 * 60 * 1000L + val callback1 = RecordingCallback() + val callback2 = RecordingCallback() + val callback3 = RecordingCallback() + + idleRefreshManager.startTimer(timeout1, callback1::alarm) + idleRefreshManager.startTimer(timeout2, callback2::alarm) + + alarmManager.advanceTime(timeout1) + assertThat(callback1.wasCalled).isTrue() + assertThat(callback2.wasCalled).isFalse() + + idleRefreshManager.startTimer(timeout1, callback3::alarm) + + alarmManager.advanceTime(timeout1) + + assertThat(alarmManager.alarmTimes).isEqualTo( + listOf(START_TIME + timeout1, START_TIME + timeout2, START_TIME + timeout1 + timeout1) + ) + assertThat(callback2.wasCalled).isTrue() + assertThat(callback3.wasCalled).isTrue() + } + + @Test + fun `reset timers`() { + val timeout = 10 * 60 * 1000L + val callback = RecordingCallback() + + idleRefreshManager.startTimer(timeout, callback::alarm) + + alarmManager.advanceTime(5 * 60 * 1000L) + assertThat(callback.wasCalled).isFalse() + + idleRefreshManager.resetTimers() + + assertThat(alarmManager.triggerTime).isEqualTo(NO_TRIGGER_TIME) + assertThat(callback.wasCalled).isTrue() + } + + @Test + fun `cancel timer`() { + val timeout = 10 * 60 * 1000L + val callback = RecordingCallback() + + val timer = idleRefreshManager.startTimer(timeout, callback::alarm) + + alarmManager.advanceTime(5 * 60 * 1000L) + timer.cancel() + + assertThat(alarmManager.triggerTime).isEqualTo(NO_TRIGGER_TIME) + assertThat(callback.wasCalled).isFalse() + } +} + +class RecordingCallback { + var wasCalled = false + private set + + fun alarm() { + wasCalled = true + } +} + +typealias Callback = () -> Unit +private const val NO_TRIGGER_TIME = -1L + +class MockSystemAlarmManager(startTime: Long) : SystemAlarmManager { + var now = startTime + var triggerTime = NO_TRIGGER_TIME + var callback: Callback? = null + val alarmTimes = mutableListOf<Long>() + + override fun setAlarm(triggerTime: Long, callback: () -> Unit) { + this.triggerTime = triggerTime + this.callback = callback + alarmTimes.add(triggerTime) + } + + override fun cancelAlarm() { + this.triggerTime = NO_TRIGGER_TIME + this.callback = null + } + + override fun now(): Long = now + + fun advanceTime(delta: Long) { + now += delta + if (now >= triggerTime) { + trigger() + } + } + + private fun trigger() { + callback?.invoke().also { + triggerTime = NO_TRIGGER_TIME + callback = null + } + } +} diff --git a/backend/imap/src/test/java/com/fsck/k9/backend/imap/ImapSyncTest.kt b/backend/imap/src/test/java/com/fsck/k9/backend/imap/ImapSyncTest.kt new file mode 100644 index 0000000..1417faf --- /dev/null +++ b/backend/imap/src/test/java/com/fsck/k9/backend/imap/ImapSyncTest.kt @@ -0,0 +1,330 @@ +package com.fsck.k9.backend.imap + +import app.k9mail.backend.testing.InMemoryBackendStorage +import assertk.assertThat +import assertk.assertions.containsAll +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.isEmpty +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.fsck.k9.backend.api.FolderInfo +import com.fsck.k9.backend.api.SyncConfig +import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy +import com.fsck.k9.backend.api.SyncListener +import com.fsck.k9.mail.FetchProfile +import com.fsck.k9.mail.Flag +import com.fsck.k9.mail.FolderType +import com.fsck.k9.mail.Message +import com.fsck.k9.mail.MessageDownloadState +import com.fsck.k9.mail.buildMessage +import com.fsck.k9.mail.store.imap.FetchListener +import com.fsck.k9.mail.store.imap.ImapMessage +import java.util.Date +import org.apache.james.mime4j.dom.field.DateTimeField +import org.apache.james.mime4j.field.DefaultFieldParser +import org.junit.Test +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never + +private const val ACCOUNT_NAME = "Account-1" +private const val FOLDER_SERVER_ID = "FOLDER_ONE" +private const val MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE = 1000 +private const val DEFAULT_VISIBLE_LIMIT = 25 +private const val DEFAULT_MESSAGE_DATE = "Tue, 04 Jan 2022 10:00:00 +0100" + +class ImapSyncTest { + private val backendStorage = createBackendStorage() + private val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID) + private val imapStore = TestImapStore() + private val imapFolder = imapStore.addFolder(FOLDER_SERVER_ID) + private val imapSync = ImapSync(ACCOUNT_NAME, backendStorage, imapStore) + private val syncListener = mock<SyncListener>() + private val defaultSyncConfig = createSyncConfig() + + @Test + fun `sync of empty folder should notify listener`() { + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + verify(syncListener).syncStarted(FOLDER_SERVER_ID) + verify(syncListener).syncAuthenticationSuccess() + verify(syncListener).syncFinished(FOLDER_SERVER_ID) + verify(syncListener, never()).syncFailed(folderServerId = any(), message = any(), exception = any()) + } + + @Test + fun `sync of folder with negative messageCount should return an error`() { + imapFolder.messageCount = -1 + + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + verify(syncListener).syncFailed( + folderServerId = eq(FOLDER_SERVER_ID), + message = eq("Exception: Message count -1 for folder $FOLDER_SERVER_ID"), + exception = any() + ) + } + + @Test + fun `successful sync should close folder`() { + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + assertThat(imapFolder.isClosed).isTrue() + } + + @Test + fun `sync with error should close folder`() { + imapFolder.messageCount = -1 + + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + assertThat(imapFolder.isClosed).isTrue() + } + + @Test + fun `sync with ExpungePolicy ON_POLL should expunge remote folder`() { + val syncConfig = defaultSyncConfig.copy(expungePolicy = ExpungePolicy.ON_POLL) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(imapFolder.wasExpunged).isTrue() + } + + @Test + fun `sync with ExpungePolicy MANUALLY should not expunge remote folder`() { + val syncConfig = defaultSyncConfig.copy(expungePolicy = ExpungePolicy.MANUALLY) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(imapFolder.wasExpunged).isFalse() + } + + @Test + fun `sync with ExpungePolicy IMMEDIATELY should not expunge remote folder`() { + val syncConfig = defaultSyncConfig.copy(expungePolicy = ExpungePolicy.IMMEDIATELY) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(imapFolder.wasExpunged).isFalse() + } + + @Test + fun `sync with syncRemoteDeletions=true should remove local messages`() { + addMessageToBackendFolder(uid = 42) + val syncConfig = defaultSyncConfig.copy(syncRemoteDeletions = true) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(backendFolder.getMessageServerIds()).isEmpty() + verify(syncListener).syncStarted(FOLDER_SERVER_ID) + verify(syncListener).syncFinished(FOLDER_SERVER_ID) + } + + @Test + fun `sync with syncRemoteDeletions=false should not remove local messages`() { + addMessageToBackendFolder(uid = 23) + val syncConfig = defaultSyncConfig.copy(syncRemoteDeletions = false) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(backendFolder.getMessageServerIds()).containsExactlyInAnyOrder("23") + verify(syncListener).syncStarted(FOLDER_SERVER_ID) + verify(syncListener).syncFinished(FOLDER_SERVER_ID) + } + + @Test + fun `sync should remove messages older than earliestPollDate`() { + addMessageToImapAndBackendFolder(uid = 23, date = "Mon, 03 Jan 2022 10:00:00 +0100") + addMessageToImapAndBackendFolder(uid = 42, date = "Wed, 05 Jan 2022 20:00:00 +0100") + val syncConfig = defaultSyncConfig.copy( + syncRemoteDeletions = true, + earliestPollDate = "Tue, 04 Jan 2022 12:00:00 +0100".toDate() + ) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(backendFolder.getMessageServerIds()).containsExactlyInAnyOrder("42") + } + + @Test + fun `sync with new messages on server should download messages`() { + addMessageToImapFolder(uid = 9) + addMessageToImapFolder(uid = 13) + + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + assertThat(backendFolder.getMessageServerIds()).containsExactlyInAnyOrder("9", "13") + verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "9", isOldMessage = false) + verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "13", isOldMessage = false) + } + + @Test + fun `sync downloading old messages should notify listener with isOldMessage=true`() { + addMessageToBackendFolder(uid = 42) + addMessageToImapFolder(uid = 23) + addMessageToImapFolder(uid = 42) + + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + assertThat(backendFolder.getMessageServerIds()).containsExactlyInAnyOrder("23", "42") + verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "23", isOldMessage = true) + } + + @Test + fun `determining the highest UID should use numerical ordering`() { + addMessageToBackendFolder(uid = 9) + addMessageToBackendFolder(uid = 100) + // When text ordering is used: "9" > "100" -> highest UID = 9 (when it should be 100) + // With 80 > 9 the message on the server is considered a new message, but it shouldn't be (80 < 100) + addMessageToImapFolder(uid = 80) + + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "80", isOldMessage = true) + } + + @Test + fun `sync should update flags of existing messages`() { + addMessageToBackendFolder(uid = 2) + addMessageToImapFolder(uid = 2, flags = setOf(Flag.SEEN, Flag.ANSWERED)) + + imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener) + + assertThat(backendFolder.getMessageFlags(messageServerId = "2")).containsAll(Flag.SEEN, Flag.ANSWERED) + } + + @Test + fun `sync with UIDVALIDITY change should clear all messages`() { + imapFolder.setUidValidity(1) + addMessageToImapFolder(uid = 300) + addMessageToImapFolder(uid = 301) + val syncConfig = defaultSyncConfig.copy(syncRemoteDeletions = false) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(backendFolder.getMessageServerIds()).containsExactlyInAnyOrder("300", "301") + + imapFolder.setUidValidity(9000) + imapFolder.removeAllMessages() + addMessageToImapFolder(uid = 1) + + imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener) + + assertThat(backendFolder.getMessageServerIds()).containsExactlyInAnyOrder("1") + verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "1", isOldMessage = false) + } + + @Test + fun `sync with multiple FETCH responses when downloading small message should report correct progress`() { + val folderServerId = "FOLDER_TWO" + backendStorage.createBackendFolder(folderServerId) + val specialImapFolder = object : TestImapFolder(folderServerId) { + override fun fetch( + messages: List<ImapMessage>, + fetchProfile: FetchProfile, + listener: FetchListener?, + maxDownloadSize: Int + ) { + super.fetch(messages, fetchProfile, listener, maxDownloadSize) + + // When fetching the body simulate an additional FETCH response + if (FetchProfile.Item.BODY in fetchProfile) { + val message = messages.first() + listener?.onFetchResponse(message, isFirstResponse = false) + } + } + } + specialImapFolder.addMessage(42) + imapStore.addFolder(specialImapFolder) + + imapSync.sync(folderServerId, defaultSyncConfig, syncListener) + + verify(syncListener, atLeast(1)).syncProgress(folderServerId, completed = 1, total = 1) + verify(syncListener, never()).syncProgress(folderServerId, completed = 2, total = 1) + } + + private fun addMessageToBackendFolder(uid: Long, date: String = DEFAULT_MESSAGE_DATE) { + val messageServerId = uid.toString() + val message = createSimpleMessage(messageServerId, date).apply { + setUid(messageServerId) + } + backendFolder.saveMessage(message, MessageDownloadState.FULL) + + val highestKnownUid = backendFolder.getFolderExtraNumber("imapHighestKnownUid") ?: 0 + if (uid > highestKnownUid) { + backendFolder.setFolderExtraNumber("imapHighestKnownUid", uid) + } + } + + private fun addMessageToImapFolder(uid: Long, flags: Set<Flag> = emptySet(), date: String = DEFAULT_MESSAGE_DATE) { + imapFolder.addMessage(uid, flags, date) + } + + private fun TestImapFolder.addMessage( + uid: Long, + flags: Set<Flag> = emptySet(), + date: String = DEFAULT_MESSAGE_DATE + ) { + val messageServerId = uid.toString() + val message = createSimpleMessage(messageServerId, date) + addMessage(uid, message) + + if (flags.isNotEmpty()) { + val imapMessage = getMessage(messageServerId) + setFlags(listOf(imapMessage), flags, true) + } + } + + private fun addMessageToImapAndBackendFolder(uid: Long, date: String) { + addMessageToBackendFolder(uid, date) + addMessageToImapFolder(uid, date = date) + } + + private fun createBackendStorage(): InMemoryBackendStorage { + return InMemoryBackendStorage().apply { + createBackendFolder(FOLDER_SERVER_ID) + } + } + + private fun InMemoryBackendStorage.createBackendFolder(serverId: String) { + createFolderUpdater().use { updater -> + val folderInfo = FolderInfo( + serverId = serverId, + name = "irrelevant", + type = FolderType.REGULAR + ) + updater.createFolders(listOf(folderInfo)) + } + } + + private fun createSyncConfig(): SyncConfig { + return SyncConfig( + expungePolicy = ExpungePolicy.MANUALLY, + earliestPollDate = null, + syncRemoteDeletions = true, + maximumAutoDownloadMessageSize = MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE, + defaultVisibleLimit = DEFAULT_VISIBLE_LIMIT, + syncFlags = setOf(Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED) + ) + } + + private fun createSimpleMessage(uid: String, date: String, text: String = "UID: $uid"): Message { + return buildMessage { + header("Subject", "Test Message") + header("From", "alice@domain.example") + header("To", "Bob <bob@domain.example>") + header("Date", date) + header("Message-ID", "<msg-$uid@domain.example>") + + textBody(text) + } + } +} + +private fun String.toDate(): Date { + val dateTimeField = DefaultFieldParser.parse("Date: $this") as DateTimeField + return dateTimeField.date +} diff --git a/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapFolder.kt b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapFolder.kt new file mode 100644 index 0000000..729a909 --- /dev/null +++ b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapFolder.kt @@ -0,0 +1,179 @@ +package com.fsck.k9.backend.imap + +import com.fsck.k9.mail.BodyFactory +import com.fsck.k9.mail.FetchProfile +import com.fsck.k9.mail.Flag +import com.fsck.k9.mail.Message +import com.fsck.k9.mail.MessageRetrievalListener +import com.fsck.k9.mail.Part +import com.fsck.k9.mail.store.imap.FetchListener +import com.fsck.k9.mail.store.imap.ImapFolder +import com.fsck.k9.mail.store.imap.ImapMessage +import com.fsck.k9.mail.store.imap.OpenMode +import com.fsck.k9.mail.store.imap.createImapMessage +import java.util.Date + +open class TestImapFolder(override val serverId: String) : ImapFolder { + override var mode: OpenMode? = null + protected set + + override var messageCount: Int = 0 + + var wasExpunged: Boolean = false + private set + + val isClosed: Boolean + get() = mode == null + + private val messages = mutableMapOf<Long, Message>() + private val messageFlags = mutableMapOf<Long, MutableSet<Flag>>() + private var uidValidity: Long? = null + + fun addMessage(uid: Long, message: Message) { + require(!messages.containsKey(uid)) { + "Folder '$serverId' already contains a message with the UID $uid" + } + + messages[uid] = message + messageFlags[uid] = mutableSetOf() + + messageCount = messages.size + } + + fun removeAllMessages() { + messages.clear() + messageFlags.clear() + } + + fun setUidValidity(value: Long) { + uidValidity = value + } + + override fun open(mode: OpenMode) { + this.mode = mode + } + + override fun close() { + mode = null + } + + override fun exists(): Boolean { + throw UnsupportedOperationException("not implemented") + } + + override fun getUidValidity() = uidValidity + + override fun getMessage(uid: String): ImapMessage { + return createImapMessage(uid) + } + + override fun getUidFromMessageId(messageId: String): String? { + throw UnsupportedOperationException("not implemented") + } + + override fun getMessages( + start: Int, + end: Int, + earliestDate: Date?, + listener: MessageRetrievalListener<ImapMessage>? + ): List<ImapMessage> { + require(start > 0) + require(end >= start) + require(end <= messages.size) + + return messages.keys.sortedDescending() + .slice((start - 1) until end) + .map { createImapMessage(uid = it.toString()) } + } + + override fun areMoreMessagesAvailable(indexOfOldestMessage: Int, earliestDate: Date?): Boolean { + throw UnsupportedOperationException("not implemented") + } + + override fun fetch( + messages: List<ImapMessage>, + fetchProfile: FetchProfile, + listener: FetchListener?, + maxDownloadSize: Int + ) { + if (messages.isEmpty()) return + + for (imapMessage in messages) { + val uid = imapMessage.uid.toLong() + + val flags = messageFlags[uid].orEmpty().toSet() + imapMessage.setFlags(flags, true) + + val storedMessage = this.messages[uid] ?: error("Message $uid not found") + for (header in storedMessage.headers) { + imapMessage.addHeader(header.name, header.value) + } + imapMessage.body = storedMessage.body + + listener?.onFetchResponse(imapMessage, isFirstResponse = true) + } + } + + override fun fetchPart( + message: ImapMessage, + part: Part, + bodyFactory: BodyFactory, + maxDownloadSize: Int + ) { + throw UnsupportedOperationException("not implemented") + } + + override fun search( + queryString: String?, + requiredFlags: Set<Flag>?, + forbiddenFlags: Set<Flag>?, + performFullTextSearch: Boolean + ): List<ImapMessage> { + throw UnsupportedOperationException("not implemented") + } + + override fun appendMessages(messages: List<Message>): Map<String, String>? { + throw UnsupportedOperationException("not implemented") + } + + override fun setFlags(flags: Set<Flag>, value: Boolean) { + if (value) { + for (messageFlagSet in messageFlags.values) { + messageFlagSet.addAll(flags) + } + } else { + for (messageFlagSet in messageFlags.values) { + messageFlagSet.removeAll(flags) + } + } + } + + override fun setFlags(messages: List<ImapMessage>, flags: Set<Flag>, value: Boolean) { + for (message in messages) { + val uid = message.uid.toLong() + val messageFlagSet = messageFlags[uid] ?: error("Unknown message with UID $uid") + if (value) { + messageFlagSet.addAll(flags) + } else { + messageFlagSet.removeAll(flags) + } + } + } + + override fun copyMessages(messages: List<ImapMessage>, folder: ImapFolder): Map<String, String>? { + throw UnsupportedOperationException("not implemented") + } + + override fun moveMessages(messages: List<ImapMessage>, folder: ImapFolder): Map<String, String>? { + throw UnsupportedOperationException("not implemented") + } + + override fun expunge() { + mode = OpenMode.READ_WRITE + wasExpunged = true + } + + override fun expungeUids(uids: List<String>) { + throw UnsupportedOperationException("not implemented") + } +} diff --git a/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapStore.kt b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapStore.kt new file mode 100644 index 0000000..db698d5 --- /dev/null +++ b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapStore.kt @@ -0,0 +1,48 @@ +package com.fsck.k9.backend.imap + +import com.fsck.k9.mail.FolderType +import com.fsck.k9.mail.store.imap.FolderListItem +import com.fsck.k9.mail.store.imap.ImapFolder +import com.fsck.k9.mail.store.imap.ImapStore + +class TestImapStore : ImapStore { + private val folders = mutableMapOf<String, ImapFolder>() + + fun addFolder(serverId: String): TestImapFolder { + require(!folders.containsKey(serverId)) { "Folder '$serverId' already exists" } + + return TestImapFolder(serverId).also { folder -> + folders[serverId] = folder + } + } + + fun addFolder(folder: ImapFolder) { + val serverId = folder.serverId + require(!folders.containsKey(serverId)) { "Folder '$serverId' already exists" } + + folders[serverId] = folder + } + + override fun getFolder(name: String): ImapFolder { + return folders[name] ?: error("Folder '$name' not found") + } + + override fun getFolders(): List<FolderListItem> { + return folders.values.map { folder -> + FolderListItem( + serverId = folder.serverId, + name = "irrelevant", + type = FolderType.REGULAR, + oldServerId = null + ) + } + } + + override fun checkSettings() { + throw UnsupportedOperationException("not implemented") + } + + override fun closeAllConnections() { + throw UnsupportedOperationException("not implemented") + } +} diff --git a/backend/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapMessageHelper.kt b/backend/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapMessageHelper.kt new file mode 100644 index 0000000..c78d12b --- /dev/null +++ b/backend/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapMessageHelper.kt @@ -0,0 +1,3 @@ +package com.fsck.k9.mail.store.imap + +fun createImapMessage(uid: String) = ImapMessage(uid) |
