Rewrite FlowedMessageUtils.deflow()
This new version should use a lot less allocations.
This commit is contained in:
parent
f7b6b8371f
commit
ef8d9abed3
2 changed files with 99 additions and 122 deletions
|
@ -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
|
||||
*
|
||||
* <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";
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue