diff --git a/README.md b/README.md index 3772869..2861631 100644 --- a/README.md +++ b/README.md @@ -193,10 +193,10 @@ JMail.tryParse("test@example.com") ```java // Get a normalized email address without any comments -String normalized = JMail.tryParse("admin(comment)@mysite.org") +Optional normalized = JMail.tryParse("admin(comment)@mysite.org") .map(Email::normalized); -// normalized == "admin@mysite.org" +// normalized == Optional.of("admin@mysite.org") ``` ### Additional Validation Rules @@ -272,7 +272,7 @@ JMail.validator().requireOnlyTopLevelDomains( #### Disallow Obsolete Whitespace -Whitespace (spaces, newlines, and carraige returns) is by default allowed between dot-separated +Whitespace (spaces, newlines, and carriage returns) is by default allowed between dot-separated parts of the local-part and domain since RFC 822. However, this whitespace is considered [obsolete since RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-4.4). @@ -287,11 +287,16 @@ JMail.validator().disallowObsoleteWhitespace(); You can require that your `EmailValidator` reject all email addresses that do not have a valid MX record associated with the domain. -> **Please note that since this rule looks up DNS records, including this rule on your email validator can significantly increase the -amount of time it takes to validate email addresses.** +> **Please note that including this rule on your email validator can increase the +amount of time it takes to validate email addresses by approximately 600ms in the worst case. +To further control the amount of time spent doing DNS lookups, you can use the overloaded method +to customize the timeout and retries.** ```java JMail.validator().requireValidMXRecord(); + +// Or, customize the timeout and retries +JMail.validator().requireValidMXRecord(50, 2); ``` ### Bonus: IP Address Validation diff --git a/src/main/java/com/sanctionco/jmail/EmailValidator.java b/src/main/java/com/sanctionco/jmail/EmailValidator.java index 0157265..5cfda52 100644 --- a/src/main/java/com/sanctionco/jmail/EmailValidator.java +++ b/src/main/java/com/sanctionco/jmail/EmailValidator.java @@ -196,8 +196,10 @@ public EmailValidator disallowObsoleteWhitespace() { * {@link ValidationRules#requireValidMXRecord(Email)} rule. * Email addresses that have a domain without a valid MX record will fail validation. * - *

NOTE: Adding this rule to your EmailValidator may significantly increase - * the amount of time it takes to validate email addresses. + *

