Skip to content

Commit

Permalink
IMAP: Add support for untagged COPYUID responses
Browse files Browse the repository at this point in the history
  • Loading branch information
cketti committed Dec 2, 2023
1 parent 4e344b1 commit 462736e
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ internal class RealImapFolder(
uids,
)

UidCopyResponse.parse(imapResponses)?.uidMapping
UidCopyResponse.parse(imapResponses, allowUntaggedResponse = true)?.uidMapping
} catch (ioe: IOException) {
throw ioExceptionHandler(connection, ioe)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package com.fsck.k9.mail.store.imap
internal class UidCopyResponse private constructor(val uidMapping: Map<String, String>) {

companion object {
fun parse(imapResponses: List<ImapResponse>): UidCopyResponse? {
fun parse(imapResponses: List<ImapResponse>, allowUntaggedResponse: Boolean = false): UidCopyResponse? {
val uidMapping = mutableMapOf<String, String>()
for (imapResponse in imapResponses) {
parseUidCopyResponse(imapResponse, uidMapping)
parseUidCopyResponse(imapResponse, allowUntaggedResponse, uidMapping)
}

return if (uidMapping.isNotEmpty()) {
Expand All @@ -17,8 +17,12 @@ internal class UidCopyResponse private constructor(val uidMapping: Map<String, S
}

@Suppress("ReturnCount", "ComplexCondition", "MagicNumber")
private fun parseUidCopyResponse(response: ImapResponse, uidMappingOutput: MutableMap<String, String>) {
if (!response.isTagged || response.size < 2 ||
private fun parseUidCopyResponse(
response: ImapResponse,
allowUntaggedResponse: Boolean,
uidMappingOutput: MutableMap<String, String>,
) {
if (!(allowUntaggedResponse || response.isTagged) || response.size < 2 ||
!ImapResponseParser.equalsIgnoreCase(response[0], Responses.OK) || !response.isList(1)
) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.fsck.k9.mail.Part
import com.fsck.k9.mail.internet.BinaryTempFileBody
import com.fsck.k9.mail.internet.MimeHeader
import com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponse
import com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponseList
import java.io.File
import java.io.IOException
import java.nio.file.Files
Expand Down Expand Up @@ -300,15 +301,37 @@ class RealImapFolderTest {
}

@Test
fun `moveMessages() with MOVE extension`() {
fun `moveMessages() with MOVE extension and tagged COPYUID response`() {
val sourceFolder = createFolder("Folder")
prepareImapFolderForOpen(OpenMode.READ_WRITE)
imapConnection.stub {
on { hasCapability(Capabilities.MOVE) } doReturn true
}
val destinationFolder = createFolder("Destination")
val messages = listOf(createImapMessage("1"))
setupMoveResponse("x OK [COPYUID 23 1 101] Success")
setupMoveResponses("x OK [COPYUID 23 1 101] Success")
sourceFolder.open(OpenMode.READ_WRITE)

val uidMapping = sourceFolder.moveMessages(messages, destinationFolder)

assertCommandWithIdsIssued("UID MOVE 1 \"Destination\"")
assertThat(uidMapping).isNotNull().containsOnly("1" to "101")
}

@Test
fun `moveMessages() with MOVE extension and untagged COPYUID response`() {
val sourceFolder = createFolder("Folder")
prepareImapFolderForOpen(OpenMode.READ_WRITE)
imapConnection.stub {
on { hasCapability(Capabilities.MOVE) } doReturn true
}
val destinationFolder = createFolder("Destination")
val messages = listOf(createImapMessage("1"))
setupMoveResponses(
"* OK [COPYUID 23 1 101]",
"* 1 EXPUNGE",
"x OK MOVE completed",
)
sourceFolder.open(OpenMode.READ_WRITE)

val uidMapping = sourceFolder.moveMessages(messages, destinationFolder)
Expand Down Expand Up @@ -1223,8 +1246,8 @@ class RealImapFolderTest {
}

@Suppress("SameParameterValue")
private fun setupMoveResponse(response: String) {
val imapResponses = listOf(createImapResponse(response))
private fun setupMoveResponses(vararg responses: String) {
val imapResponses = createImapResponseList(*responses)
whenever(imapConnection.executeCommandWithIdSet(eq(Commands.UID_MOVE), anyString(), anySet()))
.thenReturn(imapResponses)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,28 @@ class UidCopyResponseTest {
}

@Test
fun `parse() with untagged response should return null`() {
fun `parse() with allowed untagged COPYUID response should return UID mapping`() {
val imapResponses = createImapResponseList(
"* OK [COPYUID 1 1,3 10:11]",
"* 1 EXPUNGE",
"* 1 EXPUNGE",
"x OK MOVE completed",
)

val result = UidCopyResponse.parse(imapResponses, allowUntaggedResponse = true)

assertThat(result).isNotNull()
.transform { it.uidMapping }
.isEqualTo(
mapOf(
"1" to "10",
"3" to "11",
),
)
}

@Test
fun `parse() with untagged response when not allowed should return null`() {
val imapResponses = createImapResponseList("* OK [COPYUID 1 1,3:5 7:10] Success")

val result = UidCopyResponse.parse(imapResponses)
Expand Down

0 comments on commit 462736e

Please sign in to comment.