diff --git a/api/src/main/java/jakarta/mail/internet/InternetAddress.java b/api/src/main/java/jakarta/mail/internet/InternetAddress.java index 8be9acdc..423cc496 100644 --- a/api/src/main/java/jakarta/mail/internet/InternetAddress.java +++ b/api/src/main/java/jakarta/mail/internet/InternetAddress.java @@ -1385,6 +1385,7 @@ private static void checkAddress(String addr, if (addr.charAt(start) == '.') throw new AddressException("Domain starts with dot", addr); boolean inliteral = false; + boolean idnCheck = false; for (i = start; i < len; i++) { c = addr.charAt(i); if (c == '[') { @@ -1400,6 +1401,12 @@ private static void checkAddress(String addr, } else if (c <= 040 || c == 0177) { throw new AddressException( "Domain contains control or whitespace", addr); + } else if (c == '。' || c == '.' || c == '。') { + // Dot-like code points that java.net.IDN treats as label + // separators but which are not RFC 5321 dots. Reject so + // the IDN fallback below can't silently accept them. + throw new AddressException( + "Domain contains non-ASCII dot", addr); } else { // RFC 2822 rule //if (specialsNoDot.indexOf(c) >= 0) @@ -1415,9 +1422,16 @@ private static void checkAddress(String addr, * ::= | */ if (!inliteral) { - if (!(Character.isLetterOrDigit(c) || c == '-' || c == '.')) - throw new AddressException( - "Domain contains illegal character", addr); + if (!(Character.isLetterOrDigit(c) || c == '-' || c == '.')) { + if (c < 0x80) + // ASCII punctuation / specials are genuinely + // illegal in a domain; IDN can't rescue them. + throw new AddressException( + "Domain contains illegal character", addr); + // Non-ASCII: maybe a combining mark or other IDN + // letter class. Defer to java.net.IDN below. + idnCheck = true; + } if (c == '.' && lastc == '.') throw new AddressException( "Domain contains dot-dot", addr); @@ -1427,6 +1441,23 @@ private static void checkAddress(String addr, } if (lastc == '.') throw new AddressException("Domain ends with dot", addr); + if (idnCheck) { + // The fast path rejected a character the RFC 1034 rule doesn't + // cover, but Unicode domain labels legitimately contain code + // points like combining marks. Ask java.net.IDN for a second + // opinion: if the domain Punycode-encodes cleanly (IDNA2003 + + // Nameprep), accept it. The address itself is left byte-for- + // byte unchanged. + try { + java.net.IDN.toASCII(addr.substring(start), + java.net.IDN.ALLOW_UNASSIGNED); + } catch (IllegalArgumentException e) { + AddressException ae = new AddressException( + "Domain contains illegal character", addr); + ae.setNextException(e); + throw ae; + } + } } /** diff --git a/api/src/test/java/jakarta/mail/internet/InternetAddressExtraTest.java b/api/src/test/java/jakarta/mail/internet/InternetAddressExtraTest.java index 7466607c..e375018b 100644 --- a/api/src/test/java/jakarta/mail/internet/InternetAddressExtraTest.java +++ b/api/src/test/java/jakarta/mail/internet/InternetAddressExtraTest.java @@ -18,6 +18,8 @@ import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; /** @@ -57,4 +59,25 @@ public void testNewlineInLocalWithWhitespace() throws Exception { ia.validate(); // success! } + + @Test + public void testIdnRejectionChainsCause() throws Exception { + // U+FDD0 is a Unicode noncharacter — not letter-or-digit, so the + // fast-path domain check defers to java.net.IDN, which rejects it + // as a prohibited code point. The IAE from IDN should travel along + // as the chained cause. + System.setProperty("mail.mime.allowutf8", "true"); + try { + InternetAddress ia = + new InternetAddress("arnt@foo﷐.example", true); + fail("expected rejection, got " + ia); + } catch (AddressException ex) { + Throwable cause = ex.getCause(); + assertNotNull("AddressException should carry IDN's IAE as cause", + cause); + assertSame(IllegalArgumentException.class, cause.getClass()); + } finally { + System.clearProperty("mail.mime.allowutf8"); + } + } } diff --git a/api/src/test/java/jakarta/mail/internet/InternetAddressUnicodeTest.java b/api/src/test/java/jakarta/mail/internet/InternetAddressUnicodeTest.java new file mode 100644 index 00000000..a4ccee6c --- /dev/null +++ b/api/src/test/java/jakarta/mail/internet/InternetAddressUnicodeTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package jakarta.mail.internet; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * Exercise the InternetAddress parser against an address list used copied from + * Apache James' MailAddressTest, and additions from + * https://uasg.tech/download/uasg-004-use-cases-for-ua-readiness-evaluation-en/ + */ +@RunWith(Parameterized.class) +public class InternetAddressUnicodeTest { + + private final String address; + private final boolean good; + + public InternetAddressUnicodeTest(String address, boolean good) { + this.address = address; + this.good = good; + } + + @BeforeClass + public static void enableUtf8() { + System.setProperty("mail.mime.allowutf8", "true"); + } + + @AfterClass + public static void disableUtf8() { + System.clearProperty("mail.mime.allowutf8"); + } + + private static final String[] GOOD = { + "server-dev@james.apache.org", + "\"quoted@local part\"@james.apache.org", + "server-dev@james-apache.org", + "local-part+details@example.com", + "a&b@james-apache.org", + "+details@example.com", + "server-dev@[127.0.0.1]", + "server.dev@james.apache.org", + "\\.server-dev@james.apache.org", + "Abc@10.42.0.1", + "Abc.123@example.com", + "Loïc.Accentué@voilà.fr8", + "pelé@exemple.com", + "δοκιμή@παράδειγμα.δοκιμή", + "我買@屋企.香港", + "二ノ宮@黒川.日本", + "медведь@с-балалайкой.рф", + "संपर्क@डाटामेल.भारत", + "user+mailbox/department=shipping@example.com", + "user+mailbox@example.com", + "\"Abc@def\"@example.com", + "\"Fred Bloggs\"@example.com", + "\"Joe.\\Blow\"@example.com", + "!#$%&'*+-/=?^_`.{|}~@example.com", + // Extras, for good taste: + "plain@example.com", + "UPPER@EXAMPLE.COM", + "x@y.z", + "postmaster@example.com", + }; + + // Only addresses that InternetAddress itself rejects. The james test + // lists many more, but some of them are rejected by the higher-level + // James MailAddress validator rather than by jakarta's InternetAddress. + private static final String[] BAD = { + "", + "server-dev@", + "quoted local-part@james.apache.org", + "quoted@local-part@james.apache.org", + "server-dev@james. apache.org", + "@example.com", + "foo@", + "foo@@example.com", + "foo bar@example.com", + }; + + private static final String[] UASG_GOOD = { + "info1@ua-test.link", + "info2@ua-test.technology", + "info3@普遍接受-测试.top", + "info4@ua-test.世界", + "info5@普遍接受-测试.世界", + "info4@ua-test.xn--rhqv96g", + "info3@xn----f38am99bqvcd5liy1cxsg.top", + "info5@xn----f38am99bqvcd5liy1cxsg.xn--rhqv96g", + // Devanagari local part + Devanagari domain: + "uasg.tech@डाटामेल.भारत", + "युएअसजी@डाटामेल.भारत", + // CJK local part variants: + "测试1@ua-test.link", + "测试2@ua-test.technology", + "测试3@普遍接受-测试.top", + "测试4@ua-test.世界", + "测试5@普遍接受-测试.世界", + "测试4@ua-test.xn--rhqv96g", + "测试3@xn----f38am99bqvcd5liy1cxsg.top", + "测试5@xn----f38am99bqvcd5liy1cxsg.xn--rhqv96g", + // Arabic labels (bidi marks stripped): + "info6@ختبار-القبواللعالمي.top", + "测试6@ختبار-القبواللعالمي.top", + "user@السعودية.رسيل", + "السعودية.دون@رسيل", + }; + + // UASG-004 addresses that the parser must still reject: they use + // U+3002 IDEOGRAPHIC FULL STOP in place of '.', which java.net.IDN + // happens to accept as a label separator but which is not a mailbox + // dot under RFC 5321/5322/6531. Keep these on the reject path. + // + // Accepting U+3002 when a user enters an email address and replacing it with + // an ASCII dot before sending it on makes sense. But this code cannot do that, + // it has to simply reject an address containing the dot. + private static final String[] UASG_BAD = { + "info5@普遍接受-测试。世界", + "测试5@普遍接受-测试。世界", + }; + + @Parameters(name = "{index}: {0}") + public static Collection data() { + List rows = new ArrayList<>( + GOOD.length + BAD.length + UASG_GOOD.length + UASG_BAD.length); + for (String a : GOOD) { + rows.add(new Object[]{a, true}); + } + for (String a : UASG_GOOD) { + rows.add(new Object[]{a, true}); + } + for (String a : BAD) { + rows.add(new Object[]{a, false}); + } + for (String a : UASG_BAD) { + rows.add(new Object[]{a, false}); + } + return rows; + } + + @Test + public void parse() { + if (good) { + try { + InternetAddress ia = new InternetAddress(address); + assertNotNull(ia); + } catch (AddressException e) { + fail("expected " + address + " to parse, got: " + e.getMessage()); + } + } else { + try { + new InternetAddress(address); + fail("expected " + address + " to be rejected"); + } catch (AddressException e) { + // expected + } + } + } +}