Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions core/src/main/java/org/apache/james/core/Domain.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.apache.james.core;

import java.io.Serializable;
import java.net.IDN;
import java.util.Locale;
import java.util.Objects;

Expand Down Expand Up @@ -54,9 +55,16 @@ public static Domain of(String domain) {
Preconditions.checkArgument(domain.length() <= MAXIMUM_DOMAIN_LENGTH,
"Domain name length should not exceed %s characters", MAXIMUM_DOMAIN_LENGTH);

String domainWithoutBrackets = removeBrackets(domain);
String domainWithoutBrackets = IDN.toASCII(removeBrackets(domain), IDN.ALLOW_UNASSIGNED);
Preconditions.checkArgument(PART_CHAR_MATCHER.matchesAllOf(domainWithoutBrackets),
"Domain parts ASCII chars must be a-z A-Z 0-9 - or _ in %s", domain);
"Domain parts ASCII chars must be a-z A-Z 0-9 - or _ in %s", domain);

if (domainWithoutBrackets.startsWith("xn--") ||
domainWithoutBrackets.contains(".xn--")) {
domainWithoutBrackets = IDN.toUnicode(domainWithoutBrackets);
Preconditions.checkArgument(!domainWithoutBrackets.startsWith("xn--") &&
!domainWithoutBrackets.contains(".xn--"));
}

int pos = 0;
int nextDot = domainWithoutBrackets.indexOf('.');
Expand Down
28 changes: 24 additions & 4 deletions core/src/main/java/org/apache/james/core/MailAddress.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.apache.james.core;

