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 5080ca7c1f35eb3cb8711e44cd5a5a9e35b9670c Author: Benoit TELLIER <[email protected]> AuthorDate: Wed Nov 4 19:34:39 2026 +0100 [FIX] stop() in a multipart: never start a new body part After stop(), MimeEntity still opened the next body part whenever the part stream had not reached the last delimiter. Two paths reached it: - with malformedHeaderStartsBody, a field cut short by stop() unread the line read ahead into the truncated buffer. Reads were refused, so the part stream never reached eof() and an empty body part was started again and again, forever. - after a preamble or body part was read to its end, the delimiter had already been skipped, so one empty body part followed stop(), and strict parsing failed on its empty header. Once stopped, the multipart now ends right away (T_END_MULTIPART), and an unread into a truncated buffer drops the bytes like the rest of it. --- .../mime4j/io/BufferedLineReaderInputStream.java | 9 +++- .../org/apache/james/mime4j/stream/MimeEntity.java | 12 +++-- .../james/mime4j/stream/MimeTokenStreamTest.java | 59 ++++++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java b/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java index 1c360129..52323276 100644 --- a/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java +++ b/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java @@ -382,13 +382,18 @@ public class BufferedLineReaderInputStream extends LineReaderInputStream { @Override public boolean unread(ByteArrayBuffer buf) { - prepend(buf.buffer(), buf.length()); + if (!this.truncated) { + prepend(buf.buffer(), buf.length()); + } + // else truncated by stop(): the bytes are dropped, as the rest of the buffer return true; } @Override public boolean unread(RecycledByteArrayBuffer buf) { - prepend(buf.buffer(), buf.length()); + if (!this.truncated) { + prepend(buf.buffer(), buf.length()); + } return true; } diff --git a/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java b/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java index 28f384ce..ce311aea 100644 --- a/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java +++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java @@ -356,11 +356,15 @@ class MimeEntity implements EntityStateMachine { // better handle missing parts. // Removed the T_IN_BODYPART state (always use T_PREAMBLE) advanceToBoundary(); + if (stopped) { + // stop(): the multipart ends here, whatever the part stream still holds. A part cut + // short is not a malformed message, and no new part starts. + clearMimePartStream(); + state = EntityState.T_END_MULTIPART; + break; + } if (currentMimePartStream.eof() && !currentMimePartStream.isLastPart()) { - if (!stopped) { - // A part cut short by stop() is not a malformed message - monitor(Event.MIME_BODY_PREMATURE_END); - } + monitor(Event.MIME_BODY_PREMATURE_END); } else { if (!currentMimePartStream.isLastPart()) { clearMimePartStream(); diff --git a/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamTest.java b/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamTest.java index 491cd776..38e7189a 100644 --- a/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamTest.java +++ b/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamTest.java @@ -25,6 +25,7 @@ import org.junit.Before; import org.junit.Test; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -160,4 +161,62 @@ public class MimeTokenStreamTest { } Assert.assertEquals(EntityState.T_END_OF_STREAM, state); } + + @Test(timeout = 10000) + public void stopOnAFieldFollowedByAMalformedLineShouldEndTheStream() throws Exception { + String msg = "Content-Type: multipart/mixed; boundary=b\r\n" + + "xyz\r\n" + + "--b\r\n" + + "\r\n" + + "x\r\n" + + "--b--\r\n"; + MimeTokenStream stream = new MimeTokenStream(MimeConfig.custom() + .setMalformedHeaderStartsBody(true) + .build()); + stream.parse(new ByteArrayInputStream(msg.getBytes(StandardCharsets.US_ASCII))); + EntityState state = stream.next(); // T_START_HEADER + state = stream.next(); + Assert.assertEquals(EntityState.T_FIELD, state); + stream.stop(); + + int calls = 0; + while (state != EntityState.T_END_OF_STREAM) { + state = stream.next(); + Assert.assertNotEquals(EntityState.T_START_BODYPART, state); + Assert.assertTrue("no T_END_OF_STREAM after stop()", ++calls < 1000); + } + } + + @Test + public void stopAfterReadingThePreambleShouldNotStartABodyPart() throws Exception { + String msg = "Content-Type: multipart/mixed; boundary=b\r\n" + + "\r\n" + + "pre\r\n" + + "--b\r\n" + + "Subject: a\r\n" + + "\r\n" + + "x\r\n" + + "--b--\r\n"; + for (boolean strict : new boolean[] {false, true}) { + MimeTokenStream stream = new MimeTokenStream(MimeConfig.custom().setStrictParsing(strict).build()); + stream.parse(new ByteArrayInputStream(msg.getBytes(StandardCharsets.US_ASCII))); + EntityState state; + while ((state = stream.next()) != EntityState.T_PREAMBLE) { + Assert.assertNotEquals(EntityState.T_END_OF_STREAM, state); + } + InputStream preamble = stream.getInputStream(); + while (preamble.read() != -1) { + // read the preamble to its end + } + stream.stop(); + + List<EntityState> after = new ArrayList<>(); + while (state != EntityState.T_END_OF_STREAM) { + state = stream.next(); + after.add(state); + } + Assert.assertEquals(Arrays.asList(EntityState.T_END_MULTIPART, EntityState.T_END_MESSAGE, + EntityState.T_END_OF_STREAM), after); + } + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
