Skip to content

Commit

Permalink
IMAP move operation should ignore expunge policy
Browse files Browse the repository at this point in the history
Because the original IMAP specification doesn't include a move operation, we implement it as copy, followed by deleting the source message. Deleting messages in IMAP is a two stage process. First a message is marked as deleted, then the EXPUNGE command is issued. However, the EXPUNGE command will remove all messages in a folder marked as deleted. For a move operation, we don't want to remove other messages, and therefore won't issue the EXPUNGE command. However, if the server supports the UIDPLUS extension, we can specify which messages exactly should be expunged. So if that extension is available, we will use the UID EXPUNGE command on the source message of a move operation.

Since the EXPUNGE command removes all messages marked as deleted, K-9 Mail has a setting that controls when the command is issued (when deleting a message, when polling, manually via a menu option). Previously this setting was also used for move operations. However, that probably should have never been the case.
  • Loading branch information
cketti committed Nov 18, 2023
1 parent 9fe2c68 commit 8b66b04
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -893,11 +893,6 @@ void processPendingMoveOrCopy(Account account, long srcFolderId, long destFolder
}

if (operation != MoveOrCopyFlavor.COPY) {
if (backend.getSupportsExpunge() && account.getExpungePolicy() == Expunge.EXPUNGE_IMMEDIATELY) {
Timber.i("processingPendingMoveOrCopy expunging folder %s:%s", account, srcFolderServerId);
backend.expungeMessages(srcFolderServerId, uids);
}

destroyPlaceholderMessages(localSourceFolder, uids);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,11 @@ internal class RealImapFolder(
return null
}

val uids = messages.map { it.uid }

val uidMapping = copyMessages(messages, folder)
setFlags(messages, setOf(Flag.DELETED), true)
expungeUidsOnly(uids)

return uidMapping
}
Expand Down Expand Up @@ -1080,8 +1083,15 @@ internal class RealImapFolder(
}
}

@Throws(MessagingException::class)
override fun expungeUids(uids: List<String>) {
expungeUids(uids, fullExpungeFallback = true)
}

private fun expungeUidsOnly(uids: List<String>) {
expungeUids(uids, fullExpungeFallback = false)
}

private fun expungeUids(uids: List<String>, fullExpungeFallback: Boolean) {
require(uids.isNotEmpty()) { "expungeUids() must be called with a non-empty set of UIDs" }

open(OpenMode.READ_WRITE)
Expand All @@ -1091,8 +1101,10 @@ internal class RealImapFolder(
if (connection!!.isUidPlusCapable) {
val longUids = uids.map { it.toLong() }.toSet()
connection!!.executeCommandWithIdSet(Commands.UID_EXPUNGE, "", longUids)
} else {
} else if (fullExpungeFallback) {
executeSimpleCommand("EXPUNGE")
} else {
Timber.v("Server doesn't support expunging individual messages: %s", uids)
}
} catch (ioe: IOException) {
throw ioExceptionHandler(connection, ioe)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever

class RealImapFolderTest {
Expand Down Expand Up @@ -312,16 +314,37 @@ class RealImapFolderTest {
}

@Test
fun moveMessages_shouldDeleteMessagesFromSourceFolder() {
fun `moveMessages() should delete messages from source folder but not issue EXPUNGE command`() {
val sourceFolder = createFolder("Folder")
prepareImapFolderForOpen(OpenMode.READ_WRITE)
imapConnection.stub {
on { isUidPlusCapable } doReturn false
}
val destinationFolder = createFolder("Destination")
val messages = listOf(createImapMessage("1"))
sourceFolder.open(OpenMode.READ_WRITE)

sourceFolder.moveMessages(messages, destinationFolder)

assertCommandWithIdsIssued("UID STORE 1 +FLAGS.SILENT (\\Deleted)")
verify(imapConnection, never()).executeSimpleCommand("EXPUNGE")
}

@Test
fun `moveMessages() should delete messages from source folder and issue UID EXPUNGE command when available`() {
val sourceFolder = createFolder("Folder")
prepareImapFolderForOpen(OpenMode.READ_WRITE)
imapConnection.stub {
on { isUidPlusCapable } doReturn true
}
val destinationFolder = createFolder("Destination")
val messages = listOf(createImapMessage("1"))
sourceFolder.open(OpenMode.READ_WRITE)

sourceFolder.moveMessages(messages, destinationFolder)

assertCommandWithIdsIssued("UID STORE 1 +FLAGS.SILENT (\\Deleted)")
assertCommandWithIdsIssued("UID EXPUNGE 1")
}

@Test
Expand Down

0 comments on commit 8b66b04

Please sign in to comment.