Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Broken quoting of plain text mail
### Checklist

- [X] I have used the search function to see if someone else has already submitted the same bug report.
- [X] I will describe the problem with as much detail as possible.

### App version

6.000

### Where did you get the app from?

Google Play

### Android version

Android 12, One UI 4.1

### Device model

Samsung S21

### Steps to reproduce

Show mail. The mail is like this, it was a pure text mail:

```

Am 20.04.2022 um 04:16 schrieb XXXX YYYYYYYY:
>
> We also enjoy when people tell us that all their tests are green! It 
> gives us confidence ;-) So regardless of the tests color (red or 
> green), please do not hesitate to send me a short mail as both types 
> of feedback are useful. 

Apache PDFBox builds successfully with jdk19.

https://ci-builds.apache.org/job/PDFBox/job/PDFBox-Trunk-jdk19/

jacoco (tool for code coverage) had problems 
("java.lang.instrument.IllegalClassFormatException: Error while 
instrumenting sun/security/pkcs11/SunPKCS11") but it went away with 
their latest version

https://github.com/jacoco/jacoco/issues/1296

Best regards

Tilman Hausherr

```

(I did make an issue search for "quote", but the issues looked different)

### Expected behavior

more like this (from thunderbird)

![grafik](https://user-images.githubusercontent.com/6665575/164276881-5599500c-6d61-493c-82dd-1d5bb66aa782.png)


### Actual behavior

![grafik](https://user-images.githubusercontent.com/6665575/164277096-009c7251-10e3-4664-8350-ed59f235b2fb.png)


### Logs

_No response_


Discussion hints:
I can't reproduce the issue with the text snippet you've provided. Can you attach the (redacted) message source (including headers)?
Here's the (redacted) EML file
[k9.zip](https://github.com/k9mail/k-9/files/8523702/k9.zip)


Pull request: Fix bug in `FlowedMessageUtils.deflow()`
Fixes #6029

Test command: ./gradlew :mail:common:testDebugUnitTest --tests "com.fsck.k9.mail.internet.FlowedMessageUtilsTest"

Fix patch:
diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java
index 25a10847b4d..580a5acaa2e 100644
--- a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java
+++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java
@@ -45,39 +45,54 @@ public static String deflow(String text, boolean delSp) {
             String line = i < lines.length ? lines[i] : null;
             int actualQuoteDepth = 0;
 
-            if (line != null && line.length() > 0) {
-                if (line.equals(RFC2646_SIGNATURE))
+            if (line != null) {
+                if (line.equals(RFC2646_SIGNATURE)) {
                     // signature handling (the previous line is not flowed)
                     resultLineFlowed = false;
-
-                else if (line.charAt(0) == RFC2646_QUOTE) {
+                } else if (line.length() > 0 && line.charAt(0) == RFC2646_QUOTE) {
                     // Quote
                     actualQuoteDepth = 1;
-                    while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) actualQuoteDepth ++;
+                    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;
+                    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 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)
+                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;
+            } 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 followinf line to void the flow.
+            // 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);
+                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;
@@ -86,13 +101,16 @@ else if (line.charAt(0) == RFC2646_QUOTE) {
 
             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);
+                    // 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;
                 }
 
-                else resultLineFlowed = false;
-
                 resultLine.append(line);
             }
         }


Test patch:
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
new file mode 100644
index 00000000000..cf70f7145a4
--- /dev/null
+++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt
@@ -0,0 +1,202 @@
+package com.fsck.k9.mail.internet
+
+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)
+
+        assertThat(result).isEqualTo("Text that should be displayed on one line")
+    }
+
+    @Test
+    fun `deflow() with only some lines ending in space`() {
+        val input = "Text that \r\n" +
+            "should be \r\n" +
+            "displayed on \r\n" +
+            "one line.\r\n" +
+            "Text that should retain\r\n" +
+            "its line break."
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo(
+            """
+            Text that should be displayed on one line.
+            Text that should retain
+            its line break.
+            """.trimIndent().crlf()
+        )
+    }
+
+    @Test
+    fun `deflow() with nothing to do`() {
+        val input = "Line one\r\nLine two\r\n"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo(input)
+    }
+
+    @Test
+    fun `deflow() with quoted text`() {
+        val input = "On [date], [user] wrote:\r\n" +
+            "> Text that should be displayed \r\n" +
+            "> on one line\r\n" +
+            "\r\n" +
+            "Some more text that should be \r\n" +
+            "displayed on one line.\r\n"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo(
+            """
+            |On [date], [user] wrote:
+            |> Text that should be displayed on one line
+            |
+            |Some more text that should be displayed on one line.
+            |
+            """.trimMargin().crlf()
+        )
+    }
+
+    @Test
+    fun `deflow() with quoted text ending in space`() {
+        val input = "> Quoted text \r\n" +
+            "Some other text"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo("> Quoted text \r\nSome other text")
+    }
+
+    @Test
+    fun `deflow() with quoted text ending in space before quoted text of different quoting depth`() {
+        val input = ">> Depth 2 \r\n" +
+            "> Depth 1 \r\n" +
+            "> is here\r\n" +
+            "Some other text"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo(
+            """
+            >> Depth 2${" "}
+            > Depth 1 is here
+            Some other text
+            """.trimIndent().crlf()
+        )
+    }
+
+    @Test
+    fun `deflow() with quoted text ending in space followed by empty line`() {
+        val input = "> Quoted \r\n" +
+            "\r\n" +
+            "Text"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo(input)
+    }
+
+    @Test
+    fun `deflow() with delSp=true`() {
+        val input = "Text that is wrapped mid wo \r\nrd"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_YES)
+
+        assertThat(result).isEqualTo("Text that is wrapped mid word")
+    }
+
+    @Test
+    fun `deflow() with quoted text and space-stuffing and delSp=true`() {
+        val input = "> Quoted te \r\n" +
+            "> xt"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_YES)
+
+        assertThat(result).isEqualTo("> Quoted text")
+    }
+
+    @Test
+    fun `deflow() with space-stuffed second line`() {
+        val input = "Text that should be \r\n" +
+            " displayed on one line"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo("Text that should be displayed on one line")
+    }
+
+    @Test
+    fun `deflow() with only space-stuffing`() {
+        val input = "Line 1\r\n" +
+            " Line 2\r\n" +
+            " Line 3\r\n"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo("Line 1\r\nLine 2\r\nLine 3\r\n")
+    }
+
+    @Test
+    fun `deflow() with quoted space-stuffed second line`() {
+        val input = "> Text that should be \r\n" +
+            "> displayed on one line"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo("> Text that should be displayed on one line")
+    }
+
+    @Test
+    fun `deflow() with text containing signature`() {
+        val input = "Text that should be \r\n" +
+            "displayed on one line.\r\n" +
+            "\r\n" +
+            "-- \r\n" +
+            "Signature \r\n" +
+            "text"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo(
+            """
+            Text that should be displayed on one line.
+            
+            --${" "}
+            Signature text
+            """.trimIndent().crlf()
+        )
+    }
+
+    @Test
+    fun `deflow() with quoted text containing signature`() {
+        val input = "> Text that should be \r\n" +
+            "> displayed on one line.\r\n" +
+            "> \r\n" +
+            "> -- \r\n" +
+            "> Signature \r\n" +
+            "> text"
+
+        val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
+
+        assertThat(result).isEqualTo(
+            """
+            > Text that should be displayed on one line.
+            >${" "}
+            > --${" "}
+            > Signature text
+            """.trimIndent().crlf()
+        )
+    }
+}