From 09a155bcfdd06ca2a2700280ed3d5ba9e4c5ae4e Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 4 Mar 2024 13:05:29 +0100 Subject: [PATCH] Add header folding to `MimeHeaderEncoder` --- .../k9/mail/internet/MimeHeaderEncoder.kt | 18 +++-- .../k9/mail/internet/MimeHeaderEncoderTest.kt | 75 +++++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 mail/common/src/test/java/com/fsck/k9/mail/internet/MimeHeaderEncoderTest.kt diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeHeaderEncoder.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeHeaderEncoder.kt index 9654381e404..a05c85e907e 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeHeaderEncoder.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeHeaderEncoder.kt @@ -1,23 +1,29 @@ package com.fsck.k9.mail.internet +import org.apache.james.mime4j.util.MimeUtil + object MimeHeaderEncoder { @JvmStatic fun encode(name: String, value: String): String { // TODO: Fold long text that provides enough opportunities for folding and doesn't contain any characters that // need to be encoded. - return if (hasToBeEncoded(name, value)) { - EncoderUtil.encodeEncodedWord(value) + + // Number of characters already used up on the first line (header field name + colon + space) + val usedCharacters = name.length + COLON_PLUS_SPACE_LENGTH + + return if (hasToBeEncoded(value, usedCharacters)) { + MimeUtil.fold(EncoderUtil.encodeEncodedWord(value), usedCharacters) } else { value } } - private fun hasToBeEncoded(name: String, value: String): Boolean { - return exceedsRecommendedLineLength(name, value) || charactersNeedEncoding(value) + private fun hasToBeEncoded(value: String, usedCharacters: Int): Boolean { + return exceedsRecommendedLineLength(value, usedCharacters) || charactersNeedEncoding(value) } - private fun exceedsRecommendedLineLength(name: String, value: String): Boolean { - return name.length + COLON_PLUS_SPACE_LENGTH + value.length > RECOMMENDED_MAX_LINE_LENGTH + private fun exceedsRecommendedLineLength(value: String, usedCharacters: Int): Boolean { + return usedCharacters + value.length > RECOMMENDED_MAX_LINE_LENGTH } private fun charactersNeedEncoding(text: String): Boolean { diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeHeaderEncoderTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeHeaderEncoderTest.kt new file mode 100644 index 00000000000..5f807c57132 --- /dev/null +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeHeaderEncoderTest.kt @@ -0,0 +1,75 @@ +package com.fsck.k9.mail.internet + +import assertk.Assert +import assertk.all +import assertk.assertThat +import assertk.assertions.each +import assertk.assertions.isEqualTo +import assertk.assertions.isLessThanOrEqualTo +import assertk.assertions.length +import assertk.fail +import kotlin.test.Test + +class MimeHeaderEncoderTest { + @Test + fun `short subject containing only ASCII characters should not be encoded`() { + val result = MimeHeaderEncoder.encode(name = "Subject", value = "Hello World!") + + assertThat(result).isEqualTo("Hello World!") + } + + @Test + fun `short subject containing non-ASCII characters should be encoded`() { + val result = MimeHeaderEncoder.encode(name = "Subject", value = "Gänseblümchen") + + assertThat(result).isEqualTo("=?UTF-8?Q?G=C3=A4nsebl=C3=BCmchen?=") + } + + @Test + fun `subject with recommended line length should not be folded`() { + val subject = "a".repeat(RECOMMENDED_MAX_LINE_LENGTH - "Subject: ".length) + + val result = MimeHeaderEncoder.encode(name = "Subject", value = subject) + + assertThat(result).isEqualTo(subject) + } + + @Test + fun `subject exceeding recommended line length should be folded`() { + val subject = "a".repeat(34) + " " + "a".repeat(35) + + val result = MimeHeaderEncoder.encode(name = "Subject", value = subject) + + assertThat(result).all { + transform { it.lines() }.each { + it.length().isLessThanOrEqualTo(RECOMMENDED_MAX_LINE_LENGTH) + } + isValidHeader(name = "Subject") + decodesTo(subject) + } + } + + @Test + fun `subject exceeding maximum line length should be encoded`() { + val subject = "a".repeat(999) + + val result = MimeHeaderEncoder.encode(name = "Subject", value = subject) + + assertThat(result).all { + isValidHeader(name = "Subject") + decodesTo(subject) + } + } + + private fun Assert.isValidHeader(name: String) = given { value -> + try { + MimeHeaderChecker.checkHeader(name, value) + } catch (e: MimeHeaderParserException) { + fail(AssertionError("Not a valid RFC5322 header", e)) + } + } + + private fun Assert.decodesTo(expected: String) = given { value -> + assertThat(MimeUtility.unfoldAndDecode(value)).isEqualTo(expected) + } +}