This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-mime4j.git

commit 319fe5425e3d0494bdb7733eedc53b41c8589f0f
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Nov 4 19:41:37 2026 +0100

    [FIX] RawFieldParser: a bare CR or LF is deleted, as MimeUtil#unfold does
    
    The strict parsers read the field body through MimeUtil#unfold, which
    deletes every CR and LF. RawFieldParser instead took a CR or LF as a word
    separator, and kept a quoted pair "\" + bare CR as an escaped CR. So
    "multi<CR>part/mixed" was multipart for the strict parser only, and
    Content-Disposition, Content-Language, MIME-Version and Content-Location
    values differed between the strict and lenient parsers in the same way.
    
    RawFieldParser now deletes a CR or LF wherever it is: only the WSP of a
    fold separates words, and in a quoted pair the backslash applies to the
    character after the line break. This supersedes a8ed914c, which kept the
    CR after a backslash: the strict parsers never did.
---
 .../apache/james/mime4j/stream/RawFieldParser.java | 57 +++++++++-----
 .../james/mime4j/stream/RawFieldParserTest.java    | 23 ++++--
 .../field/BareLineBreakStrictLenientTest.java      | 92 ++++++++++++++++++++++
 .../field/address/LenientAddressBuilderTest.java   |  3 +-
 4 files changed, 149 insertions(+), 26 deletions(-)

