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

- * + * 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() + ) + } }