IMAP: Fix creating header values from a BODYSTRUCTURE item
This commit is contained in:
parent
b7435fa2ef
commit
d82b462565
3 changed files with 101 additions and 5 deletions
|
@ -138,14 +138,14 @@ object MimeParameterEncoder {
|
||||||
return length
|
return length
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.isToken() = when {
|
fun String.isToken() = when {
|
||||||
isEmpty() -> false
|
isEmpty() -> false
|
||||||
else -> all { it.isTokenChar() }
|
else -> all { it.isTokenChar() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.isQuotable() = all { it.isQuotable() }
|
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]
|
// quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
|
||||||
// qcontent = qtext / quoted-pair
|
// qcontent = qtext / quoted-pair
|
||||||
// quoted-pair = ("\" (VCHAR / WSP))
|
// quoted-pair = ("\" (VCHAR / WSP))
|
||||||
|
|
|
@ -15,6 +15,8 @@ import com.fsck.k9.mail.internet.MimeBodyPart
|
||||||
import com.fsck.k9.mail.internet.MimeHeader
|
import com.fsck.k9.mail.internet.MimeHeader
|
||||||
import com.fsck.k9.mail.internet.MimeMessageHelper
|
import com.fsck.k9.mail.internet.MimeMessageHelper
|
||||||
import com.fsck.k9.mail.internet.MimeMultipart
|
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 com.fsck.k9.mail.internet.MimeUtility
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -889,7 +891,8 @@ internal class RealImapFolder(
|
||||||
for (i in bodyParams.indices step 2) {
|
for (i in bodyParams.indices step 2) {
|
||||||
val paramName = bodyParams.getString(i)
|
val paramName = bodyParams.getString(i)
|
||||||
val paramValue = bodyParams.getString(i + 1)
|
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) {
|
for (i in bodyDispositionParams.indices step 2) {
|
||||||
val paramName = bodyDispositionParams.getString(i).lowercase()
|
val paramName = bodyDispositionParams.getString(i).lowercase()
|
||||||
val paramValue = bodyDispositionParams.getString(i + 1)
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -731,7 +731,83 @@ class RealImapFolderTest {
|
||||||
|
|
||||||
folder.fetch(messages, fetchProfile, null, MAX_DOWNLOAD_SIZE)
|
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
|
@Test
|
||||||
|
@ -1118,6 +1194,22 @@ class RealImapFolderTest {
|
||||||
.thenReturn(imapResponses)
|
.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 {
|
companion object {
|
||||||
private const val MAX_DOWNLOAD_SIZE = -1
|
private const val MAX_DOWNLOAD_SIZE = -1
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue