Loading mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt +62 −99 Original line number Diff line number Diff line 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 * * <p>Manages texts encoded as <code>text/plain; format=flowed</code>.</p> * <p>As a reference see:</p> * <ul> * <li><a href='http://www.rfc-editor.org/rfc/rfc2646.txt'>RFC2646</a></li> * <li><a href='http://www.rfc-editor.org/rfc/rfc3676.txt'>RFC3676</a> (new method with DelSP support). * </ul> * <h4>Note</h4> * <ul> * <li>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). * </li> * </ul> * 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"; private FlowedMessageUtils() { // this class cannot be instantiated } 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" /** * 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; @JvmStatic fun deflow(text: String, delSp: Boolean): String { var lineStartIndex = 0 var lastLineQuoteDepth = 0 var lastLineFlowed = false 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++; } // if quote-depth changes wrt the previous line then this is not flowed if (resultLineQuoteDepth != actualQuoteDepth) { resultLineFlowed = false; return buildString { while (lineStartIndex <= text.lastIndex) { var quoteDepth = 0 while (lineStartIndex <= text.lastIndex && text[lineStartIndex] == QUOTE) { quoteDepth++ lineStartIndex++ } line = line.substring(actualQuoteDepth); } else { // if quote-depth changes wrt the first line then this is not flowed if (resultLineQuoteDepth > 0) { resultLineFlowed = false; } // Remove space stuffing if (lineStartIndex <= text.lastIndex && text[lineStartIndex] == SPACE) { lineStartIndex++ } if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE) { // Line space-stuffed line = line.substring(1); // 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 the previous was the last then it was not flowed } else { resultLineFlowed = false; if (lastLineFlowed && quoteDepth != lastLineQuoteDepth) { append(CRLF) lastLineFlowed = 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); val lineIsSignatureMarker = lineEndIndex - lineStartIndex == SIGNATURE.length && text.regionMatches(lineStartIndex, SIGNATURE, 0, SIGNATURE.length) var lineFlowed = false if (lineIsSignatureMarker) { if (lastLineFlowed) { append(CRLF) lastLineFlowed = false } } else if (lineEndIndex > lineStartIndex && text[lineEndIndex - 1] == SPACE) { lineFlowed = true if (delSp) { lineEndIndex-- } for (int j = 0; j < resultLineQuoteDepth; j++) { resultLine.insert(0, RFC2646_QUOTE); } if (result == null) { result = new StringBuffer(); } else { result.append(RFC2646_CRLF); if (!lastLineFlowed && quoteDepth > 0) { // This is not a continuation line, so prefix the text with quote characters. repeat(quoteDepth) { append(QUOTE) } result.append(resultLine.toString()); resultLine = new StringBuffer(); resultLineFlowed = false; append(SPACE) } 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) if (delSp) { line = line.substring(0, line.length() - 1); } resultLineFlowed = true; } else { resultLineFlowed = false; append(text, lineStartIndex, lineEndIndex) if (!lineFlowed && lineBreakFound) { append(CRLF) } resultLine.append(line); lineStartIndex = if (lineBreakFound) lineFeedIndex + 1 else text.length lastLineQuoteDepth = quoteDepth lastLineFlowed = lineFlowed } } return result.toString(); } } mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt +31 −17 Original line number Diff line number Diff line Loading @@ -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") } Loading @@ -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( """ Loading @@ -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) } Loading @@ -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( """ Loading @@ -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") } Loading @@ -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( """ Loading @@ -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) } Loading @@ -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") } Loading @@ -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") } Loading @@ -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") } Loading @@ -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") } Loading @@ -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") } Loading @@ -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( """ Loading @@ -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( """ Loading @@ -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() ) } } Loading
mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt +62 −99 Original line number Diff line number Diff line 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 * * <p>Manages texts encoded as <code>text/plain; format=flowed</code>.</p> * <p>As a reference see:</p> * <ul> * <li><a href='http://www.rfc-editor.org/rfc/rfc2646.txt'>RFC2646</a></li> * <li><a href='http://www.rfc-editor.org/rfc/rfc3676.txt'>RFC3676</a> (new method with DelSP support). * </ul> * <h4>Note</h4> * <ul> * <li>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). * </li> * </ul> * 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"; private FlowedMessageUtils() { // this class cannot be instantiated } 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" /** * 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; @JvmStatic fun deflow(text: String, delSp: Boolean): String { var lineStartIndex = 0 var lastLineQuoteDepth = 0 var lastLineFlowed = false 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++; } // if quote-depth changes wrt the previous line then this is not flowed if (resultLineQuoteDepth != actualQuoteDepth) { resultLineFlowed = false; return buildString { while (lineStartIndex <= text.lastIndex) { var quoteDepth = 0 while (lineStartIndex <= text.lastIndex && text[lineStartIndex] == QUOTE) { quoteDepth++ lineStartIndex++ } line = line.substring(actualQuoteDepth); } else { // if quote-depth changes wrt the first line then this is not flowed if (resultLineQuoteDepth > 0) { resultLineFlowed = false; } // Remove space stuffing if (lineStartIndex <= text.lastIndex && text[lineStartIndex] == SPACE) { lineStartIndex++ } if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE) { // Line space-stuffed line = line.substring(1); // 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 the previous was the last then it was not flowed } else { resultLineFlowed = false; if (lastLineFlowed && quoteDepth != lastLineQuoteDepth) { append(CRLF) lastLineFlowed = 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); val lineIsSignatureMarker = lineEndIndex - lineStartIndex == SIGNATURE.length && text.regionMatches(lineStartIndex, SIGNATURE, 0, SIGNATURE.length) var lineFlowed = false if (lineIsSignatureMarker) { if (lastLineFlowed) { append(CRLF) lastLineFlowed = false } } else if (lineEndIndex > lineStartIndex && text[lineEndIndex - 1] == SPACE) { lineFlowed = true if (delSp) { lineEndIndex-- } for (int j = 0; j < resultLineQuoteDepth; j++) { resultLine.insert(0, RFC2646_QUOTE); } if (result == null) { result = new StringBuffer(); } else { result.append(RFC2646_CRLF); if (!lastLineFlowed && quoteDepth > 0) { // This is not a continuation line, so prefix the text with quote characters. repeat(quoteDepth) { append(QUOTE) } result.append(resultLine.toString()); resultLine = new StringBuffer(); resultLineFlowed = false; append(SPACE) } 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) if (delSp) { line = line.substring(0, line.length() - 1); } resultLineFlowed = true; } else { resultLineFlowed = false; append(text, lineStartIndex, lineEndIndex) if (!lineFlowed && lineBreakFound) { append(CRLF) } resultLine.append(line); lineStartIndex = if (lineBreakFound) lineFeedIndex + 1 else text.length lastLineQuoteDepth = quoteDepth lastLineFlowed = lineFlowed } } return result.toString(); } }
mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt +31 −17 Original line number Diff line number Diff line Loading @@ -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") } Loading @@ -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( """ Loading @@ -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) } Loading @@ -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( """ Loading @@ -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") } Loading @@ -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( """ Loading @@ -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) } Loading @@ -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") } Loading @@ -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") } Loading @@ -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") } Loading @@ -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") } Loading @@ -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") } Loading @@ -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( """ Loading @@ -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( """ Loading @@ -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() ) } }