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 d9d2f89ee836fd4831763f5e7403452b6352a1f7
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Aug 31 15:34:05 2026 +0200

    [FIX] Limit multipart count and nesting
---
 .../mime4j/io/MaxNestingDepthLimitException.java   |  38 ++++
 .../mime4j/io/MaxPartCountLimitException.java      |  38 ++++
 .../org/apache/james/mime4j/stream/MimeConfig.java |  85 +++++++
 .../james/mime4j/stream/MimeTokenStream.java       |  30 +++
 .../james/mime4j/stream/MimeEntityLimitsTest.java  | 253 +++++++++++++++++++++
 .../james/mime4j/dom/MessagePartLimitsTest.java    |  99 ++++++++
 6 files changed, 543 insertions(+)

diff --git 
a/core/src/main/java/org/apache/james/mime4j/io/MaxNestingDepthLimitException.java
 
b/core/src/main/java/org/apache/james/mime4j/io/MaxNestingDepthLimitException.java
new file mode 100644
index 00000000..399ed758
--- /dev/null
+++ 
b/core/src/main/java/org/apache/james/mime4j/io/MaxNestingDepthLimitException.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.mime4j.io;
+
+import org.apache.james.mime4j.MimeException;
+
+/**
+ * Signals a parsing error due to MIME entities (multiparts and embedded
+ * messages) being nested more deeply than the maximum limit.
+ *
+ * @see 
org.apache.james.mime4j.stream.MimeConfig.Builder#setMaxNestingDepth(int)
+ */
+public class MaxNestingDepthLimitException extends MimeException {
+
+    private static final long serialVersionUID = 6011628067570972836L;
+
+    public MaxNestingDepthLimitException(final String message) {
+        super(message);
+    }
+
+}
diff --git 
a/core/src/main/java/org/apache/james/mime4j/io/MaxPartCountLimitException.java 
b/core/src/main/java/org/apache/james/mime4j/io/MaxPartCountLimitException.java
new file mode 100644
index 00000000..e26bcacd
--- /dev/null
+++ 
b/core/src/main/java/org/apache/james/mime4j/io/MaxPartCountLimitException.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.mime4j.io;
+
+import org.apache.james.mime4j.MimeException;
+
+/**
+ * Signals a parsing error due to the total number of MIME entities (body parts
+ * and embedded messages) contained in a message exceeding the maximum limit.
+ *
+ * @see org.apache.james.mime4j.stream.MimeConfig.Builder#setMaxPartCount(int)
+ */
+public class MaxPartCountLimitException extends MimeException {
+
+    private static final long serialVersionUID = -7742064603843344870L;
+
+    public MaxPartCountLimitException(final String message) {
+        super(message);
+    }
+
+}
diff --git a/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java 
b/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java
index 77ad1527..c11ee6f7 100644
--- a/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java
+++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java
@@ -43,6 +43,8 @@ public final class MimeConfig {
     private final int maxHeaderCount;
     private final int maxHeaderLen;
     private final long maxContentLen;
+    private final int maxPartCount;
+    private final int maxNestingDepth;
     private final boolean countLineNumbers;
     private final String headlessParsing;
     private final boolean malformedHeaderStartsBody;
@@ -53,6 +55,8 @@ public final class MimeConfig {
             int maxHeaderCount,
             int maxHeaderLen,
             long maxContentLen,
+            int maxPartCount,
+            int maxNestingDepth,
             boolean countLineNumbers,
             String headlessParsing,
             boolean malformedHeaderStartsBody) {
@@ -63,6 +67,8 @@ public final class MimeConfig {
         this.maxHeaderCount = maxHeaderCount;
         this.maxHeaderLen = maxHeaderLen;
         this.maxContentLen = maxContentLen;
+        this.maxPartCount = maxPartCount;
+        this.maxNestingDepth = maxNestingDepth;
         this.headlessParsing = headlessParsing;
     }
 
@@ -131,6 +137,28 @@ public final class MimeConfig {
         return maxContentLen;
     }
 
+    /**
+     * Returns the maximum part count limit
+     *
+     * @see Builder#setMaxPartCount(int)
+     *
+     * @return value of the maximum part count limit
+     */
+    public int getMaxPartCount() {
+        return maxPartCount;
+    }
+
+    /**
+     * Returns the maximum nesting depth limit
+     *
+     * @see Builder#setMaxNestingDepth(int)
+     *
+     * @return value of the maximum nesting depth limit
+     */
+    public int getMaxNestingDepth() {
+        return maxNestingDepth;
+    }
+
     /**
      * Returns the value of the line number counting mode.
      *
@@ -159,6 +187,8 @@ public final class MimeConfig {
                 .append(", maxHeaderCount=").append(maxHeaderCount)
                 .append(", maxHeaderLen=").append(maxHeaderLen)
                 .append(", maxContentLen=").append(maxContentLen)
+                .append(", maxPartCount=").append(maxPartCount)
+                .append(", maxNestingDepth=").append(maxNestingDepth)
                 .append(", countLineNumbers=").append(countLineNumbers)
                 .append(", headlessParsing=").append(headlessParsing)
                 .append(", 
malformedHeaderStartsBody=").append(malformedHeaderStartsBody)
@@ -180,6 +210,8 @@ public final class MimeConfig {
             .setMaxHeaderCount(config.getMaxHeaderCount())
             .setMaxHeaderLen(config.getMaxHeaderLen())
             .setMaxContentLen(config.getMaxContentLen())
+            .setMaxPartCount(config.getMaxPartCount())
+            .setMaxNestingDepth(config.getMaxNestingDepth())
             .setCountLineNumbers(config.isCountLineNumbers())
             .setHeadlessParsing(config.getHeadlessParsing())
             
.setMalformedHeaderStartsBody(config.isMalformedHeaderStartsBody());
@@ -192,6 +224,8 @@ public final class MimeConfig {
         private int maxHeaderCount;
         private int maxHeaderLen;
         private long maxContentLen;
+        private int maxPartCount;
+        private int maxNestingDepth;
         private boolean countLineNumbers;
         private String headlessParsing;
         private boolean malformedHeaderStartsBody;
@@ -204,6 +238,8 @@ public final class MimeConfig {
             this.maxHeaderCount = 1000;
             this.maxHeaderLen = 10000;
             this.maxContentLen = -1;
+            this.maxPartCount = 512;
+            this.maxNestingDepth = 64;
             this.headlessParsing = null;
         }
 
@@ -309,6 +345,53 @@ public final class MimeConfig {
             return this;
         }
 
+        /**
+         * Sets the maximum number of MIME entities (body parts and embedded
+         * messages) a message may be made of. Parsing will be terminated with 
a
+         * {@link org.apache.james.mime4j.io.MaxPartCountLimitException} if a
+         * message contains more entities than this limit. If this parameter is
+         * set to a non positive value the part count check will be disabled.
+         * <p>
+         * The top level message itself is not counted, so a limit of
+         * <code>1</code> allows a multipart message holding a single body 
part.
+         * <p>
+         * Unlike {@link #setMaxHeaderCount(int)} and
+         * {@link #setMaxContentLen(long)}, which are enforced per entity, this
+         * limit applies to the message as a whole. Without it a small message
+         * made of a very large number of tiny parts can make a consumer
+         * building an object graph per part exhaust its memory.
+         * <p>
+         * Default value: <code>512</code>
+         *
+         * @param maxPartCount
+         *            maximum part count limit
+         */
+        public Builder setMaxPartCount(int maxPartCount) {
+            this.maxPartCount = maxPartCount;
+            return this;
+        }
+
+        /**
+         * Sets the maximum nesting depth of MIME entities. Parsing will be
+         * terminated with a
+         * {@link org.apache.james.mime4j.io.MaxNestingDepthLimitException} if
+         * entities are nested more deeply than this limit. If this parameter 
is
+         * set to a non positive value the nesting depth check will be 
disabled.
+         * <p>
+         * The top level message is at depth <code>1</code>, so a limit of
+         * <code>2</code> allows a multipart message whose body parts are not
+         * themselves multipart or embedded messages.
+         * <p>
+         * Default value: <code>64</code>
+         *
+         * @param maxNestingDepth
+         *            maximum nesting depth limit
+         */
+        public Builder setMaxNestingDepth(int maxNestingDepth) {
+            this.maxNestingDepth = maxNestingDepth;
+            return this;
+        }
+
         /**
          * Defines whether the parser should count line numbers. If enabled 
line
          * numbers are included in the debug output.
@@ -346,6 +429,8 @@ public final class MimeConfig {
                     maxHeaderCount,
                     maxHeaderLen,
                     maxContentLen,
+                    maxPartCount,
+                    maxNestingDepth,
                     countLineNumbers,
                     headlessParsing,
                     malformedHeaderStartsBody);
diff --git 
a/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java 
b/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java
index 8b6bcd7c..4fba9d0b 100644
--- a/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java
+++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java
@@ -31,6 +31,8 @@ import org.apache.james.mime4j.Charsets;
 import org.apache.james.mime4j.MimeException;
 import org.apache.james.mime4j.codec.DecodeMonitor;
 import org.apache.james.mime4j.io.LineNumberInputStream;
+import org.apache.james.mime4j.io.MaxNestingDepthLimitException;
+import org.apache.james.mime4j.io.MaxPartCountLimitException;
 import org.apache.james.mime4j.util.CharsetUtil;
 
 /**
@@ -88,6 +90,7 @@ public class MimeTokenStream {
     private EntityStateMachine currentStateMachine;
     private RecursionMode recursionMode = RecursionMode.M_RECURSE;
     private MimeEntity rootentity;
+    private int partCount;
 
     /**
      * Constructs a standard (lax) stream.
@@ -204,6 +207,7 @@ public class MimeTokenStream {
 
         rootentity.setRecursionMode(recursionMode);
         currentStateMachine = rootentity;
+        partCount = 0;
         entities.clear();
         entities.add(currentStateMachine);
         state = currentStateMachine.getState();
@@ -374,6 +378,7 @@ public class MimeTokenStream {
         while (currentStateMachine != null) {
             EntityStateMachine next = currentStateMachine.advance();
             if (next != null) {
+                checkEntityLimits();
                 entities.add(next);
                 currentStateMachine = next;
             }
@@ -396,6 +401,31 @@ public class MimeTokenStream {
         return state;
     }
 
+    /**
+     * Enforces the limits on the total number of MIME entities a message may 
be
+     * made of and on how deeply those entities may be nested. Both are message
+     * wide limits, unlike the header and content limits which are enforced per
+     * entity, and they bound the work a consumer of this token stream can be
+     * made to do by a small but pathologically structured message.
+     * <p>
+     * Called before a newly discovered entity is pushed onto the stack.
+     */
+    private void checkEntityLimits() throws MimeException {
+        int maxPartCount = config.getMaxPartCount();
+        if (maxPartCount > 0 && partCount >= maxPartCount) {
+            throw new MaxPartCountLimitException("Maximum part count limit ("
+                    + maxPartCount + ") exceeded");
+        }
+        partCount++;
+        int maxNestingDepth = config.getMaxNestingDepth();
+        // entities already holds the parent chain of the entity being pushed,
+        // the root message included, so the new entity sits at depth size + 1.
+        if (maxNestingDepth > 0 && entities.size() >= maxNestingDepth) {
+            throw new MaxNestingDepthLimitException("Maximum nesting depth 
limit ("
+                    + maxNestingDepth + ") exceeded");
+        }
+    }
+
     /**
      * Renders a state as a string suitable for logging.
      * @param state
diff --git 
a/core/src/test/java/org/apache/james/mime4j/stream/MimeEntityLimitsTest.java 
b/core/src/test/java/org/apache/james/mime4j/stream/MimeEntityLimitsTest.java
new file mode 100644
index 00000000..6305e1a6
--- /dev/null
+++ 
b/core/src/test/java/org/apache/james/mime4j/stream/MimeEntityLimitsTest.java
@@ -0,0 +1,253 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.mime4j.stream;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.apache.james.mime4j.Charsets;
+import org.apache.james.mime4j.io.MaxNestingDepthLimitException;
+import org.apache.james.mime4j.io.MaxPartCountLimitException;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests the message wide limits bounding the number of MIME entities a message
+ * may be made of and how deeply they may be nested.
+ */
+public class MimeEntityLimitsTest {
+
+    /**
+     * A multipart message made of <code>count</code> empty body parts. Each 
part
+     * costs about 10 bytes on the wire but makes a consumer building an object
+     * graph per part allocate far more than that.
+     */
+    private static InputStream flatMultipart(int count) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Content-Type: multipart/mixed; boundary=b\r\n\r\n");
+        for (int i = 0; i < count; i++) {
+            sb.append("--b\r\n\r\n");
+        }
+        sb.append("--b--\r\n");
+        return new 
ByteArrayInputStream(sb.toString().getBytes(Charsets.US_ASCII));
+    }
+
+    /**
+     * A message of nested multiparts, <code>depth</code> levels deep. The top
+     * level message is at depth 1, so the innermost body part is at depth
+     * <code>depth</code>.
+     */
+    private static InputStream nestedMultipart(int depth) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 1; i < depth; i++) {
+            sb.append("Content-Type: multipart/mixed; 
boundary=b").append(i).append("\r\n\r\n");
+            sb.append("--b").append(i).append("\r\n");
+        }
+        sb.append("\r\n");
+        for (int i = depth - 1; i >= 1; i--) {
+            sb.append("--b").append(i).append("--\r\n");
+        }
+        return new 
ByteArrayInputStream(sb.toString().getBytes(Charsets.US_ASCII));
+    }
+
+    private static int parse(MimeConfig config, InputStream in) throws 
Exception {
+        MimeTokenStream stream = new MimeTokenStream(config);
+        stream.parse(in);
+        int parts = 0;
+        for (EntityState state = stream.getState();
+             state != EntityState.T_END_OF_STREAM;
+             state = stream.next()) {
+            if (state == EntityState.T_START_BODYPART) {
+                parts++;
+            }
+        }
+        return parts;
+    }
+
+    @Test
+    public void partCountLimitShouldAcceptAMessageAtTheLimit() throws 
Exception {
+        MimeConfig config = MimeConfig.custom().setMaxPartCount(3).build();
+        Assert.assertEquals(3, parse(config, flatMultipart(3)));
+    }
+
+    @Test
+    public void partCountLimitShouldRejectAMessageOverTheLimit() throws 
Exception {
+        MimeConfig config = MimeConfig.custom().setMaxPartCount(3).build();
+        try {
+            parse(config, flatMultipart(4));
+            Assert.fail("MaxPartCountLimitException expected");
+        } catch (MaxPartCountLimitException expected) {
+            Assert.assertEquals("Maximum part count limit (3) exceeded", 
expected.getMessage());
+        }
+    }
+
+    @Test
+    public void partCountLimitShouldBeDisabledWhenNegative() throws Exception {
+        MimeConfig config = MimeConfig.custom().setMaxPartCount(-1).build();
+        Assert.assertEquals(2000, parse(config, flatMultipart(2000)));
+    }
+
+    @Test
+    public void partCountLimitShouldBeDisabledWhenZero() throws Exception {
+        MimeConfig config = MimeConfig.custom().setMaxPartCount(0).build();
+        Assert.assertEquals(2000, parse(config, flatMultipart(2000)));
+    }
+
+    @Test
+    public void partCountLimitShouldBeDisabledOnACopyOfPermissive() throws 
Exception {
+        MimeConfig config = 
MimeConfig.copy(MimeConfig.PERMISSIVE).setMaxPartCount(-1).build();
+        Assert.assertEquals(-1, config.getMaxPartCount());
+        Assert.assertEquals(2000, parse(config, flatMultipart(2000)));
+    }
+
+    @Test
+    public void partCountLimitShouldCountEmbeddedMessages() throws Exception {
+        // multipart/mixed holding one message/rfc822: the body part and the
+        // embedded message are two entities.
+        String message = "Content-Type: multipart/mixed; boundary=b\r\n\r\n"
+                + "--b\r\n"
+                + "Content-Type: message/rfc822\r\n\r\n"
+                + "Subject: embedded\r\n\r\n"
+                + "body\r\n"
+                + "--b--\r\n";
+        InputStream in = new 
ByteArrayInputStream(message.getBytes(Charsets.US_ASCII));
+        try {
+            parse(MimeConfig.custom().setMaxPartCount(1).build(), in);
+            Assert.fail("MaxPartCountLimitException expected");
+        } catch (MaxPartCountLimitException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void defaultConfigShouldAcceptAMessageWithinThePartCountDefault() 
throws Exception {
+        Assert.assertEquals(512, parse(MimeConfig.DEFAULT, 
flatMultipart(512)));
+    }
+
+    @Test
+    public void defaultConfigShouldBoundThePartCount() throws Exception {
+        try {
+            parse(MimeConfig.DEFAULT, flatMultipart(513));
+            Assert.fail("MaxPartCountLimitException expected");
+        } catch (MaxPartCountLimitException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void permissiveConfigShouldBoundThePartCount() throws Exception {
+        try {
+            parse(MimeConfig.PERMISSIVE, flatMultipart(513));
+            Assert.fail("MaxPartCountLimitException expected");
+        } catch (MaxPartCountLimitException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void nestingDepthLimitShouldAcceptAMessageAtTheLimit() throws 
Exception {
+        MimeConfig config = MimeConfig.custom()
+                .setMaxNestingDepth(4)
+                .setMaxPartCount(-1)
+                .build();
+        Assert.assertEquals(3, parse(config, nestedMultipart(4)));
+    }
+
+    @Test
+    public void nestingDepthLimitShouldRejectAMessageOverTheLimit() throws 
Exception {
+        MimeConfig config = MimeConfig.custom()
+                .setMaxNestingDepth(4)
+                .setMaxPartCount(-1)
+                .build();
+        try {
+            parse(config, nestedMultipart(5));
+            Assert.fail("MaxNestingDepthLimitException expected");
+        } catch (MaxNestingDepthLimitException expected) {
+            Assert.assertEquals("Maximum nesting depth limit (4) exceeded", 
expected.getMessage());
+        }
+    }
+
+    @Test
+    public void nestingDepthLimitShouldBeDisabledWhenNegative() throws 
Exception {
+        MimeConfig config = MimeConfig.custom()
+                .setMaxNestingDepth(-1)
+                .setMaxPartCount(-1)
+                .build();
+        Assert.assertEquals(199, parse(config, nestedMultipart(200)));
+    }
+
+    @Test
+    public void nestingDepthLimitShouldBeDisabledWhenZero() throws Exception {
+        MimeConfig config = MimeConfig.custom()
+                .setMaxNestingDepth(0)
+                .setMaxPartCount(0)
+                .build();
+        Assert.assertEquals(199, parse(config, nestedMultipart(200)));
+    }
+
+    @Test
+    public void nestingDepthLimitShouldBeDisabledOnACopyOfPermissive() throws 
Exception {
+        MimeConfig config = MimeConfig.copy(MimeConfig.PERMISSIVE)
+                .setMaxNestingDepth(-1)
+                .setMaxPartCount(-1)
+                .build();
+        Assert.assertEquals(-1, config.getMaxNestingDepth());
+        Assert.assertEquals(199, parse(config, nestedMultipart(200)));
+    }
+
+    @Test
+    public void 
defaultConfigShouldAcceptAMessageWithinTheNestingDepthDefault() throws 
Exception {
+        MimeConfig config = 
MimeConfig.copy(MimeConfig.DEFAULT).setMaxPartCount(-1).build();
+        Assert.assertEquals(63, parse(config, nestedMultipart(64)));
+    }
+
+    @Test
+    public void defaultConfigShouldBoundTheNestingDepth() throws Exception {
+        MimeConfig config = 
MimeConfig.copy(MimeConfig.DEFAULT).setMaxPartCount(-1).build();
+        try {
+            parse(config, nestedMultipart(65));
+            Assert.fail("MaxNestingDepthLimitException expected");
+        } catch (MaxNestingDepthLimitException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void limitsShouldBeResetBetweenParses() throws Exception {
+        MimeConfig config = MimeConfig.custom().setMaxPartCount(3).build();
+        MimeTokenStream stream = new MimeTokenStream(config);
+        for (int i = 0; i < 3; i++) {
+            stream.parse(flatMultipart(3));
+            while (stream.getState() != EntityState.T_END_OF_STREAM) {
+                stream.next();
+            }
+        }
+    }
+
+    @Test
+    public void copyShouldCarryTheLimitsOver() {
+        MimeConfig config = MimeConfig.copy(MimeConfig.custom()
+                .setMaxPartCount(7)
+                .setMaxNestingDepth(9)
+                .build()).build();
+        Assert.assertEquals(7, config.getMaxPartCount());
+        Assert.assertEquals(9, config.getMaxNestingDepth());
+    }
+}
diff --git 
a/dom/src/test/java/org/apache/james/mime4j/dom/MessagePartLimitsTest.java 
b/dom/src/test/java/org/apache/james/mime4j/dom/MessagePartLimitsTest.java
new file mode 100644
index 00000000..954fefb7
--- /dev/null
+++ b/dom/src/test/java/org/apache/james/mime4j/dom/MessagePartLimitsTest.java
@@ -0,0 +1,99 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.mime4j.dom;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.apache.james.mime4j.Charsets;
+import org.apache.james.mime4j.MimeIOException;
+import org.apache.james.mime4j.io.MaxNestingDepthLimitException;
+import org.apache.james.mime4j.io.MaxPartCountLimitException;
+import org.apache.james.mime4j.message.DefaultMessageBuilder;
+import org.apache.james.mime4j.stream.MimeConfig;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Building a DOM allocates an object graph per MIME entity, so a small message
+ * made of a huge number of tiny parts is an amplification vector. These tests
+ * pin down that the message wide entity limits bound that work, including 
under
+ * {@link MimeConfig#PERMISSIVE} which is what mail servers typically use on
+ * untrusted inbound traffic.
+ */
+public class MessagePartLimitsTest {
+
+    private static InputStream flatMultipart(int count) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Subject: hi\r\n");
+        sb.append("Content-Type: multipart/mixed; boundary=b\r\n\r\n");
+        for (int i = 0; i < count; i++) {
+            sb.append("--b\r\n\r\n");
+        }
+        sb.append("--b--\r\n");
+        return new 
ByteArrayInputStream(sb.toString().getBytes(Charsets.US_ASCII));
+    }
+
+    private static InputStream nestedMultipart(int depth) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 1; i < depth; i++) {
+            sb.append("Content-Type: multipart/mixed; 
boundary=b").append(i).append("\r\n\r\n");
+            sb.append("--b").append(i).append("\r\n");
+        }
+        sb.append("\r\n");
+        for (int i = depth - 1; i >= 1; i--) {
+            sb.append("--b").append(i).append("--\r\n");
+        }
+        return new 
ByteArrayInputStream(sb.toString().getBytes(Charsets.US_ASCII));
+    }
+
+    private static DefaultMessageBuilder builder(MimeConfig config) {
+        DefaultMessageBuilder builder = new DefaultMessageBuilder();
+        builder.setMimeEntityConfig(config);
+        return builder;
+    }
+
+    @Test
+    public void parseMessageShouldRejectTooManyPartsWithPermissiveConfig() 
throws Exception {
+        try {
+            builder(MimeConfig.PERMISSIVE).parseMessage(flatMultipart(50000));
+            Assert.fail("MimeIOException expected");
+        } catch (MimeIOException e) {
+            Assert.assertTrue(e.getCause() instanceof 
MaxPartCountLimitException);
+        }
+    }
+
+    @Test
+    public void 
parseMessageShouldRejectTooDeeplyNestedPartsWithPermissiveConfig() throws 
Exception {
+        MimeConfig config = 
MimeConfig.copy(MimeConfig.PERMISSIVE).setMaxPartCount(-1).build();
+        try {
+            builder(config).parseMessage(nestedMultipart(5000));
+            Assert.fail("MimeIOException expected");
+        } catch (MimeIOException e) {
+            Assert.assertTrue(e.getCause() instanceof 
MaxNestingDepthLimitException);
+        }
+    }
+
+    @Test
+    public void parseMessageShouldAcceptAMessageWithinTheLimits() throws 
Exception {
+        Message message = 
builder(MimeConfig.PERMISSIVE).parseMessage(flatMultipart(512));
+        Assert.assertEquals(512, ((Multipart) message.getBody()).getCount());
+    }
+}


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

Reply via email to