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 a18cb891ef57e587a378cd70fdbf924bbda85384
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Nov 4 15:14:06 2026 +0100

    [FIX] Fields: keep a long Content-Disposition parameter within the line 
limit
    
    EncoderUtil#encodeHeaderParameter wrote a parameter as one token or
    quoted string whatever its length, and folding never breaks a run
    without whitespace: a long filename or Content-Type parameter gave a
    header line over 998 octets that DefaultMessageBuilder rejects. A new
    encodeHeaderParameter(name, value, rfc2231Sections) splits such a
    parameter into RFC 2231 sections (percent encoded in UTF-8 when not
    ASCII), which the Content-Disposition parser decodes, and rejects it
    with an IllegalArgumentException otherwise: no Content-Type parser
    decodes RFC 2231 sections, so Fields.contentType rejects such a value.
    
    Fixes #105
---
 .../org/apache/james/mime4j/codec/EncoderUtil.java | 107 +++++++++++++++++++++
 .../java/org/apache/james/mime4j/field/Fields.java |  10 +-
 .../james/mime4j/message/MultipartBuilderTest.java |  50 ++++++++++
 3 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/core/src/main/java/org/apache/james/mime4j/codec/EncoderUtil.java 
b/core/src/main/java/org/apache/james/mime4j/codec/EncoderUtil.java
index 8a874e37..9613e717 100644
--- a/core/src/main/java/org/apache/james/mime4j/codec/EncoderUtil.java
+++ b/core/src/main/java/org/apache/james/mime4j/codec/EncoderUtil.java
@@ -54,6 +54,17 @@ public class EncoderUtil {
 
     private static final BitSet TOKEN_CHARS = initChars("()<>@,;:\\\"/[]?=");
 
+    // RFC 2231 attribute-char: a token char other than "*", "'" and "%"
+    private static final BitSet ATTRIBUTE_CHARS = 
initChars("()<>@,;:\\\"/[]?=*'%");
+
+    private static final Pattern WHITESPACE = Pattern.compile("[ \t]");
+
+    /** Longest run without whitespace written as is: it fits, with the 
folding space and a ";", on a 998 octet line */
+    private static final int MAX_PARAMETER_LENGTH = 998 - 2;
+
+    /** Length of the value of an RFC 2231 section written for a parameter 
that is too long */
+    private static final int PARAMETER_SECTION_LENGTH = 60;
+
     private static final BitSet ATEXT_CHARS = initChars("()<>@.,;:\\\"[]");
 
     private static final Pattern QUOTE = Pattern.compile("[\\\\\"]");
@@ -215,6 +226,41 @@ public class EncoderUtil {
         }
         name = name.toLowerCase(Locale.US);
 
+        return encodeHeaderParameterSection(name, value);
+    }
+
+    /**
+     * Encodes the specified strings into a header parameter like
+     * {@link #encodeHeaderParameter(String, String)}, for a header that is
+     * written on lines of at most 998 octets (RFC 5322 section 2.1.1). Folding
+     * only breaks a line at a whitespace: a parameter that holds a run without
+     * whitespace too long for one line is split into RFC 2231 sections, 
percent encoded in UTF-8 when the value
+     * is not printable ASCII, if the header parser of the field decodes them.
+     *
+     * @param name
+     *            parameter name.
+     * @param value
+     *            parameter value.
+     * @param rfc2231Sections
+     *            whether a parameter too long for one line may be split into
+     *            RFC 2231 sections.
+     * @return encoded result.
+     * @throws IllegalArgumentException if the name is invalid, or if the
+     *             parameter is too long for one line and cannot be split.
+     */
+    public static String encodeHeaderParameter(String name, String value, 
boolean rfc2231Sections) {
+        String parameter = encodeHeaderParameter(name, value);
+        if (fitsOnFoldedLines(parameter)) {
+            return parameter;
+        }
+        name = name.toLowerCase(Locale.US);
+        if (!rfc2231Sections || name.indexOf('*') >= 0) {
+            throw new IllegalArgumentException("Parameter too long for a 
header line: " + name);
+        }
+        return isPrintableAscii(value) ? encodeSections(name, value) : 
encodeEncodedSections(name, value);
+    }
+
+    private static String encodeHeaderParameterSection(String name, String 
value) {
         // value := token / quoted-string
         if (isToken(value)) {
             return name + "=" + value;
@@ -223,6 +269,67 @@ public class EncoderUtil {
         }
     }
 
+    private static boolean fitsOnFoldedLines(String parameter) {
+        for (String run : WHITESPACE.split(parameter)) {
+            if (run.getBytes(Charsets.UTF_8).length > MAX_PARAMETER_LENGTH) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean isPrintableAscii(String value) {
+        for (int idx = 0; idx < value.length(); idx++) {
+            char ch = value.charAt(idx);
+            if (ch < 32 || ch >= 127) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // RFC 2231 section 3: name*0="..."; name*1="..."
+    private static String encodeSections(String name, String value) {
+        StringBuilder sb = new StringBuilder();
+        int section = 0;
+        for (int start = 0; start < value.length(); start += 
PARAMETER_SECTION_LENGTH) {
+            if (section > 0) {
+                sb.append("; ");
+            }
+            String part = value.substring(start, Math.min(value.length(), 
start + PARAMETER_SECTION_LENGTH));
+            sb.append(encodeHeaderParameterSection(name + "*" + section, 
part));
+            section++;
+        }
+        return sb.toString();
+    }
+
+    // RFC 2231 sections 3 and 4: name*0*=utf-8''%XX...; name*1*=%XX...
+    private static String encodeEncodedSections(String name, String value) {
+        StringBuilder sb = new StringBuilder();
+        StringBuilder current = new StringBuilder("utf-8''");
+        int section = 0;
+        for (byte b : value.getBytes(Charsets.UTF_8)) {
+            int v = b & 0xff;
+            String encoded = ATTRIBUTE_CHARS.get(v)
+                    ? String.valueOf((char) v)
+                    : "%" + hexDigit(v >>> 4) + hexDigit(v & 0xf);
+            if (current.length() + encoded.length() > 
PARAMETER_SECTION_LENGTH) {
+                appendEncodedSection(sb, name, section++, current);
+                current.setLength(0);
+            }
+            current.append(encoded);
+        }
+        appendEncodedSection(sb, name, section, current);
+        return sb.toString();
+    }
+
+    private static void appendEncodedSection(StringBuilder sb, String name, 
int section, CharSequence value) {
+        if (section > 0) {
+            sb.append("; ");
+        }
+        sb.append(name).append('*').append(section).append("*=").append(value);
+    }
+
     /**
      * Shortcut method that encodes the specified text into an encoded-word if
      * the text has to be encoded.
diff --git a/dom/src/main/java/org/apache/james/mime4j/field/Fields.java 
b/dom/src/main/java/org/apache/james/mime4j/field/Fields.java
index c24b0c1b..3881a98e 100644
--- a/dom/src/main/java/org/apache/james/mime4j/field/Fields.java
+++ b/dom/src/main/java/org/apache/james/mime4j/field/Fields.java
@@ -103,8 +103,9 @@ public class Fields {
             StringBuilder sb = new StringBuilder(mimeType);
             for (Map.Entry<String, String> entry : parameters.entrySet()) {
                 sb.append("; ");
+                // no Content-Type parser decodes RFC 2231 sections: a value 
too long for a line is rejected
                 sb.append(EncoderUtil.encodeHeaderParameter(entry.getKey(),
-                        entry.getValue()));
+                        entry.getValue(), false));
             }
             String contentType = sb.toString();
             return contentType(contentType);
@@ -136,7 +137,8 @@ public class Fields {
                 sb.append("; ");
                 String name = param.getName();
                 String value = param.getValue();
-                sb.append(EncoderUtil.encodeHeaderParameter(name, value != 
null ? value : ""));
+                // no Content-Type parser decodes RFC 2231 sections: a value 
too long for a line is rejected
+                sb.append(EncoderUtil.encodeHeaderParameter(name, value != 
null ? value : "", false));
             }
             String contentType = sb.toString();
             return contentType(contentType);
@@ -214,7 +216,7 @@ public class Fields {
             for (Map.Entry<String, String> entry : parameters.entrySet()) {
                 sb.append("; ");
                 sb.append(EncoderUtil.encodeHeaderParameter(entry.getKey(),
-                        entry.getValue()));
+                        entry.getValue(), true));
             }
             String contentDisposition = sb.toString();
             return contentDisposition(contentDisposition);
@@ -246,7 +248,7 @@ public class Fields {
                 sb.append("; ");
                 String name = param.getName();
                 String value = param.getValue();
-                sb.append(EncoderUtil.encodeHeaderParameter(name, value != 
null ? value : ""));
+                sb.append(EncoderUtil.encodeHeaderParameter(name, value != 
null ? value : "", true));
             }
             String contentDisposition = sb.toString();
             return contentDisposition(contentDisposition);
diff --git 
a/dom/src/test/java/org/apache/james/mime4j/message/MultipartBuilderTest.java 
b/dom/src/test/java/org/apache/james/mime4j/message/MultipartBuilderTest.java
index baefb160..25de31ef 100644
--- 
a/dom/src/test/java/org/apache/james/mime4j/message/MultipartBuilderTest.java
+++ 
b/dom/src/test/java/org/apache/james/mime4j/message/MultipartBuilderTest.java
@@ -33,9 +33,11 @@ import org.apache.james.mime4j.dom.Header;
 import org.apache.james.mime4j.dom.Message;
 import org.apache.james.mime4j.dom.Multipart;
 import org.apache.james.mime4j.dom.TextBody;
+import org.apache.james.mime4j.dom.field.ContentDispositionField;
 import org.apache.james.mime4j.dom.field.ContentTransferEncodingField;
 import org.apache.james.mime4j.dom.field.ContentTypeField;
 import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.stream.NameValuePair;
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -239,4 +241,52 @@ public class MultipartBuilderTest {
         Assert.assertEquals("message/rfc822", part.getMimeType());
         Assert.assertEquals("text/plain", embedded.getMimeType());
     }
+
+    @Test
+    public void longContentTypeParameterShouldBeRejected() {
+        String value = repeat("a", 1100);
+        MultipartBuilder multipart = MultipartBuilder.create("report")
+            .addContentTypeParameter(new NameValuePair("report-type", value));
+
+        Assert.assertThrows(IllegalArgumentException.class,
+            () -> Message.Builder.of().setBody(multipart.build()));
+    }
+
+    @Test
+    public void longFilenameIsWrittenWithinTheLineLimit() throws Exception {
+        assertLongFilenameRoundTrips(repeat("a", 1100));
+    }
+
+    @Test
+    public void longNonAsciiFilenameIsWrittenWithinTheLineLimit() throws 
Exception {
+        assertLongFilenameRoundTrips(repeat("\u00e9", 600));
+    }
+
+    private void assertLongFilenameRoundTrips(String filename) throws 
Exception {
+        Message message = Message.Builder.of()
+            .setBody(MultipartBuilder.create("mixed")
+                .addBodyPart(BodyPartBuilder.create()
+                    .setBody("body", StandardCharsets.UTF_8)
+                    .setContentDisposition("attachment", filename))
+                .build())
+            .build();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        new DefaultMessageWriter().writeMessage(message, out);
+
+        for (String line : new String(out.toByteArray(), 
StandardCharsets.ISO_8859_1).split("\r\n")) {
+            Assert.assertTrue(line, line.length() <= 998);
+        }
+        Message parsed = new DefaultMessageBuilder().parseMessage(new 
ByteArrayInputStream(out.toByteArray()));
+        Entity part = ((Multipart) parsed.getBody()).getBodyParts().get(0);
+        Assert.assertEquals(filename,
+            ((ContentDispositionField) 
part.getHeader().getField("Content-Disposition")).getFilename());
+    }
+
+    private static String repeat(String s, int count) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < count; i++) {
+            sb.append(s);
+        }
+        return sb.toString();
+    }
 }


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

Reply via email to