Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add header folding to MimeHeaderEncoder #7687

Merged
merged 1 commit into from Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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 {
Expand Down
@@ -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<String>.isValidHeader(name: String) = given { value ->
try {
MimeHeaderChecker.checkHeader(name, value)
} catch (e: MimeHeaderParserException) {
fail(AssertionError("Not a valid RFC5322 header", e))
}
}

private fun Assert<String>.decodesTo(expected: String) = given { value ->
assertThat(MimeUtility.unfoldAndDecode(value)).isEqualTo(expected)
}
}