(geronimo-mail) branch main updated: GERONIMO-6851 - Add support for "mail.mime.parameters.strict" to ease transition between mail libs
[email protected] Sun, 19 Jul 2026 07:48:30 +0000
| Newsgroups | gmane.comp.java.geronimo.cvs |
|---|---|
| Message-ID | <178444731004.431430.18223708471147495526@gitbox3-he-fi.apache.org> |
This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/geronimo-mail.git
The following commit(s) were added to refs/heads/main by this push:
new 1fba502 GERONIMO-6851 - Add support for "mail.mime.parameters.strict" to ease transition between mail libs
1fba502 is described below
commit 1fba502ec596f620bfdd77f63eb57a5d9d786f34
Author: Richard Zowalla <[email protected]>
AuthorDate: Sun Jul 19 09:46:19 2026 +0200
GERONIMO-6851 - Add support for "mail.mime.parameters.strict" to ease transition between mail libs
---
.../jakarta/mail/internet/HeaderTokenizer.java | 6 +-
.../java/jakarta/mail/internet/ParameterList.java | 128 +++++++++++++--------
.../jakarta/mail/internet/ParameterListTest.java | 70 +++++++++++
3 files changed, 157 insertions(+), 47 deletions(-)
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/HeaderTokenizer.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/HeaderTokenizer.java
index d6d9d00..9a2b4d1 100644
--- a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/HeaderTokenizer.java
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/HeaderTokenizer.java
@@ -406,7 +406,11 @@ public class HeaderTokenizer {
}
}
- if (i <= 0) {
+ // i is the index of the last non-whitespace character, or -1 when the
+ // string is empty or all whitespace. Note that index 0 may well hold a
+ // real character (a one character token), so only a negative index means
+ // there is nothing left.
+ if (i < 0) {
return "";
} else {
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/ParameterList.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/ParameterList.java
index 67c5f36..979d0e6 100644
--- a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/ParameterList.java
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/ParameterList.java
@@ -64,6 +64,7 @@ public class ParameterList {
private static final String MIME_ENCODEPARAMETERS = "mail.mime.encodeparameters";
private static final String MIME_DECODEPARAMETERS = "mail.mime.decodeparameters";
private static final String MIME_DECODEPARAMETERS_STRICT = "mail.mime.decodeparameters.strict";
+ private static final String MIME_PARAMETERS_STRICT = "mail.mime.parameters.strict";
private static final int HEADER_SIZE_LIMIT = 76;
@@ -115,6 +116,7 @@ public class ParameterList {
private boolean encodeParameters = false;
private boolean decodeParameters = false;
private boolean decodeParametersStrict = false;
+ private boolean parametersStrict = true;
public ParameterList() {
// figure out how parameter handling is to be performed.
@@ -126,13 +128,20 @@ public class ParameterList {
getInitialProperties();
// get a token parser for the type information
final HeaderTokenizer tokenizer = new HeaderTokenizer(list, HeaderTokenizer.MIME);
+ // In lenient mode ("mail.mime.parameters.strict" set to false), reading an
+ // unquoted value that contains special characters swallows the ';' that
+ // terminates the value. This flag remembers that so the next loop pass
+ // does not insist on seeing another ';' before the following parameter name.
+ boolean separatorConsumed = false;
while (true) {
HeaderTokenizer.Token token = tokenizer.next();
if (token.getType() == HeaderTokenizer.Token.EOF) {
// the EOF token terminates parsing.
break;
- } else if (token.getType() == ';') {
+ }
+
+ if (token.getType() == ';') {
// each new parameter is separated by a semicolon, including the
// first, which separates
// the parameters from the main part of the header.
@@ -143,65 +152,88 @@ public class ParameterList {
if (token.getType() == HeaderTokenizer.Token.EOF) {
break;
}
+ } else if (!(separatorConsumed && token.getType() == HeaderTokenizer.Token.ATOM)) {
+ // in lenient mode the previous value scan may have consumed the
+ // separator already, in which case this token is the next parameter
+ // name. In every other situation a missing semicolon is an error.
+ throw new ParseException("Missing ';'");
+ }
+ separatorConsumed = false;
- if (token.getType() != HeaderTokenizer.Token.ATOM) {
- throw new ParseException("Invalid parameter name: " + token.getValue());
- }
+ if (token.getType() != HeaderTokenizer.Token.ATOM) {
+ throw new ParseException("Invalid parameter name: " + token.getValue());
+ }
- // get the parameter name as a lower case version for better
- // mapping.
- String name = token.getValue().toLowerCase();
+ // get the parameter name as a lower case version for better
+ // mapping.
+ String name = token.getValue().toLowerCase();
- token = tokenizer.next();
+ token = tokenizer.next();
- // parameters are name=value, so we must have the "=" here.
- if (token.getType() != '=') {
- throw new ParseException("Missing '='");
- }
+ // parameters are name=value, so we must have the "=" here.
+ if (token.getType() != '=') {
+ throw new ParseException("Missing '='");
+ }
- // now the value, which may be an atom or a literal
+ // now the value, which may be an atom or a literal
+ final String value;
+ if (parametersStrict) {
token = tokenizer.next();
if (token.getType() != HeaderTokenizer.Token.ATOM && token.getType() != HeaderTokenizer.Token.QUOTEDSTRING) {
throw new ParseException("Invalid parameter value: " + token.getValue());
}
-
- final String value = token.getValue();
- String decodedValue = null;
-
- // we might have to do some additional decoding. A name that
- // ends with "*"
- // is marked as being encoded, so if requested, we decode the
- // value.
- if (decodeParameters && name.endsWith("*") && !isMultiSegmentName(name)) {
- // the name needs to be pruned of the marker, and we need to
- // decode the value.
- name = name.substring(0, name.length() - 1);
- // get a new decoder
- final RFC2231Encoder decoder = new RFC2231Encoder(HeaderTokenizer.MIME);
-
- try {
- // decode the value
- decodedValue = decoder.decode(value);
- } catch (final Exception e) {
- // if we're doing things strictly, then raise a parsing
- // exception for errors.
- // otherwise, leave the value in its current state.
- if (decodeParametersStrict) {
- throw new ParseException("Invalid RFC2231 encoded parameter");
- }
- }
- _parameters.put(name, new ParameterValue(name, decodedValue, value));
- } else if (isMultiSegmentName(name)) {
- // multisegment parameter
- _multiSegmentParameters.put(new MultiSegmentEntry(name), new ParameterValue(name, value));
+ value = token.getValue();
+ } else if (tokenizer.peek().getType() == HeaderTokenizer.Token.QUOTEDSTRING) {
+ // lenient mode, but the value is a properly quoted string. Read it
+ // exactly like the strict path so embedded ';' and escapes keep working.
+ value = tokenizer.next().getValue();
+ } else {
+ // lenient mode with an unquoted value: take the raw text up to the
+ // next ';' (or the end of the header) as the value, even if it
+ // contains whitespace or special characters.
+ token = tokenizer.next(';', true);
+ if (token.getType() == HeaderTokenizer.Token.EOF || token.getType() == ';') {
+ // there was nothing between the '=' and the terminator, treat
+ // this as an empty value rather than failing.
+ value = "";
} else {
- _parameters.put(name, new ParameterValue(name, value));
+ value = token.getValue().trim();
}
+ // the raw scan stops after the terminating ';' (if there was one).
+ separatorConsumed = true;
+ }
+ String decodedValue = null;
+
+ // we might have to do some additional decoding. A name that
+ // ends with "*"
+ // is marked as being encoded, so if requested, we decode the
+ // value.
+ if (decodeParameters && name.endsWith("*") && !isMultiSegmentName(name)) {
+ // the name needs to be pruned of the marker, and we need to
+ // decode the value.
+ name = name.substring(0, name.length() - 1);
+ // get a new decoder
+ final RFC2231Encoder decoder = new RFC2231Encoder(HeaderTokenizer.MIME);
+
+ try {
+ // decode the value
+ decodedValue = decoder.decode(value);
+ } catch (final Exception e) {
+ // if we're doing things strictly, then raise a parsing
+ // exception for errors.
+ // otherwise, leave the value in its current state.
+ if (decodeParametersStrict) {
+ throw new ParseException("Invalid RFC2231 encoded parameter");
+ }
+ }
+ _parameters.put(name, new ParameterValue(name, decodedValue, value));
+ } else if (isMultiSegmentName(name)) {
+ // multisegment parameter
+ _multiSegmentParameters.put(new MultiSegmentEntry(name), new ParameterValue(name, value));
} else {
-
- throw new ParseException("Missing ';'");
+ _parameters.put(name, new ParameterValue(name, value));
}
}
@@ -326,6 +358,10 @@ public class ParameterList {
decodeParameters = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS, true); //since JavaMail 1.5 RFC 2231 support is enabled by default
decodeParametersStrict = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS_STRICT, false);
encodeParameters = SessionUtil.getBooleanProperty(MIME_ENCODEPARAMETERS, true); //since JavaMail 1.5 RFC 2231 support is enabled by default
+ // when false, parameter values that fail to follow the MIME token/quoted-string
+ // rules are recovered by reading the raw text up to the next ';' instead of
+ // raising a ParseException. The default is the spec-conforming strict mode.
+ parametersStrict = SessionUtil.getBooleanProperty(MIME_PARAMETERS_STRICT, true);
}
public int size() {
diff --git a/geronimo-mail_2.1_spec/src/test/java/jakarta/mail/internet/ParameterListTest.java b/geronimo-mail_2.1_spec/src/test/java/jakarta/mail/internet/ParameterListTest.java
index a90a47a..d7605bf 100644
--- a/geronimo-mail_2.1_spec/src/test/java/jakarta/mail/internet/ParameterListTest.java
+++ b/geronimo-mail_2.1_spec/src/test/java/jakarta/mail/internet/ParameterListTest.java
@@ -19,10 +19,13 @@
package jakarta.mail.internet;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @version $Rev$ $Date$
@@ -169,6 +172,73 @@ public class ParameterListTest {
assertEquals(list2.toString(), encodedTest);
}
+ // header reported in GERONIMO-6851: the type parameter value contains an
+ // unquoted '/' which is a MIME special character
+ private static final String UNQUOTED_SPECIALS_HEADER =
+ "multipart/related; type=text/html; boundary=MErelboundary-32775405-3-1674841212";
+
+ @AfterEach
+ public void clearParametersStrictProperty() {
+ System.clearProperty("mail.mime.parameters.strict");
+ }
+
+ @Test
+ public void testLenientParsingOfUnquotedSpecials() throws Exception {
+ System.setProperty("mail.mime.parameters.strict", "false");
+
+ // the reporter's exact header must now parse
+ final ContentType contentType = new ContentType(UNQUOTED_SPECIALS_HEADER);
+ assertEquals("multipart", contentType.getPrimaryType());
+ assertEquals("related", contentType.getSubType());
+ assertEquals("text/html", contentType.getParameter("type"));
+ assertEquals("MErelboundary-32775405-3-1674841212", contentType.getParameter("boundary"));
+
+ // and isMimeType on a part carrying that raw header must work instead of
+ // throwing a ParseException
+ final InternetHeaders headers = new InternetHeaders();
+ headers.addHeader("Content-Type", UNQUOTED_SPECIALS_HEADER);
+ final MimeBodyPart part = new MimeBodyPart(headers, new byte[0]);
+ assertFalse(part.isMimeType("text/plain"));
+ assertTrue(part.isMimeType("multipart/related"));
+ }
+
+ @Test
+ public void testStrictDefaultRejectsUnquotedSpecials() {
+ // without the property set, strict parsing is the default and the
+ // reporter's header must still be rejected
+ assertThrows(ParseException.class, () -> new ContentType(UNQUOTED_SPECIALS_HEADER));
+ assertThrows(ParseException.class, () ->
+ new ParameterList("; type=text/html; boundary=MErelboundary-32775405-3-1674841212"));
+
+ // an explicit "true" behaves the same way
+ System.setProperty("mail.mime.parameters.strict", "true");
+ assertThrows(ParseException.class, () -> new ContentType(UNQUOTED_SPECIALS_HEADER));
+ }
+
+ @Test
+ public void testLenientParsingKeepsWellFormedValues() throws Exception {
+ System.setProperty("mail.mime.parameters.strict", "false");
+
+ // quoted values (including embedded ';' and whitespace) are untouched
+ final ParameterList quoted = new ParameterList("; foo=\"a;b c\"; bar=plain");
+ assertEquals("a;b c", quoted.get("foo"));
+ assertEquals("plain", quoted.get("bar"));
+
+ // RFC 2231 encoded parameters still decode
+ final ParameterList encoded = new ParameterList("; title*=us-ascii'en-us'This%20is%20fun; charset=us-ascii");
+ assertEquals("This is fun", encoded.get("title"));
+ assertEquals("us-ascii", encoded.get("charset"));
+
+ // RFC 2231 multi-segment parameters still combine
+ final ParameterList multi = new ParameterList(";foo*0=one;foo*1=\"two\"");
+ assertEquals("onetwo", multi.get("foo"));
+
+ // an unquoted value with whitespace runs to the next ';' and is trimmed
+ final ParameterList spaces = new ParameterList("; name=hello world stuff ; next=x");
+ assertEquals("hello world stuff", spaces.get("name"));
+ assertEquals("x", spaces.get("next"));
+ }
+
@Test
public void testStrictDecodeRejectsBadHex() {
System.setProperty("mail.mime.decodeparameters", "true");