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]

Reply via email to