import java.net.IDN;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -418,7 +419,7 @@ public Optional<InternetAddress> toInternetAddress() {
try {
return Optional.of(new InternetAddress(toString()));
} catch (AddressException ae) {
LOGGER.warn("A valid address '{}' as per James criterial fails to parse as a jakarta.mail InternetAdrress", asString());
LOGGER.warn("A valid address '{}' as per James criteria fails to parse as a jakarta.mail InternetAdrress", asString());
return Optional.empty();
}
}
Expand Down Expand Up @@ -549,15 +550,15 @@ private int parseUnquotedLocalPart(StringBuilder lpSB, String address, int pos)
//End of local-part
break;
} else {
//<c> ::= any one of the 128 ASCII characters, but not any
// <special> or <SP>
//<c> ::= any printable ASCII character, or any non-ASCII
// unicode codepoint, but not <special> or <SP>
//<special> ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "."
// | "," | ";" | ":" | "@" """ | the control
// characters (ASCII codes 0 through 31 inclusive and
// 127)
//<SP> ::= the space character (ASCII code 32)
char c = address.charAt(pos);
if (c <= 31 || c >= 127 || c == ' ') {
if (c <= 31 || c == 127 || c == ' ') {
throw new AddressException("Invalid character in local-part (user account) at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
Expand Down Expand Up @@ -688,6 +689,7 @@ private int parseDomain(StringBuilder dSB, String address, int pos) throws Addre
// in practice though, we should relax this as domain names can start
// with digits as well as letters. So only check that doesn't start
// or end with hyphen.
boolean unicode = false;
while (true) {
if (pos >= address.length()) {
break;
Expand All @@ -700,13 +702,31 @@ private int parseDomain(StringBuilder dSB, String address, int pos) throws Addre
resultSB.append(ch);
pos++;
continue;
} else if (ch >= 0x0080) {
resultSB.append(ch);
pos++;
unicode = true;
continue;
}
if (ch == '.') {
break;
}
throw new AddressException("Invalid character at " + pos + " in '" + address + "'", address, pos);
}
String result = resultSB.toString();
if (unicode) {
try {
result = IDN.toASCII(result, IDN.ALLOW_UNASSIGNED);
} catch (IllegalArgumentException e) {
throw new AddressException("Domain invalid according to IDNA", address);
}
}
if (result.startsWith("xn--") || result.contains(".xn--")) {
result = IDN.toUnicode(result);
if (result.startsWith("xn--") || result.contains(".xn--")) {
throw new AddressException("Domain invalid according to IDNA", address);
}
}
if (result.startsWith("-") || result.endsWith("-")) {
throw new AddressException("Domain name cannot begin or end with a hyphen \"-\" at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
Expand Down
81 changes: 81 additions & 0 deletions core/src/test/java/org/apache/james/core/DomainTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/****************************************************************
* 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.core;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;


class DomainTest {
@Test
void testPlainDomain() {
Domain d1 = Domain.of("example.com");
assertThat(d1.name().equals(d1.asString()));
Domain d2 = Domain.of("Example.com");
assertThat(d2.name()).isNotEqualTo(d2.asString());
assertThat(d1.asString()).isEqualTo(d2.asString());
}

@Test
void testIPv4Domain() {
Domain d1 = Domain.of("192.0.4.1");
assertThat(d1.asString()).isEqualTo("192.0.4.1");
}

@Test
void testPunycodeIDN() {
Domain d1 = Domain.of("xn--gr-zia.example");
assertThat(d1.asString()).isEqualTo("grå.example");
}

@Test
void testDevanagariDomain() {
Domain d1 = Domain.of("डाटामेल.भारत");
assertThat(d1.asString()).isEqualTo(d1.name());
}

private static Stream<Arguments> malformedDomains() {
return Stream.of(
"😊☺️.example", // emoji not permitted by IDNA
"#.example", // really and truly not permitted
"\uFEFF.example", // U+FEFF is the byte order mark
"\u200C.example", // U+200C is a zero-width non-joiner
"\u200Eibm.example" // U+200E is left-to-right
)
.map(Arguments::of);
}

@ParameterizedTest
@MethodSource("malformedDomains")
void testMalformedDomains(String malformed) {
assertThatThrownBy(() -> Domain.of(malformed))
.as("rejecting malformed domain " + malformed)
.isInstanceOf(IllegalArgumentException.class);
}
}


34 changes: 25 additions & 9 deletions core/src/test/java/org/apache/james/core/MailAddressTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

import java.util.Properties;
import java.util.stream.Stream;

import jakarta.mail.Session;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
Expand Down Expand Up @@ -55,6 +58,13 @@ private static Stream<Arguments> goodAddresses() {
"\\[email protected]",
"[email protected]",
"[email protected]",
"Loïc.Accentué@voilà.fr8",
"pelé@exemple.com",
"δοκιμή@παράδειγμα.δοκιμή",
"我買@屋企.香港",
"二ノ宮@黒川.日本",
"медведь@с-балалайкой.рф",
//"संपर्क@डाटामेल.भारत", fails in Jakarta, reason still unknown
"user+mailbox/[email protected]",
"[email protected]",
"\"Abc@def\"@example.com",
Expand Down Expand Up @@ -96,40 +106,46 @@ private static Stream<Arguments> badAddresses() {
"server-dev@[127.0.1.1.1]",
"server-dev@[127.0.1.-1]",
"test@dom+ain.com",
"[email protected]",
"\"a..b\"@domain.com", // jakarta.mail is unable to handle this so we better reject it
"server-dev\\[email protected]", // jakarta.mail is unable to handle this so we better reject it
"[email protected]",
// According to wikipedia these addresses are valid but as jakarta.mail is unable
// to work with them we shall rather reject them (note that this is not breaking retro-compatibility)
"Loïc.Accentué@voilà.fr8",
"pelé@exemple.com",
"δοκιμή@παράδειγμα.δοκιμή",
"我買@屋企.香港",
"二ノ宮@黒川.日本",
"медведь@с-балалайкой.рф",
"संपर्क@डाटामेल.भारत",
"sales@\u200Eibm.example", // U+200E is left-to-right
// According to wikipedia this address is valid but as jakarta.mail is unable
// to work with it we shall rather reject them (note that this is not breaking retro-compatibility)
"mail.allow\\,[email protected]")
.map(Arguments::of);
}

@BeforeEach
void setup() {
Properties props = new Properties();
props.setProperty("mail.mime.allowutf8", "true");
Session s = Session.getDefaultInstance(props);
assertThat(Boolean.parseBoolean(s.getProperties().getProperty("mail.mime.allowutf8", "false")));
}

@ParameterizedTest
@MethodSource("goodAddresses")
void testGoodMailAddressString(String mailAddress) {
assertThatCode(() -> new MailAddress(mailAddress))
.as("parses " + mailAddress)
.doesNotThrowAnyException();
}

@ParameterizedTest
@MethodSource("goodAddresses")
void toInternetAddressShouldNoop(String mailAddress) throws Exception {
assertThat(new MailAddress(mailAddress).toInternetAddress())
.as("tries to parse " + mailAddress + " using jakarta.mail")
.isNotEmpty();
}

@ParameterizedTest
@MethodSource("badAddresses")
void testBadMailAddressString(String mailAddress) {
Assertions.assertThatThrownBy(() -> new MailAddress(mailAddress))
.as("fails to parse " + mailAddress)
.isInstanceOf(AddressException.class);
}

Expand Down
45 changes: 45 additions & 0 deletions mailbox/opensearch/src/test/resources/eml/cve-2024-23184.eml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
MIME-Version: 1.0
Subject: Test
From: Benoit TELLIER <[email protected]>
To: Benoit TELLIER <[email protected]>
Date: Tue, 13 Feb 2024 23:01:18 +0000
Message-ID: <[email protected]>
Content-Type: multipart/mixed;
boundary="-=Part.17f.732e3d28e1c76db4.18da4b40791.62ef5e3fa995057d=-"

---=Part.17f.732e3d28e1c76db4.18da4b40791.62ef5e3fa995057d=-
Content-Type: multipart/alternative;
boundary="-=Part.17e.48ac92d73c356567.18da4b40791.360a293e2f389efe=-"

---=Part.17e.48ac92d73c356567.18da4b40791.360a293e2f389efe=-
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

Test

---=Part.17e.48ac92d73c356567.18da4b40791.360a293e2f389efe=-
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

<div>Test<br><br></div>

---=Part.17e.48ac92d73c356567.18da4b40791.360a293e2f389efe=---

---=Part.17f.732e3d28e1c76db4.18da4b40791.62ef5e3fa995057d=-
Content-Type: application/json; name="=?US-ASCII?Q?id=5Frsa.txt?="
Content-Disposition: attachment
Content-Transfer-Encoding: base64

c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFDQVFDa0dXMkp5c2lKR2hQZXdBOXRr
bVFFQm5EVjRaQ0llLy92ZFoyV0RybnZiNlZLQzdpWldjODFpU1ZkTFcxUkRBTll4c3ExN0dQanpV
OFlWdk9sRkFJSk1WTm9ESWhuQWtYOU9VUUJpd1hpOHlHZ3FLNGR0RmIxczJBRzNrQmxNUFFJOE5K
MkpLT2Z5MW51VnJubEtoVDlCVnpYMm5iSjNOak9PZlkxQlJEaDZZcVl1a2RuejBUT2k1Rkp1YUJT
NDZQemx3eWdIa0dzeXBLVHM2Y2FUNjBRdjl3eWFadm4yenN1RmNML3o2Mmd3aGZyZGFsakF1UGRX
cERlNG1IRVFmMXA2SXNRMDdPb0lwTmRHQ0tLZHRZQlVTcktzTXRpMllLUGZpSzB2WGU1L3owRWJE
VlRja1BrY3NwQ2cwYVZuZTB2eFVsRGt2U2pwV2tiQkZ0YTk5ekJjOVlJL0ROK28vRmtONlFTdXV5
U29tNDZkamZpUjdqSzNMRmJKUkhaem9BblNvaTZvRlR0MW1LWjNzam44bnZWUG1PV3pJWHY0Tm1O
R1ExZHFrV1hXcUtyQjlIZUZiQnRPWVAzaEkxQ0kvaVhNbVR1SkdvcHVTUmlTNW1QZXlSQWV6VGtk
UG8vZ2NSVWNzbklhVW1EallUWHBFNzU3Yk5LWVNHbFJsS3FrbEhKc2JveEdTK0NaVzBJS2dZeTdG
cmZRZ1FGMTdvaUpWM1JJQ1VHcU9rM1I2VnZOYlhlL2VmZS9IT24xd0lZUS9qVGRzY0hCamRIM2FF
MmY4Y3dVS1IzNUtWNlJ1SE4vYVpiekxiVkJxUEMvUTcwd3NMQlloV29Da1dRMElUUmxGV2N3bnN3
VTE5NnlGWkVHSmthOUNEaHZQdUVBV0NLWnFRT3gyMnRoYWVSQlE9PSBiZW53YUBob3Jpem9uCg==
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@
<james.groupId>org.apache.james</james.groupId>
<james.protocols.groupId>${james.groupId}.protocols</james.protocols.groupId>
<activemq.version>6.1.6</activemq.version>
<apache-mime4j.version>0.8.12</apache-mime4j.version>
<apache-mime4j.version>0.8.13-SNAPSHOT</apache-mime4j.version>
<apache.openjpa.version>4.0.0</apache.openjpa.version>
<derby.version>10.14.2.0</derby.version>
<log4j2.version>2.23.1</log4j2.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@

package org.apache.james.protocols.api;

import static java.nio.charset.StandardCharsets.US_ASCII;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -216,7 +215,7 @@ public <T> Optional<T> getAttachment(AttachmentKey<T> key, State state) {
*/
@Override
public Charset getCharset() {
return US_ASCII;
return StandardCharsets.UTF_8;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private boolean readNextLine() {
if (!headerLines.hasMoreElements()) {
line += LINE_SEPERATOR;
}
currLine = line.getBytes(StandardCharsets.US_ASCII);
currLine = line.getBytes(StandardCharsets.UTF_8);
return true;
} else {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
* </pre>
*/
public class MailHeaders extends InternetHeaders implements Serializable, Cloneable {

private static final long serialVersionUID = 238748126601L;
private static final boolean ALLOWUTF_8 = true;
private boolean modified = false;
private long size = -1;

Expand All @@ -67,7 +67,7 @@ public MailHeaders() {
*/
public MailHeaders(InputStream in) throws MessagingException {
super();
load(in);
load(in, ALLOWUTF_8);
}

/**
Expand Down
Loading