diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt
index 580a5acaa..600fe234f 100644
--- a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt
+++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt
@@ -1,120 +1,83 @@
-package com.fsck.k9.mail.internet;
-
+package com.fsck.k9.mail.internet
/**
- * Adapted from the Apache James project, see
- * https://james.apache.org/mailet/base/apidocs/org/apache/mailet/base/FlowedMessageUtils.html
- *
- *
Manages texts encoded as text/plain; format=flowed
.
- * As a reference see:
- *
- * Note
- *
- * - In order to decode, the input text must belong to a mail with headers similar to:
- * Content-Type: text/plain; charset="CHARSET"; [delsp="yes|no"; ]format="flowed"
- * (the quotes around CHARSET are not mandatory).
- * Furthermore the header Content-Transfer-Encoding MUST NOT BE Quoted-Printable
- * (see RFC3676 paragraph 4.2).(In fact this happens often for non 7bit messages).
- *
- *
+ * Decodes text encoded as `text/plain; format=flowed` (RFC 3676).
*/
-public final class FlowedMessageUtils {
- private static final char RFC2646_SPACE = ' ';
- private static final char RFC2646_QUOTE = '>';
- private static final String RFC2646_SIGNATURE = "-- ";
- private static final String RFC2646_CRLF = "\r\n";
+object FlowedMessageUtils {
+ private const val QUOTE = '>'
+ private const val SPACE = ' '
+ private const val CR = '\r'
+ private const val LF = '\n'
+ private const val SIGNATURE = "-- "
+ private const val CRLF = "\r\n"
- private FlowedMessageUtils() {
- // this class cannot be instantiated
- }
+ @JvmStatic
+ fun deflow(text: String, delSp: Boolean): String {
+ var lineStartIndex = 0
+ var lastLineQuoteDepth = 0
+ var lastLineFlowed = false
- /**
- * Decodes a text previously wrapped using "format=flowed".
- */
- public static String deflow(String text, boolean delSp) {
- String[] lines = text.split("\r\n|\n", -1);
- StringBuffer result = null;
- StringBuffer resultLine = new StringBuffer();
- int resultLineQuoteDepth = 0;
- boolean resultLineFlowed = false;
- // One more cycle, to close the last line
- for (int i = 0; i <= lines.length; i++) {
- String line = i < lines.length ? lines[i] : null;
- int actualQuoteDepth = 0;
+ return buildString {
+ while (lineStartIndex <= text.lastIndex) {
+ var quoteDepth = 0
+ while (lineStartIndex <= text.lastIndex && text[lineStartIndex] == QUOTE) {
+ quoteDepth++
+ lineStartIndex++
+ }
- if (line != null) {
- if (line.equals(RFC2646_SIGNATURE)) {
- // signature handling (the previous line is not flowed)
- resultLineFlowed = false;
- } else if (line.length() > 0 && line.charAt(0) == RFC2646_QUOTE) {
- // Quote
- actualQuoteDepth = 1;
- while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) {
- actualQuoteDepth++;
+ // Remove space stuffing
+ if (lineStartIndex <= text.lastIndex && text[lineStartIndex] == SPACE) {
+ lineStartIndex++
+ }
+
+ // We support both LF and CRLF line endings. To cover both cases we search for LF.
+ val lineFeedIndex = text.indexOf(LF, lineStartIndex)
+ val lineBreakFound = lineFeedIndex != -1
+
+ var lineEndIndex = if (lineBreakFound) lineFeedIndex else text.length
+ if (lineEndIndex > 0 && text[lineEndIndex - 1] == CR) {
+ lineEndIndex--
+ }
+
+ if (lastLineFlowed && quoteDepth != lastLineQuoteDepth) {
+ append(CRLF)
+ lastLineFlowed = false
+ }
+
+ val lineIsSignatureMarker = lineEndIndex - lineStartIndex == SIGNATURE.length &&
+ text.regionMatches(lineStartIndex, SIGNATURE, 0, SIGNATURE.length)
+
+ var lineFlowed = false
+ if (lineIsSignatureMarker) {
+ if (lastLineFlowed) {
+ append(CRLF)
+ lastLineFlowed = false
}
- // if quote-depth changes wrt the previous line then this is not flowed
- if (resultLineQuoteDepth != actualQuoteDepth) {
- resultLineFlowed = false;
- }
- line = line.substring(actualQuoteDepth);
-
- } else {
- // if quote-depth changes wrt the first line then this is not flowed
- if (resultLineQuoteDepth > 0) {
- resultLineFlowed = false;
- }
- }
-
- if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE) {
- // Line space-stuffed
- line = line.substring(1);
- }
-
- // if the previous was the last then it was not flowed
- } else {
- resultLineFlowed = false;
- }
-
- // Add the PREVIOUS line.
- // This often will find the flow looking for a space as the last char of the line.
- // With quote changes or signatures it could be the following line to void the flow.
- if (!resultLineFlowed && i > 0) {
- if (resultLineQuoteDepth > 0) {
- resultLine.insert(0, RFC2646_SPACE);
- }
- for (int j = 0; j < resultLineQuoteDepth; j++) {
- resultLine.insert(0, RFC2646_QUOTE);
- }
- if (result == null) {
- result = new StringBuffer();
- } else {
- result.append(RFC2646_CRLF);
- }
- result.append(resultLine.toString());
- resultLine = new StringBuffer();
- resultLineFlowed = false;
- }
- resultLineQuoteDepth = actualQuoteDepth;
-
- if (line != null) {
- if (!line.equals(RFC2646_SIGNATURE) && line.endsWith("" + RFC2646_SPACE) && i < lines.length - 1) {
- // Line flowed (NOTE: for the split operation the line having i == lines.length is the last that
- // does not end with RFC2646_CRLF)
+ } else if (lineEndIndex > lineStartIndex && text[lineEndIndex - 1] == SPACE) {
+ lineFlowed = true
if (delSp) {
- line = line.substring(0, line.length() - 1);
+ lineEndIndex--
}
- resultLineFlowed = true;
- } else {
- resultLineFlowed = false;
}
- resultLine.append(line);
+ if (!lastLineFlowed && quoteDepth > 0) {
+ // This is not a continuation line, so prefix the text with quote characters.
+ repeat(quoteDepth) {
+ append(QUOTE)
+ }
+ append(SPACE)
+ }
+
+ append(text, lineStartIndex, lineEndIndex)
+
+ if (!lineFlowed && lineBreakFound) {
+ append(CRLF)
+ }
+
+ lineStartIndex = if (lineBreakFound) lineFeedIndex + 1 else text.length
+ lastLineQuoteDepth = quoteDepth
+ lastLineFlowed = lineFlowed
}
}
-
- return result.toString();
}
}
diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt
index cf70f7145..f9ef77ede 100644
--- a/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt
+++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt
@@ -4,16 +4,13 @@ import com.fsck.k9.mail.crlf
import com.google.common.truth.Truth.assertThat
import org.junit.Test
-private const val DEL_SP_NO = false
-private const val DEL_SP_YES = true
-
class FlowedMessageUtilsTest {
@Test
fun `deflow() with simple text`() {
val input = "Text that should be \r\n" +
"displayed on one line"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("Text that should be displayed on one line")
}
@@ -27,7 +24,7 @@ class FlowedMessageUtilsTest {
"Text that should retain\r\n" +
"its line break."
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@@ -42,7 +39,7 @@ class FlowedMessageUtilsTest {
fun `deflow() with nothing to do`() {
val input = "Line one\r\nLine two\r\n"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(input)
}
@@ -56,7 +53,7 @@ class FlowedMessageUtilsTest {
"Some more text that should be \r\n" +
"displayed on one line.\r\n"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@@ -74,7 +71,7 @@ class FlowedMessageUtilsTest {
val input = "> Quoted text \r\n" +
"Some other text"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("> Quoted text \r\nSome other text")
}
@@ -86,7 +83,7 @@ class FlowedMessageUtilsTest {
"> is here\r\n" +
"Some other text"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@@ -103,7 +100,7 @@ class FlowedMessageUtilsTest {
"\r\n" +
"Text"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(input)
}
@@ -112,7 +109,7 @@ class FlowedMessageUtilsTest {
fun `deflow() with delSp=true`() {
val input = "Text that is wrapped mid wo \r\nrd"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_YES)
+ val result = FlowedMessageUtils.deflow(input, delSp = true)
assertThat(result).isEqualTo("Text that is wrapped mid word")
}
@@ -122,7 +119,7 @@ class FlowedMessageUtilsTest {
val input = "> Quoted te \r\n" +
"> xt"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_YES)
+ val result = FlowedMessageUtils.deflow(input, delSp = true)
assertThat(result).isEqualTo("> Quoted text")
}
@@ -132,7 +129,7 @@ class FlowedMessageUtilsTest {
val input = "Text that should be \r\n" +
" displayed on one line"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("Text that should be displayed on one line")
}
@@ -143,7 +140,7 @@ class FlowedMessageUtilsTest {
" Line 2\r\n" +
" Line 3\r\n"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("Line 1\r\nLine 2\r\nLine 3\r\n")
}
@@ -153,7 +150,7 @@ class FlowedMessageUtilsTest {
val input = "> Text that should be \r\n" +
"> displayed on one line"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("> Text that should be displayed on one line")
}
@@ -167,7 +164,7 @@ class FlowedMessageUtilsTest {
"Signature \r\n" +
"text"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@@ -188,7 +185,7 @@ class FlowedMessageUtilsTest {
"> Signature \r\n" +
"> text"
- val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+ val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@@ -199,4 +196,21 @@ class FlowedMessageUtilsTest {
""".trimIndent().crlf()
)
}
+
+ @Test
+ fun `deflow() with flowed line followed by signature separator`() {
+ val input = "Fake flowed line \r\n" +
+ "-- \r\n" +
+ "Signature"
+
+ val result = FlowedMessageUtils.deflow(input, delSp = true)
+
+ assertThat(result).isEqualTo(
+ """
+ Fake flowed line
+ --${" "}
+ Signature
+ """.trimIndent().crlf()
+ )
+ }
}