Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ef8d9abe authored by cketti's avatar cketti
Browse files

Rewrite `FlowedMessageUtils.deflow()`

This new version should use a lot less allocations.
parent f7b6b837
Loading
Loading
Loading
Loading
+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();
    }
}
+31 −17
Original line number Diff line number Diff line
@@ -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()
        )
    }
}