Skip to content

Commit

Permalink
Allow configurable timeouts for MX record lookup (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
RohanNagar committed May 29, 2023
1 parent 4395fde commit 7bee24d
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 8 deletions.
15 changes: 10 additions & 5 deletions README.md
Expand Up @@ -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<String> normalized = JMail.tryParse("admin(comment)@mysite.org")
.map(Email::normalized);

// normalized == "admin@mysite.org"
// normalized == Optional.of("admin@mysite.org")
```

### Additional Validation Rules
Expand Down Expand Up @@ -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).

Expand All @@ -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
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/com/sanctionco/jmail/EmailValidator.java
Expand Up @@ -196,15 +196,36 @@ public EmailValidator disallowObsoleteWhitespace() {
* {@link ValidationRules#requireValidMXRecord(Email)} rule.
* Email addresses that have a domain without a valid MX record will fail validation.
*
* <p><strong>NOTE: Adding this rule to your EmailValidator may significantly increase
* the amount of time it takes to validate email addresses.</strong>
* <p><strong>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.</strong>
*
* @return the new {@code EmailValidator} instance
*/
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.
*
* <p>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
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/sanctionco/jmail/ValidationRules.java
Expand Up @@ -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);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/sanctionco/jmail/dns/DNSLookupUtil.java
Expand Up @@ -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.
Expand All @@ -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<String, String> 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);
Expand Down
11 changes: 10 additions & 1 deletion src/test/java/com/sanctionco/jmail/EmailValidatorTest.java
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/com/sanctionco/jmail/dns/DNSLookupUtilTest.java
Expand Up @@ -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);
}
}

0 comments on commit 7bee24d

Please sign in to comment.