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 994242bd2a27c6ea8c64a93407b47c23ba1ea3ff
Author: Benoit TELLIER <[email protected]>
AuthorDate: Thu Nov 5 12:28:44 2026 +0100

    [FIX] MimeEntity: closing a body of an encapsulated message does not close 
its parent
    
    MimeStreamParser closes each body stream, and that close went down to what
    the encapsulated entity reads: the decoder of the message/rfc822 part, or
    the caller's stream. Since dab2f50f, leaving T_BODY reads what is left of
    the content when maxContentLen is set, so the read hit the closed stream.
    Under PERMISSIVE, parse() failed with an IOException on a base64 or
    quoted-printable message/rfc822 whose body the handler left unread, or
    holding another message/rfc822, and read the caller's stream after closing
    it without an encoding.
    
    The encapsulated entity now reads through a stream that ignores close(),
    as MimeBoundaryInputStream#close already does inside a multipart. Its parent
    closes the decoder once it has read its content. A body of a message/rfc822
    therefore no longer closes the caller's stream.
---
 .../org/apache/james/mime4j/stream/MimeEntity.java | 32 +++++++-
 .../stream/MimeTokenStreamContentLimitTest.java    | 88 ++++++++++++++++++++++
 2 files changed, 118 insertions(+), 2 deletions(-)

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 186bccf8..dc02ef2c 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
@@ -19,6 +19,7 @@
 
 package org.apache.james.mime4j.stream;
 
