diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fd57892..2729ee78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,15 @@
CHANGELOG
=========
+3.8.0
+------------------
+
+* `commons-validator` has been removed as a dependency. This module now does
+ its own email and domain name validation. This was done to reduce the number
+ of dependencies and any security vulnerabilities in them. The new email
+ validation of the local part is somewhat more lax than the previous
+ validation.
+
3.7.2 (2025-05-28)
------------------
diff --git a/pom.xml b/pom.xml
index 43023b6a..be0692fb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,11 +62,6 @@
geoip2
4.3.1
-
- commons-validator
- commons-validator
- 1.9.0
-
org.junit.jupiter
junit-jupiter
diff --git a/src/main/java/com/maxmind/minfraud/request/Email.java b/src/main/java/com/maxmind/minfraud/request/Email.java
index f0b5f1dc..e4f94e9e 100644
--- a/src/main/java/com/maxmind/minfraud/request/Email.java
+++ b/src/main/java/com/maxmind/minfraud/request/Email.java
@@ -13,8 +13,6 @@
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
-import org.apache.commons.validator.routines.DomainValidator;
-import org.apache.commons.validator.routines.EmailValidator;
/**
* The email information for the transaction.
@@ -28,6 +26,9 @@ public final class Email extends AbstractModel {
private static final Map equivalentDomains;
private static final Map fastmailDomains;
private static final Map yahooDomains;
+ private static final Pattern DOMAIN_LABEL_PATTERN = Pattern.compile(
+ "^[a-zA-Z0-9]$|^[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9]$"
+ );
private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
private static final Pattern TRAILING_DOT_PATTERN = Pattern.compile("\\.+$");
private static final Pattern REPEAT_COM_PATTERN = Pattern.compile("(?:\\.com){2,}$");
@@ -338,7 +339,7 @@ public Builder(boolean enableValidation) {
* @throws IllegalArgumentException when address is not a valid email address.
*/
public Email.Builder address(String address) {
- if (enableValidation && !EmailValidator.getInstance().isValid(address)) {
+ if (enableValidation && !isValidEmail(address)) {
throw new IllegalArgumentException(
"The email address " + address + " is not valid.");
}
@@ -373,7 +374,7 @@ public Email.Builder hashAddress() {
* @throws IllegalArgumentException when domain is not a valid domain.
*/
public Email.Builder domain(String domain) {
- if (enableValidation && !DomainValidator.getInstance().isValid(domain)) {
+ if (enableValidation && !isValidDomain(domain)) {
throw new IllegalArgumentException("The email domain " + domain + " is not valid.");
}
this.domain = domain;
@@ -458,6 +459,34 @@ private String cleanAddress(String address) {
return localPart + "@" + domain;
}
+ private static boolean isValidEmail(String email) {
+ if (email == null || email.isEmpty()) {
+ return false;
+ }
+
+ // In RFC 5321, the forward path limits the mailbox to 254 characters
+ // even though a domain can be 255 and the local part 64
+ if (email.length() > 254) {
+ return false;
+ }
+
+ int atIndex = email.lastIndexOf('@');
+ if (atIndex <= 0) {
+ return false;
+ }
+
+ String localPart = email.substring(0, atIndex);
+ String domainPart = email.substring(atIndex + 1);
+
+ // The local-part has a maximum length of 64 characters.
+ if (localPart.length() > 64) {
+ return false;
+ }
+
+ return isValidDomain(domainPart);
+ }
+
+
private String cleanDomain(String domain) {
if (domain == null) {
return null;
@@ -491,6 +520,48 @@ private String cleanDomain(String domain) {
return domain;
}
+ private static boolean isValidDomain(String domain) {
+ if (domain == null || domain.isEmpty()) {
+ return false;
+ }
+
+ try {
+ domain = IDN.toASCII(domain);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+
+ if (domain.endsWith(".")) {
+ domain = domain.substring(0, domain.length() - 1);
+ }
+
+ if (domain.length() > 255) {
+ return false;
+ }
+
+ String[] labels = domain.split("\\.");
+
+ if (labels.length < 2) {
+ return false;
+ }
+
+ for (String label : labels) {
+ if (!isValidDomainLabel(label)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean isValidDomainLabel(String label) {
+ if (label == null || label.isEmpty() || label.length() > 63) {
+ return false;
+ }
+
+ return DOMAIN_LABEL_PATTERN.matcher(label).matches();
+ }
+
/**
* @return The domain of the email address used in the transaction.
*/
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index dfc2ebf6..0d7600fb 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -7,7 +7,6 @@
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.datatype.jsr310;
requires transitive com.maxmind.geoip2;
- requires org.apache.commons.validator;
requires java.net.http;
exports com.maxmind.minfraud;
diff --git a/src/test/java/com/maxmind/minfraud/request/EmailTest.java b/src/test/java/com/maxmind/minfraud/request/EmailTest.java
index 4025f1a3..687c2519 100644
--- a/src/test/java/com/maxmind/minfraud/request/EmailTest.java
+++ b/src/test/java/com/maxmind/minfraud/request/EmailTest.java
@@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+
import com.maxmind.minfraud.request.Email.Builder;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
@@ -12,6 +13,8 @@
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
public class EmailTest {
@@ -242,11 +245,26 @@ private String toMD5(String s) throws NoSuchAlgorithmException {
return String.format("%032x", i);
}
- @Test
- public void testInvalidAddress() {
+ @ParameterizedTest(name = "Run #{index}: email = \"{0}\"")
+ @ValueSource(strings = {
+ "test.com",
+ "test@",
+ "@test.com",
+ "",
+ " ",
+ "test@test com.com",
+ "test@test_domain.com",
+ "test@-test.com",
+ "test@test-.com",
+ "test@.test.com",
+ "test@test..com",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@test.com",
+ "test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com"
+ })
+ void testInvalidAddresses(String invalidAddress) {
assertThrows(
IllegalArgumentException.class,
- () -> new Builder().address("a@test@test.org").build()
+ () -> new Builder().address(invalidAddress).build()
);
}
@@ -264,11 +282,32 @@ public void testDomainWithoutValidation() {
assertEquals(domain, email.getDomain());
}
- @Test
- public void testInvalidDomain() {
+ @ParameterizedTest(name = "Run #{index}: domain = \"{0}\"")
+ @ValueSource(strings = {
+ "example",
+ "",
+ " ",
+ " domain.com",
+ "domain.com ",
+ "domain com.com",
+ "domain_name.com",
+ "domain$.com",
+ "-domain.com",
+ "domain-.com",
+ "domain..com",
+ ".domain.com",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" +
+ ".a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" +
+ ".a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" +
+ ".a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" +
+ ".com",
+ "xn--.com"
+ })
+ void testInvalidDomains(String invalidDomain) {
assertThrows(
IllegalArgumentException.class,
- () -> new Builder().domain(" domain.com").build()
+ () -> new Builder().domain(invalidDomain).build()
);
}
}