diff --git 
a/core/src/main/java/org/apache/james/mime4j/stream/RawFieldParser.java 
b/core/src/main/java/org/apache/james/mime4j/stream/RawFieldParser.java
index 22044939..c0b87673 100644
--- a/core/src/main/java/org/apache/james/mime4j/stream/RawFieldParser.java
+++ b/core/src/main/java/org/apache/james/mime4j/stream/RawFieldParser.java
@@ -191,6 +191,10 @@ public class RawFieldParser {
             char current = (char) (buf.byteAt(cursor.getPos()) & 0xff);
             if (delimiters != null && delimiters.get(current)) {
                 break;
+            } else if (isLineBreak(current)) {
+                // As MimeUtil#unfold, which the strict parsers read: CR and 
LF are deleted, only
+                // the WSP of a fold separates words
+                cursor.updatePos(cursor.getPos() + 1);
             } else if (CharsetUtil.isWhitespace(current)) {
                 skipWhiteSpace(buf, cursor);
                 whitespace = true;
@@ -229,6 +233,10 @@ public class RawFieldParser {
             char current = (char) (buf.byteAt(cursor.getPos()) & 0xff);
             if (delimiters != null && delimiters.get(current)) {
                 break;
+            } else if (isLineBreak(current)) {
+                // As MimeUtil#unfold, which the strict parsers read: CR and 
LF are deleted, only
+                // the WSP of a fold separates words
+                cursor.updatePos(cursor.getPos() + 1);
             } else if (CharsetUtil.isWhitespace(current)) {
                 skipWhiteSpace(buf, cursor);
                 whitespace = true;
@@ -358,6 +366,11 @@ public class RawFieldParser {
         for (int i = indexFrom; i < indexTo; i++) {
             byte bVal = buf.byteAt(i);
             char current = (char) (bVal & 0xff);
+            if (isLineBreak(current)) {
+                // deleted, as by MimeUtil#unfold: the word goes on
+                pos++;
+                continue;
+            }
             if ((delimiters != null && delimiters.get(current))
                     || CharsetUtil.isWhitespace(current) || current == '(') {
                 break;
@@ -369,11 +382,17 @@ public class RawFieldParser {
                 ByteArrayBuffer dstRaw = DECODE_BUFFER.get();
                 dstRaw.clear();
                 for (int j = indexFrom; j <= i; j++) {
-                    dstRaw.append(buf.byteAt(j));
+                    if (!isLineBreak((char) (buf.byteAt(j) & 0xff))) {
+                        dstRaw.append(buf.byteAt(j));
+                    }
                 }
                 for (int k = i + 1; k < indexTo; k++) {
                     byte bk = buf.byteAt(k);
                     char ck = (char) (bk & 0xff);
+                    if (isLineBreak(ck)) {
+                        pos++;
+                        continue;
+                    }
                     if ((delimiters != null && delimiters.get(ck))
                             || CharsetUtil.isWhitespace(ck) || ck == '(') {
                         break;
@@ -409,6 +428,11 @@ public class RawFieldParser {
         for (int i = indexFrom; i < indexTo; i++) {
             byte bVal = buf.byteAt(i);
             char current = (char) (bVal & 0xff);
+            if (isLineBreak(current)) {
+                // deleted, as by MimeUtil#unfold: the word goes on
+                pos++;
+                continue;
+            }
             if ((delimiters != null && delimiters.get(current))
                     || CharsetUtil.isWhitespace(current) || current == '(' || 
current == '\"') {
                 break;
@@ -420,11 +444,17 @@ public class RawFieldParser {
                 ByteArrayBuffer dstRaw = DECODE_BUFFER.get();
                 dstRaw.clear();
                 for (int j = indexFrom; j <= i; j++) {
-                    dstRaw.append(buf.byteAt(j));
+                    if (!isLineBreak((char) (buf.byteAt(j) & 0xff))) {
+                        dstRaw.append(buf.byteAt(j));
+                    }
                 }
                 for (int k = i + 1; k < indexTo; k++) {
                     byte bk = buf.byteAt(k);
                     char ck = (char) (bk & 0xff);
+                    if (isLineBreak(ck)) {
+                        pos++;
+                        continue;
+                    }
                     if ((delimiters != null && delimiters.get(ck))
                             || CharsetUtil.isWhitespace(ck) || ck == '(' || ck 
== '\"') {
                         break;
@@ -453,22 +483,8 @@ public class RawFieldParser {
         copyQuotedContent(buf, cursor, dst, false);
     }
 
-    /**
-     * Whether the CR or LF at <code>index</code> belongs to a fold: CRLF or 
LF followed by SP or HTAB.
-     */
-    private static boolean isFoldAt(final ByteSequence buf, final int index, 
final int indexTo) {
-        int i = index;
-        if (buf.byteAt(i) == '\r') {
-            i++;
-            if (i >= indexTo || buf.byteAt(i) != '\n') {
-                return false;
-            }
-        }
-        if (buf.byteAt(i) != '\n') {
-            return false;
-        }
-        i++;
-        return i < indexTo && (buf.byteAt(i) == ' ' || buf.byteAt(i) == '\t');
+    private static boolean isLineBreak(final char c) {
+        return c == '\r' || c == '\n';
     }
 
     private void copyQuotedContent(final ByteSequence buf, final ParserCursor 
cursor,
@@ -494,8 +510,9 @@ public class RawFieldParser {
             byte currentByte = buf.byteAt(i);
             current = (char) (currentByte & 0xff);
             if (escaped) {
-                if (isFoldAt(buf, i, indexTo)) {
-                    // A fold is not part of the value: the quoted pair 
applies to the WSP after it
+                if (isLineBreak(current)) {
+                    // Deleted, as by MimeUtil#unfold (a fold or a bare CR / 
LF): the quoted pair
+                    // applies to the character after it
                     continue;
                 }
                 // A boundary cannot hold a backslash (RFC 2046 bchars): the 
quoted pair is unescaped
diff --git 
a/core/src/test/java/org/apache/james/mime4j/stream/RawFieldParserTest.java 
b/core/src/test/java/org/apache/james/mime4j/stream/RawFieldParserTest.java
index ed285332..596f6a43 100644
--- a/core/src/test/java/org/apache/james/mime4j/stream/RawFieldParserTest.java
+++ b/core/src/test/java/org/apache/james/mime4j/stream/RawFieldParserTest.java
@@ -534,14 +534,15 @@ public class RawFieldParserTest {
     }
 
     @Test
-    public void backslashBeforeALineBreakThatIsNotAFoldShouldKeepItsMeaning() 
throws Exception {
-        Assert.assertEquals("a\\\r\"b", parseValue("\"a\\\r\\\"b\""));
-        Assert.assertEquals("a\\\n\"b", parseValue("\"a\\\n\n\\\"b\""));
+    public void aLineBreakThatIsNotAFoldShouldBeDeletedAsByUnfold() throws 
Exception {
+        // MimeUtil#unfold, which the strict parsers read, deletes every CR 
and LF: the quoted pair
+        // then applies to the character after the line break
+        Assert.assertEquals(parseValue("\"a\\\\\"b\""), 
parseValue("\"a\\\r\\\"b\""));
+        Assert.assertEquals(parseValue("\"a\\\\\"b\""), 
parseValue("\"a\\\n\n\\\"b\""));
 
         ByteSequence raw = ContentUtil.encode("attachment; 
filename=\"a\\\r\\\"; size=1.txt\"");
         RawBody body = parser.parseRawBody(raw, new ParserCursor(0, 
raw.length()));
-        Assert.assertEquals(1, body.getParams().size());
-        Assert.assertEquals("a\\\r\"; size=1.txt", 
body.getParams().get(0).getValue());
+        Assert.assertEquals("a\\", body.getParams().get(0).getValue());
     }
 
     @Test
@@ -570,4 +571,16 @@ public class RawFieldParserTest {
         Assert.assertEquals("Subject", obsolete.getName());
         Assert.assertEquals("hello", obsolete.getBody());
     }
+
+    @Test
+    public void bareLineBreaksShouldBeDeletedLikeUnfoldDoes() {
+        ByteSequence buf = ContentUtil.encode("multi\rpart/mi\nxed; 
boundary=\"a\\\rb\"; c\r=d\r\n e");
+        ParserCursor cursor = new ParserCursor(0, buf.length());
+        RawBody body = parser.parseRawBody(buf, cursor);
+        org.junit.Assert.assertEquals("multipart/mixed", body.getValue());
+        List<NameValuePair> params = body.getParams();
+        org.junit.Assert.assertEquals("ab", params.get(0).getValue());
+        org.junit.Assert.assertEquals("c", params.get(1).getName());
+        org.junit.Assert.assertEquals("d e", params.get(1).getValue());
+    }
 }
diff --git 
a/dom/src/test/java/org/apache/james/mime4j/field/BareLineBreakStrictLenientTest.java
 
b/dom/src/test/java/org/apache/james/mime4j/field/BareLineBreakStrictLenientTest.java
new file mode 100644
index 00000000..5fc1bd06
--- /dev/null
+++ 
b/dom/src/test/java/org/apache/james/mime4j/field/BareLineBreakStrictLenientTest.java
@@ -0,0 +1,92 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+import org.apache.james.mime4j.dom.field.ContentDispositionField;
+import org.apache.james.mime4j.dom.field.ContentLanguageField;
+import org.apache.james.mime4j.dom.field.ContentLocationField;
+import org.apache.james.mime4j.dom.field.ContentTypeField;
+import org.apache.james.mime4j.dom.field.MimeVersionField;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * A CR or LF that is not part of a fold is deleted by MimeUtil#unfold, which 
the strict parsers
+ * read: the lenient parsers must not take it as a word separator either.
+ */
+public class BareLineBreakStrictLenientTest {
+
+    @Test
+    public void 
bareCarriageReturnInsideTheTypeShouldBeReadTheSameWayByBothParsers() throws 
Exception {
+        String raw = "Content-Type: multi\rpart/mixed; boundary=x";
+
+        ContentTypeField strict = (ContentTypeField) 
DefaultFieldParser.parse(raw);
+        ContentTypeField lenient = (ContentTypeField) 
LenientFieldParser.parse(raw);
+
+        Assert.assertEquals("multipart/mixed", strict.getMimeType());
+        Assert.assertEquals(strict.getMimeType(), lenient.getMimeType());
+        Assert.assertTrue(lenient.isMultipart());
+        Assert.assertEquals("x", lenient.getBoundary());
+    }
+
+    @Test
+    public void bareLineBreaksShouldBeReadTheSameWayByBothParsers() throws 
Exception {
+        assertSameMimeType("Content-Type: text/pl\nain; charset=us-ascii");
+        assertSameMimeType("Content-Type: text/plain\r");
+        assertSameDisposition("Content-Disposition: +;e='\r4", "e");
+        assertSameDisposition("Content-Disposition: e;filename=\"\\\n\\\"\"", 
"filename");
+
+        ContentLanguageField strictLanguage = (ContentLanguageField) 
DefaultFieldParser.parse("Content-Language: e\rs-ie2-ze4-1l");
+        ContentLanguageField lenientLanguage = (ContentLanguageField) 
LenientFieldParser.parse("Content-Language: e\rs-ie2-ze4-1l");
+        Assert.assertEquals(strictLanguage.getLanguages(), 
lenientLanguage.getLanguages());
+
+        MimeVersionField strictVersion = (MimeVersionField) 
DefaultFieldParser.parse("MIME-Version: 2\r2.0");
+        MimeVersionField lenientVersion = (MimeVersionField) 
LenientFieldParser.parse("MIME-Version: 2\r2.0");
+        Assert.assertEquals(22, strictVersion.getMajorVersion());
+        Assert.assertEquals(strictVersion.getMajorVersion(), 
lenientVersion.getMajorVersion());
+        Assert.assertEquals(strictVersion.getMinorVersion(), 
lenientVersion.getMinorVersion());
+
+        String location = "Content-Location: \"\\\n\\\r\n \"";
+        ContentLocationField strictLocation = (ContentLocationField) 
DefaultFieldParser.parse(location);
+        ContentLocationField lenientLocation = (ContentLocationField) 
LenientFieldParser.parse(location);
+        Assert.assertEquals(strictLocation.getLocation(), 
lenientLocation.getLocation());
+    }
+
+    @Test
+    public void aFoldShouldStillSeparateWords() throws Exception {
+        ContentDispositionField lenient = (ContentDispositionField) 
LenientFieldParser.parse(
+            "Content-Disposition: attachment; filename=\"a\r\n b\"; size=1\r\n 
2");
+        Assert.assertEquals("a b", lenient.getFilename());
+    }
+
+    private static void assertSameMimeType(String raw) throws Exception {
+        ContentTypeField strict = (ContentTypeField) 
DefaultFieldParser.parse(raw);
+        ContentTypeField lenient = (ContentTypeField) 
LenientFieldParser.parse(raw);
+        Assert.assertEquals(raw, strict.getMimeType(), lenient.getMimeType());
+        Assert.assertEquals(raw, strict.getParameters(), 
lenient.getParameters());
+    }
+
+    private static void assertSameDisposition(String raw, String parameter) 
throws Exception {
+        ContentDispositionField strict = (ContentDispositionField) 
DefaultFieldParser.parse(raw);
+        ContentDispositionField lenient = (ContentDispositionField) 
LenientFieldParser.parse(raw);
+        Assert.assertEquals(raw, strict.getDispositionType(), 
lenient.getDispositionType());
+        Assert.assertEquals(raw, strict.getParameter(parameter), 
lenient.getParameter(parameter));
+    }
+}
diff --git 
a/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
 
b/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
index 651267ac..97d2ee8c 100644
--- 
a/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
+++ 
b/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
@@ -618,7 +618,8 @@ public class LenientAddressBuilderTest {
 
     @Test
     public void formattedLocalPartShouldNotLoseItsContent() {
-        Mailbox parsed = (Mailbox) parser.parseAddressList("\"\\\r").get(0);
+        // The parser deletes a bare CR, as MimeUtil#unfold does: build the 
local part directly
+        Mailbox parsed = new Mailbox("\\\r", null);
         String encoded = AddressFormatter.DEFAULT.encode(parsed);
         Mailbox again = (Mailbox) parser.parseAddressList(encoded).get(0);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to