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 726042ff212a9056ce41d620367314976f46398d Author: Benoit TELLIER <[email protected]> AuthorDate: Wed Nov 4 15:10:13 2026 +0100 [FIX] MimeBoundaryInputStream: scan the buffered bytes before reading more The constructor always read the underlying stream first, before looking for the delimiter in the bytes already buffered. For a nested multipart read in one go, the enclosing part was then already exhausted: in strict mode, the extra read of an enclosing part without a closing delimiter threw "Unexpected end of stream" before the nested preamble, while small reads reported the preamble and the next part first. The constructor now scans the buffered bytes first. That read also told, as a side effect, whether the source ends right after a delimiter line: skipBoundary now reads ahead when nothing is buffered after the delimiter, so that eof() stays the same. Fixes #111 --- .../james/mime4j/io/MimeBoundaryInputStream.java | 22 +++++++-- .../mime4j/stream/StrictMimeTokenStreamTest.java | 53 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java index 609a5d9c..6169d39d 100644 --- a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java +++ b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java @@ -104,7 +104,12 @@ public class MimeBoundaryInputStream extends LineReaderInputStream { this.boundary[i + 2] = boundaryBytes.byteAt(i); } - fillBuffer(); + if (inbuffer.hasBufferedData()) { + // Scan the bytes already buffered before reading more: the enclosing part may hold no more + findLimit(); + } else { + fillBuffer(); + } // Read until we know whether the stream is empty, whatever the size of the reads while (!atBoundary && !eof && !hasData()) { fillBuffer(); @@ -266,10 +271,16 @@ public class MimeBoundaryInputStream extends LineReaderInputStream { } else { bytesRead = 0; } - if (atBoundary) { - return bytesRead; + if (!atBoundary) { + findLimit(); } + return bytesRead; + } + /** + * Sets the limit of the readable data from the buffered bytes: up to the delimiter if they hold it. + */ + private void findLimit() throws IOException { int i = findDelimiter(); if (i != -1) { limit = i; @@ -288,7 +299,6 @@ public class MimeBoundaryInputStream extends LineReaderInputStream { } limit = end; } - return bytesRead; } /** @@ -437,6 +447,10 @@ public class MimeBoundaryInputStream extends LineReaderInputStream { fillBuffer(); } } + if (!eof && !buffer.hasBufferedData()) { + // Find out whether anything follows the delimiter line + fillBuffer(); + } } } diff --git a/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java b/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java index 02c3dbf5..b0c8ab68 100644 --- a/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java +++ b/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java @@ -19,13 +19,19 @@ package org.apache.james.mime4j.stream; +import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.MimeIOException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; public class StrictMimeTokenStreamTest { @@ -94,6 +100,53 @@ public class StrictMimeTokenStreamTest { } } + private static final String NESTED_UNTERMINATED = + "Content-Type: multipart/mixed; boundary=a\r\n\r\n" + + "--a\r\n" + + "Content-Type: multipart/mixed; boundary=b\r\n\r\n" + + "Th\r\n" + + "--b\r\n" + + "x"; + + private static List<String> events(InputStream in) throws Exception { + MimeTokenStream stream = new MimeTokenStream(MimeConfig.STRICT); + stream.parse(in); + List<String> events = new ArrayList<>(); + try { + for (EntityState s = stream.getState(); s != EntityState.T_END_OF_STREAM; s = stream.next()) { + if (s == EntityState.T_PREAMBLE || s == EntityState.T_BODY || s == EntityState.T_EPILOGUE) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream body = stream.getInputStream(); + byte[] b = new byte[512]; + int n; + while ((n = body.read(b)) != -1) { + out.write(b, 0, n); + } + events.add(s + " " + out.toString("US-ASCII")); + } else { + events.add(s.toString()); + } + } + } catch (MimeException | IOException e) { + events.add("EXCEPTION " + e.getMessage()); + } + return events; + } + + @Test + public void nestedMultipartWithoutClosingDelimiterDoesNotDependOnReadSizes() throws Exception { + byte[] message = NESTED_UNTERMINATED.getBytes("US-ASCII"); + InputStream oneByteReads = new FilterInputStream(new ByteArrayInputStream(message)) { + @Override + public int read(byte[] b, int off, int len) throws IOException { + return super.read(b, off, Math.min(len, 1)); + } + }; + List<String> whole = events(new ByteArrayInputStream(message)); + Assert.assertTrue(whole.toString(), whole.contains("T_PREAMBLE Th")); + Assert.assertEquals(events(oneByteReads), whole); + } + private void checkNextIs(EntityState expected) throws Exception { Assert.assertEquals(MimeTokenStream.stateToString(expected), MimeTokenStream.stateToString(parser.next())); } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
