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 b90149fb8520ac6d7839a45625d7c769c4266a0c Author: Benoit TELLIER <[email protected]> AuthorDate: Wed Nov 4 15:12:49 2026 +0100 [FIX] RawFieldParser: a quoted pair in a quoted boundary stands for the escaped character copyQuotedContent keeps the backslash of a quoted pair before any character other than a quote or a backslash, so boundary="a\bc" was read as a\bc by the lenient parsers and MimeTokenStream, while the strict DOM parser reads abc: the two split the multipart on different delimiters. The boundary already has its own path (no encoded-word decoding); it now drops the backslash of every quoted pair too, as RFC 2046 bchars hold no backslash. Other parameters are unchanged. Fixes #108 --- .../apache/james/mime4j/stream/RawFieldParser.java | 22 ++++++++++++---------- .../james/mime4j/stream/RawFieldParserTest.java | 12 ++++++++++++ .../mime4j/field/LenientContentTypeFieldTest.java | 12 ++++++++++++ 3 files changed, 36 insertions(+), 10 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 13c17953..22044939 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 @@ -166,8 +166,9 @@ public class RawFieldParser { if (delim == ';') { return new NameValuePair(name, null); } - // The boundary is a byte exact delimiter: never decode it as an encoded word - String value = parseValue(buf, cursor, SEMICOLON, !"boundary".equalsIgnoreCase(name)); + // The boundary is a byte exact delimiter: never decode it as an encoded word, + // and a quoted pair always stands for the escaped character alone + String value = parseValue(buf, cursor, SEMICOLON, "boundary".equalsIgnoreCase(name)); if (!cursor.atEnd()) { cursor.updatePos(cursor.getPos() + 1); } @@ -217,11 +218,11 @@ public class RawFieldParser { * is not delimited by any character. */ public String parseValue(final ByteSequence buf, final ParserCursor cursor, final BitSet delimiters) { - return parseValue(buf, cursor, delimiters, true); + return parseValue(buf, cursor, delimiters, false); } private String parseValue(final ByteSequence buf, final ParserCursor cursor, final BitSet delimiters, - final boolean decodeEncodedWords) { + final boolean boundary) { StringBuilder dst = new StringBuilder(); boolean whitespace = false; while (!cursor.atEnd()) { @@ -237,7 +238,7 @@ public class RawFieldParser { if (dst.length() > 0 && whitespace) { dst.append(' '); } - copyQuotedContent(buf, cursor, dst, decodeEncodedWords); + copyQuotedContent(buf, cursor, dst, boundary); whitespace = false; } else { if (dst.length() > 0 && whitespace) { @@ -449,7 +450,7 @@ public class RawFieldParser { */ public void copyQuotedContent(final ByteSequence buf, final ParserCursor cursor, final StringBuilder dst) { - copyQuotedContent(buf, cursor, dst, true); + copyQuotedContent(buf, cursor, dst, false); } /** @@ -471,7 +472,7 @@ public class RawFieldParser { } private void copyQuotedContent(final ByteSequence buf, final ParserCursor cursor, - final StringBuilder dst, final boolean decodeEncodedWords) { + final StringBuilder dst, final boolean boundary) { if (cursor.atEnd()) { return; } @@ -497,7 +498,8 @@ public class RawFieldParser { // A fold is not part of the value: the quoted pair applies to the WSP after it continue; } - if (current != '\"' && current != '\\') { + // A boundary cannot hold a backslash (RFC 2046 bchars): the quoted pair is unescaped + if (!boundary && current != '\"' && current != '\\') { dstRaw.append('\\'); } dstRaw.append(currentByte); @@ -517,7 +519,7 @@ public class RawFieldParser { if (CharsetUtil.isASCII(dstRaw)) { // Check for encoded word directly on bytes — avoids an intermediate String - if (decodeEncodedWords && dstRaw.length() > 1 && dstRaw.byteAt(0) == '=' && dstRaw.byteAt(1) == '?') { + if (!boundary && dstRaw.length() > 1 && dstRaw.byteAt(0) == '=' && dstRaw.byteAt(1) == '?') { String raw = ContentUtil.decode(dstRaw); dst.append(DecoderUtil.decodeEncodedWords(raw, DecodeMonitor.SILENT)); } else { @@ -528,7 +530,7 @@ public class RawFieldParser { } } else { String decoded = ContentUtil.decode(StandardCharsets.UTF_8, dstRaw); - if (decodeEncodedWords && decoded.startsWith("=?")) { + if (!boundary && decoded.startsWith("=?")) { decoded = DecoderUtil.decodeEncodedWords(decoded, DecodeMonitor.SILENT); } dst.append(decoded); 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 d9e042ec..ed285332 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 @@ -544,6 +544,18 @@ public class RawFieldParserTest { Assert.assertEquals("a\\\r\"; size=1.txt", body.getParams().get(0).getValue()); } + @Test + public void quotedPairInBoundaryShouldBeUnescaped() throws Exception { + ByteSequence raw = ContentUtil.encode("multipart/mixed; boundary=\"a\\bc\"; name=\"a\\bc\""); + RawBody body = parser.parseRawBody(raw, new ParserCursor(0, raw.length())); + Assert.assertEquals("abc", body.getParams().get(0).getValue()); + Assert.assertEquals("a\\bc", body.getParams().get(1).getValue()); + + FallbackBodyDescriptorBuilder builder = new FallbackBodyDescriptorBuilder(); + builder.addField(new RawField("Content-Type", "multipart/mixed; boundary=\"a\\bc\"")); + Assert.assertEquals("abc", builder.build().getBoundary()); + } + @Test public void fieldNameShouldNotHoldComments() throws Exception { RawField renamed = parser.parseField(ContentUtil.encode("Subj(x)ect: hello")); diff --git a/dom/src/test/java/org/apache/james/mime4j/field/LenientContentTypeFieldTest.java b/dom/src/test/java/org/apache/james/mime4j/field/LenientContentTypeFieldTest.java index 08c832eb..0a62e2fb 100644 --- a/dom/src/test/java/org/apache/james/mime4j/field/LenientContentTypeFieldTest.java +++ b/dom/src/test/java/org/apache/james/mime4j/field/LenientContentTypeFieldTest.java @@ -108,4 +108,16 @@ public class LenientContentTypeFieldTest { Assert.assertEquals(strict.getMimeType(), lenient.getMimeType()); Assert.assertEquals("message/rfc822", parse("Content-Type: message/rfc822 ").getMimeType()); } + + @Test + public void quotedPairInBoundaryShouldBeUnescapedLikeTheStrictParser() throws Exception { + String raw = "Content-Type: multipart/mixed; boundary=\"a\\bc\"; name=\"a\\bc\""; + ContentTypeField lenient = parse(raw); + ContentTypeField strict = (ContentTypeField) DefaultFieldParser.parse(raw); + + Assert.assertEquals("abc", strict.getBoundary()); + Assert.assertEquals("abc", lenient.getBoundary()); + // other parameters keep the backslash before an ordinary character, as before + Assert.assertEquals("a\\bc", lenient.getParameter("name")); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
