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

Unverified Commit d9bf7b49 authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #5603 from k9mail/format_flowed_reply

Properly decode format=flowed body before including the text in a reply
parents b73659c1 c2ae85e2
Loading
Loading
Loading
Loading
+0 −9
Original line number Diff line number Diff line
@@ -20,9 +20,7 @@ import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.Viewable;
import com.fsck.k9.mail.internet.Viewable.Flowed;
import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError;
import com.fsck.k9.mailstore.util.FlowedMessageUtils;
import com.fsck.k9.message.extractors.AttachmentInfoExtractor;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.message.html.HtmlProcessor;
@@ -271,10 +269,6 @@ public class MessageViewInfoExtractor {
            String t = getTextFromPart(part);
            if (t == null) {
                t = "";
            } else if (viewable instanceof Flowed) {
                boolean delSp = ((Flowed) viewable).isDelSp();
                t = FlowedMessageUtils.deflow(t, delSp);
                t = HtmlConverter.textToHtml(t);
            } else if (viewable instanceof Text) {
                t = HtmlConverter.textToHtml(t);
            } else if (!(viewable instanceof Html)) {
@@ -310,9 +304,6 @@ public class MessageViewInfoExtractor {
                t = "";
            } else if (viewable instanceof Html) {
                t = HtmlConverter.htmlToText(t);
            } else if (viewable instanceof Flowed) {
                boolean delSp = ((Flowed) viewable).isDelSp();
                t = FlowedMessageUtils.deflow(t, delSp);
            } else if (!(viewable instanceof Text)) {
                throw new IllegalStateException("unhandled case!");
            }
+0 −102
Original line number Diff line number Diff line
package com.fsck.k9.mailstore.util;


/**
 * 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>
 */
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
    }

    /**
     * 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;

            if (line != null && line.length() > 0) {
                if (line.equals(RFC2646_SIGNATURE))
                    // signature handling (the previous line is not flowed)
                    resultLineFlowed = false;

                else if (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;
                    line = line.substring(actualQuoteDepth);

                } else {
                    // id 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 if (line == null) 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 followinf 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)
                    if (delSp) line = line.substring(0, line.length() - 1);
                    resultLineFlowed = true;
                }

                else resultLineFlowed = false;

                resultLine.append(line);
            }
        }

        return result.toString();
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ package com.fsck.k9.mailstore;


import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -17,6 +18,7 @@ import com.fsck.k9.DI;
import com.fsck.k9.K9RobolectricTest;
import com.fsck.k9.TestCoreResourceProvider;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
@@ -135,7 +137,7 @@ public class MessageViewInfoExtractorTest extends K9RobolectricTest {
    @Test
    public void testTextPlainFormatFlowed() throws MessagingException {
        // Create text/plain body
        TextBody body = new TextBody(BODY_TEXT_FLOWED);
        Body body = new BinaryMemoryBody(BODY_TEXT_FLOWED.getBytes(StandardCharsets.UTF_8), "utf-8");

        // Create message
        MimeMessage message = new MimeMessage();
+91 −21
Original line number Diff line number Diff line
package com.fsck.k9.mail.internet;


import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter;
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
/**
 * 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>
 */
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
    }

    /**
     * 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;

            if (line != null && line.length() > 0) {
                if (line.equals(RFC2646_SIGNATURE))
                    // signature handling (the previous line is not flowed)
                    resultLineFlowed = false;

                else if (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;
                    line = line.substring(actualQuoteDepth);

                } else {
                    // id quote-depth changes wrt the first line then this is not flowed
                    if (resultLineQuoteDepth > 0) resultLineFlowed = false;
                }

class FlowedMessageUtils {
    private static final String TEXT_PLAIN = "text/plain";
    private static final String HEADER_PARAM_FORMAT = "format";
    private static final String HEADER_FORMAT_FLOWED = "flowed";
    private static final String HEADER_PARAM_DELSP = "delsp";
    private static final String HEADER_DELSP_YES = "yes";
                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 if (line == null) resultLineFlowed = false;

    static boolean isFormatFlowed(String contentType) {
        String mimeType = getHeaderParameter(contentType, null);
        if (isSameMimeType(TEXT_PLAIN, mimeType)) {
            String formatParameter = getHeaderParameter(contentType, HEADER_PARAM_FORMAT);
            return HEADER_FORMAT_FLOWED.equalsIgnoreCase(formatParameter);
            // 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 followinf 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;
            }
        return 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)
                    if (delSp) line = line.substring(0, line.length() - 1);
                    resultLineFlowed = true;
                }

    static boolean isDelSp(String contentType) {
        if (isFormatFlowed(contentType)) {
            String delSpParameter = getHeaderParameter(contentType, HEADER_PARAM_DELSP);
            return HEADER_DELSP_YES.equalsIgnoreCase(delSpParameter);
                else resultLineFlowed = false;

                resultLine.append(line);
            }
        return false;
        }

        return result.toString();
    }
}
+30 −0
Original line number Diff line number Diff line
package com.fsck.k9.mail.internet

import java.util.Locale

internal object FormatFlowedHelper {
    private const val TEXT_PLAIN = "text/plain"
    private const val HEADER_PARAM_FORMAT = "format"
    private const val HEADER_FORMAT_FLOWED = "flowed"
    private const val HEADER_PARAM_DELSP = "delsp"
    private const val HEADER_DELSP_YES = "yes"

    @JvmStatic
    fun checkFormatFlowed(contentTypeHeaderValue: String?): FormatFlowedResult {
        if (contentTypeHeaderValue == null) return negativeResult()

        val mimeValue = MimeParameterDecoder.decode(contentTypeHeaderValue)
        if (!MimeUtility.isSameMimeType(TEXT_PLAIN, mimeValue.value)) return negativeResult()

        val formatParameter = mimeValue.parameters[HEADER_PARAM_FORMAT]?.toLowerCase(Locale.ROOT)
        if (formatParameter != HEADER_FORMAT_FLOWED) return negativeResult()

        val delSpParameter = mimeValue.parameters[HEADER_PARAM_DELSP]?.toLowerCase(Locale.ROOT)

        return FormatFlowedResult(isFormatFlowed = true, isDelSp = delSpParameter == HEADER_DELSP_YES)
    }

    private fun negativeResult() = FormatFlowedResult(isFormatFlowed = false, isDelSp = false)
}

internal data class FormatFlowedResult(val isFormatFlowed: Boolean, val isDelSp: Boolean)
Loading