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]
