IMAP: Fix creating header values from a BODYSTRUCTURE item

This commit is contained in:
cketti 2022-08-02 19:36:30 +02:00
parent b7435fa2ef
commit d82b462565
3 changed files with 101 additions and 5 deletions

View file

@ -138,14 +138,14 @@ object MimeParameterEncoder {
return length
}
private fun String.isToken() = when {
fun String.isToken() = when {
isEmpty() -> false
else -> all { it.isTokenChar() }
}
private fun String.isQuotable() = all { it.isQuotable() }
private fun String.quoted(): String {
fun String.quoted(): String {
// quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
// qcontent = qtext / quoted-pair
// quoted-pair = ("\" (VCHAR / WSP))

View file

@ -15,6 +15,8 @@ import com.fsck.k9.mail.internet.MimeBodyPart
import com.fsck.k9.mail.internet.MimeHeader
import com.fsck.k9.mail.internet.MimeMessageHelper
import com.fsck.k9.mail.internet.MimeMultipart
import com.fsck.k9.mail.internet.MimeParameterEncoder.isToken
import com.fsck.k9.mail.internet.MimeParameterEncoder.quoted
import com.fsck.k9.mail.internet.MimeUtility
import java.io.IOException
import java.io.InputStream
@ -889,7 +891,8 @@ internal class RealImapFolder(
for (i in bodyParams.indices step 2) {
val paramName = bodyParams.getString(i)
val paramValue = bodyParams.getString(i + 1)
contentType.append(String.format(";\r\n %s=\"%s\"", paramName, paramValue))
val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quoted()
contentType.append(String.format(";\r\n %s=%s", paramName, encodedValue))
}
}
@ -915,7 +918,8 @@ internal class RealImapFolder(
for (i in bodyDispositionParams.indices step 2) {
val paramName = bodyDispositionParams.getString(i).lowercase()
val paramValue = bodyDispositionParams.getString(i + 1)
contentDisposition.append(String.format(";\r\n %s=\"%s\"", paramName, paramValue))
val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quoted()
contentDisposition.append(String.format(";\r\n %s=%s", paramName, encodedValue))
}
}
}

View file

@ -731,7 +731,83 @@ class RealImapFolderTest {
folder.fetch(messages, fetchProfile, null, MAX_DOWNLOAD_SIZE)
verify(messages[0]).setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain;\r\n CHARSET=\"US-ASCII\"")
verify(messages[0]).setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain;\r\n CHARSET=US-ASCII")
}
@Test
fun `fetch() with simple content type parameter`() {
testHeaderFromBodyStructure(
bodyStructure = """("text" "plain" ("name" "token") NIL NIL "7bit" 42 23)""",
headerName = MimeHeader.HEADER_CONTENT_TYPE,
expectedHeaderValue = "text/plain;\r\n name=token"
)
}
@Test
fun `fetch() with content type parameter that needs to be a quoted string`() {
testHeaderFromBodyStructure(
bodyStructure = """("text" "plain" ("name" "one two three") NIL NIL "7bit" 42 23)""",
headerName = MimeHeader.HEADER_CONTENT_TYPE,
expectedHeaderValue = "text/plain;\r\n name=\"one two three\""
)
}
@Test
fun `fetch() with content type parameter that needs to be a quoted string with escaped characters`() {
testHeaderFromBodyStructure(
bodyStructure = """("text" "plain" ("name" "one \"two\" three") NIL NIL "7bit" 42 23)""",
headerName = MimeHeader.HEADER_CONTENT_TYPE,
expectedHeaderValue = "text/plain;\r\n name=\"one \\\"two\\\" three\""
)
}
@Test
fun `fetch() with RFC 2231 encoded content type parameter`() {
testHeaderFromBodyStructure(
bodyStructure = """("text" "plain" ("name*" "utf-8''filen%C3%A4me.ext") NIL NIL "7bit" 42 23)""",
headerName = MimeHeader.HEADER_CONTENT_TYPE,
expectedHeaderValue = "text/plain;\r\n name*=utf-8''filen%C3%A4me.ext"
)
}
@Test
fun `fetch() with simple content disposition parameter`() {
testHeaderFromBodyStructure(
bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ +
"""("attachment" ("filename" "token")) NIL NIL)""",
headerName = MimeHeader.HEADER_CONTENT_DISPOSITION,
expectedHeaderValue = "attachment;\r\n filename=token;\r\n size=23"
)
}
@Test
fun `fetch() with content disposition parameter that needs to be a quoted string`() {
testHeaderFromBodyStructure(
bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ +
"""("attachment" ("filename" "one two three")) NIL NIL)""",
headerName = MimeHeader.HEADER_CONTENT_DISPOSITION,
expectedHeaderValue = "attachment;\r\n filename=\"one two three\";\r\n size=23"
)
}
@Test
fun `fetch() with content disposition parameter that needs to be a quoted string with escaped characters`() {
testHeaderFromBodyStructure(
bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ +
"""("attachment" ("filename" "one \"two\" three")) NIL NIL)""",
headerName = MimeHeader.HEADER_CONTENT_DISPOSITION,
expectedHeaderValue = "attachment;\r\n filename=\"one \\\"two\\\" three\";\r\n size=23"
)
}
@Test
fun `fetch() with RFC 2231 encoded content disposition parameter`() {
testHeaderFromBodyStructure(
bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ +
"""("attachment" ("filename*" "utf-8''filen%C3%A4me.ext")) NIL NIL)""",
headerName = MimeHeader.HEADER_CONTENT_DISPOSITION,
expectedHeaderValue = "attachment;\r\n filename*=utf-8''filen%C3%A4me.ext;\r\n size=23"
)
}
@Test
@ -1118,6 +1194,22 @@ class RealImapFolderTest {
.thenReturn(imapResponses)
}
private fun testHeaderFromBodyStructure(bodyStructure: String, headerName: String, expectedHeaderValue: String) {
val folder = createFolder("Folder")
prepareImapFolderForOpen(OpenMode.READ_ONLY)
folder.open(OpenMode.READ_ONLY)
whenever(imapConnection.readResponse(anyOrNull()))
.thenReturn(createImapResponse("* 1 FETCH (BODYSTRUCTURE $bodyStructure UID 1)"))
.thenReturn(createImapResponse("x OK"))
val imapMessage = ImapMessage("1")
val messages = listOf(imapMessage)
val fetchProfile = createFetchProfile(FetchProfile.Item.STRUCTURE)
folder.fetch(messages, fetchProfile, null, MAX_DOWNLOAD_SIZE)
assertThat(imapMessage.getHeader(headerName)).asList().containsExactly(expectedHeaderValue)
}
companion object {
private const val MAX_DOWNLOAD_SIZE = -1
}