NOTE: Adding this rule to your EmailValidator may increase + * the amount of time it takes to validate email addresses, as the default initial timeout is + * 100ms and the number of retries using exponential backoff is 2. + * Use {@link #requireValidMXRecord(int, int)} to customize the timeout and retries. * * @return the new {@code EmailValidator} instance */ @@ -205,6 +207,25 @@ public EmailValidator requireValidMXRecord() { return withRule(REQUIRE_VALID_MX_RECORD_PREDICATE); } + /** + * Create a new {@code EmailValidator} with all rules from the current instance and the + * {@link ValidationRules#requireValidMXRecord(Email, int, int)} rule. + * Email addresses that have a domain without a valid MX record will fail validation. + * + *

This method allows you to customize the timeout and retries for performing DNS lookups. + * The initial timeout is supplied in milliseconds, and the number of retries indicate how many + * times to retry the lookup using exponential backoff. Each successive retry will use a + * timeout that is twice as long as the previous try. + * + * @param initialTimeout the timeout in milliseconds for the initial DNS lookup + * @param numRetries the number of retries to perform using exponential backoff + * @return the new {@code EmailValidator} instance + */ + public EmailValidator requireValidMXRecord(int initialTimeout, int numRetries) { + return withRule(email -> + ValidationRules.requireValidMXRecord(email, initialTimeout, numRetries)); + } + /** * Return true if the given email address is valid according to all registered validation rules, * or false otherwise. See {@link JMail#tryParse(String)} for details on the basic diff --git a/src/main/java/com/sanctionco/jmail/ValidationRules.java b/src/main/java/com/sanctionco/jmail/ValidationRules.java index c33a6c5..51ae4b3 100644 --- a/src/main/java/com/sanctionco/jmail/ValidationRules.java +++ b/src/main/java/com/sanctionco/jmail/ValidationRules.java @@ -148,4 +148,16 @@ public static boolean disallowReservedDomains(Email email) { public static boolean requireValidMXRecord(Email email) { return DNSLookupUtil.hasMXRecord(email.domainWithoutComments()); } + + /** + * Rejects an email address that does not have a valid MX record for the domain. + * + * @param email the email address to validate + * @param initialTimeout the timeout in milliseconds for the initial DNS lookup + * @param numRetries the number of retries to perform using exponential backoff + * @return true if this email address has a valid MX record, or false if it does not + */ + public static boolean requireValidMXRecord(Email email, int initialTimeout, int numRetries) { + return DNSLookupUtil.hasMXRecord(email.domainWithoutComments(), initialTimeout, numRetries); + } } diff --git a/src/main/java/com/sanctionco/jmail/dns/DNSLookupUtil.java b/src/main/java/com/sanctionco/jmail/dns/DNSLookupUtil.java index 2098c7d..6034a65 100644 --- a/src/main/java/com/sanctionco/jmail/dns/DNSLookupUtil.java +++ b/src/main/java/com/sanctionco/jmail/dns/DNSLookupUtil.java @@ -11,6 +11,8 @@ * Utility class that provides static methods for DNS related operations. */ public final class DNSLookupUtil { + private static final int DEFAULT_INITIAL_TIMEOUT = 100; + private static final int DEFAULT_RETRIES = 2; /** * Private constructor to prevent instantiation. @@ -25,8 +27,22 @@ private DNSLookupUtil() { * @return true if the domain has a valid MX record, or false if it does not */ public static boolean hasMXRecord(String domain) { + return hasMXRecord(domain, DEFAULT_INITIAL_TIMEOUT, DEFAULT_RETRIES); + } + + /** + * Determine if the given domain has a valid MX record. + * + * @param domain the domain whose MX record to check + * @param initialTimeout the timeout in milliseconds for the initial DNS lookup + * @param numRetries the number of retries to perform using exponential backoff + * @return true if the domain has a valid MX record, or false if it does not + */ + public static boolean hasMXRecord(String domain, int initialTimeout, int numRetries) { Hashtable env = new Hashtable<>(); env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + env.put("com.sun.jndi.dns.timeout.initial", String.valueOf(initialTimeout)); + env.put("com.sun.jndi.dns.timeout.retries", String.valueOf(numRetries)); try { DirContext ctx = new InitialDirContext(env); diff --git a/src/test/java/com/sanctionco/jmail/EmailValidatorTest.java b/src/test/java/com/sanctionco/jmail/EmailValidatorTest.java index 6121ef1..d542514 100644 --- a/src/test/java/com/sanctionco/jmail/EmailValidatorTest.java +++ b/src/test/java/com/sanctionco/jmail/EmailValidatorTest.java @@ -195,9 +195,18 @@ void rejectsDomainsWithoutMXRecord(String email) { @ValueSource(strings = { "test@gmail.com", "test@hotmail.com", "test@yahoo.com", "test@utexas.edu", "test@gmail.(comment)com"}) - void allowsDomansWithMXRecord(String email) { + void allowsDomainsWithMXRecord(String email) { runValidTest(JMail.validator().requireValidMXRecord(), email); } + + @Test + void correctlyCustomizesTimeoutAndRetries() { + long startTime = System.currentTimeMillis(); + runInvalidTest(JMail.validator().requireValidMXRecord(10, 1), "test@coolio.com"); + long endTime = System.currentTimeMillis(); + + assertThat(endTime - startTime).isLessThan(500); + } } @Nested diff --git a/src/test/java/com/sanctionco/jmail/dns/DNSLookupUtilTest.java b/src/test/java/com/sanctionco/jmail/dns/DNSLookupUtilTest.java index f17da5f..0ccb044 100644 --- a/src/test/java/com/sanctionco/jmail/dns/DNSLookupUtilTest.java +++ b/src/test/java/com/sanctionco/jmail/dns/DNSLookupUtilTest.java @@ -19,4 +19,13 @@ void failsToFindInvalidMXRecord() { assertThat(DNSLookupUtil.hasMXRecord("a.com")).isFalse(); assertThat(DNSLookupUtil.hasMXRecord("whatis.hello")).isFalse(); } + + @Test + void customTimeoutWorksAsExpected() { + long startTime = System.currentTimeMillis(); + assertThat(DNSLookupUtil.hasMXRecord("coolio.com", 10, 1)).isFalse(); + long endTime = System.currentTimeMillis(); + + assertThat(endTime - startTime).isLessThan(100); + } }