+import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.ref.SoftReference;
@@ -82,6 +83,8 @@ class MimeEntity implements EntityStateMachine {
     private boolean nested;
     /** Whether the content of this entity was parsed as an encapsulated 
message. */
     private boolean encapsulating;
+    /** The decoder of the encapsulated message, closed once this entity has 
read its content. */
+    private InputStream encapsulatedDecoder;
     private LineReaderInputStreamAdaptor dataStream;
     /** The limited view of {@link #dataStream}, shared by every reader of the 
current content. */
     private InputStream limitedContentStream;
@@ -395,6 +398,7 @@ class MimeEntity implements EntityStateMachine {
             break;
         case T_BODY:
             drainContent();
+            closeEncapsulatedDecoder();
             state = endState;
             break;
         case T_END_MULTIPART:
@@ -481,8 +485,32 @@ class MimeEntity implements EntityStateMachine {
         // optimize nesting of streams returning the "lower" stream instead of
         // always return dataStream (that would add a 
LineReaderInputStreamAdaptor in the chain)
         InputStream instream = currentMimePartStream != null ? 
currentMimePartStream : inbuffer;
-        instream = decodedStream(instream);
-        return nextMimeEntity(EntityState.T_START_MESSAGE, 
EntityState.T_END_MESSAGE, instream);
+        InputStream decoded = decodedStream(instream);
+        if (decoded != instream) {
+            encapsulatedDecoder = decoded;
+        }
+        return nextMimeEntity(EntityState.T_START_MESSAGE, 
EntityState.T_END_MESSAGE, new CloseShield(decoded));
+    }
+
+    private void closeEncapsulatedDecoder() throws IOException {
+        if (encapsulatedDecoder != null) {
+            encapsulatedDecoder.close();
+            encapsulatedDecoder = null;
+        }
+    }
+
+    /**
+     * Closing a body of the encapsulated message (MimeStreamParser does) must 
not close what this
+     * entity reads after it, as MimeBoundaryInputStream#close guarantees 
inside a multipart.
+     */
+    private static final class CloseShield extends FilterInputStream {
+        CloseShield(InputStream in) {
+            super(in);
+        }
+
+        @Override
+        public void close() {
+        }
     }
 
     private InputStream decodedStream(InputStream instream) {
diff --git 
a/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamContentLimitTest.java
 
b/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamContentLimitTest.java
index cbe828f1..9430152b 100644
--- 
a/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamContentLimitTest.java
+++ 
b/core/src/test/java/org/apache/james/mime4j/stream/MimeTokenStreamContentLimitTest.java
@@ -24,9 +24,12 @@ import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 
 import org.apache.james.mime4j.MimeException;
 import org.apache.james.mime4j.io.MaxContentLengthLimitException;
+import org.apache.james.mime4j.parser.AbstractContentHandler;
+import org.apache.james.mime4j.parser.MimeStreamParser;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -122,4 +125,89 @@ public class MimeTokenStreamContentLimitTest {
         Assert.assertEquals("MaxContentLengthLimitException", outcome(new 
ByteArrayInputStream(bytes(plain)), 100));
         Assert.assertEquals("MaxContentLengthLimitException", 
outcome(oneByteAtATime(bytes(plain)), 100));
     }
+
+    private static final String LEAF = "Content-Type: 
message/rfc822\n\nSubject: a\n\nbody\n";
+    private static final String ENCODED_MESSAGE = "Content-Type: 
message/rfc822\n"
+        + "Content-Transfer-Encoding: quoted-printable\n\nSubject: 
a\n\nbody\n";
+
+    private static void parseReadingBodies(InputStream source) throws 
Exception {
+        MimeStreamParser parser = new MimeStreamParser(MimeConfig.PERMISSIVE);
+        parser.setContentHandler(new AbstractContentHandler() {
+            @Override
+            public void body(BodyDescriptor bd, InputStream is) throws 
IOException {
+                byte[] buffer = new byte[256];
+                while (is.read(buffer) != -1) {
+                    // read the whole body, as a DOM builder does
+                }
+            }
+        });
+        parser.parse(source);
+    }
+
+    private static InputStream failingOnceClosed(String message) {
+        return new FilterInputStream(new ByteArrayInputStream(bytes(message))) 
{
+            private boolean closed;
+
+            @Override
+            public void close() {
+                closed = true;
+            }
+
+            @Override
+            public int read(byte[] b, int off, int len) throws IOException {
+                if (closed) {
+                    throw new IOException("Stream closed");
+                }
+                return super.read(b, off, len);
+            }
+
+            @Override
+            public int read() throws IOException {
+                if (closed) {
+                    throw new IOException("Stream closed");
+                }
+                return super.read();
+            }
+        };
+    }
+
+    @Test
+    public void closingABodyShouldNotFailAnEnclosingBase64Message() throws 
Exception {
+        String message = "Content-Type: 
message/rfc822\nContent-Transfer-Encoding: base64\n\n"
+            + Base64.getEncoder().encodeToString(bytes(LEAF)) + "\n";
+
+        parseReadingBodies(new ByteArrayInputStream(bytes(message)));
+        parseReadingBodies(new ByteArrayInputStream(bytes(
+            "Content-Type: multipart/mixed; boundary=b\n\n--b\n" + message + 
"--b--\n")));
+    }
+
+    @Test
+    public void closingABodyShouldNotLeadToReadingTheClosedSource() throws 
Exception {
+        parseReadingBodies(failingOnceClosed("Content-Type: 
message/rfc822\n\n" + LEAF));
+    }
+
+    @Test
+    public void aHandlerIgnoringTheBodyShouldNotFailAMessage() throws 
Exception {
+        for (String message : new String[] {ENCODED_MESSAGE, "Content-Type: 
message/rfc822\n\nSubject: a\n\nbody\n",
+            "Content-Type: multipart/mixed; boundary=b\n\n--b\n" + 
ENCODED_MESSAGE + "--b--\n"}) {
+            MimeStreamParser parser = new 
MimeStreamParser(MimeConfig.PERMISSIVE);
+            parser.setContentHandler(new AbstractContentHandler() {
+            });
+
+            parser.parse(failingOnceClosed(message));
+        }
+    }
+
+    @Test
+    public void aHandlerReadingPartOfTheBodyShouldNotFailAnEncodedMessage() 
throws Exception {
+        MimeStreamParser parser = new MimeStreamParser(MimeConfig.PERMISSIVE);
+        parser.setContentHandler(new AbstractContentHandler() {
+            @Override
+            public void body(BodyDescriptor bd, InputStream is) throws 
IOException {
+                is.read();
+            }
+        });
+
+        parser.parse(new ByteArrayInputStream(bytes(ENCODED_MESSAGE)));
+    }
 }


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

Reply via email to