All posts

How to Validate Email Addresses in Java: Commons Validator, @Email and DNS

··7 min read

To validate an email address in Java, strip the input and check it with Apache Commons Validator's EmailValidator.getInstance().isValid(). Then look up the domain's MX records through JNDI's DNS provider, with explicit timeouts, to confirm it accepts mail. If you use Jakarta Bean Validation's @Email, pair it with @NotBlank, because it accepts empty strings. Don't use InternetAddress as a validator.

Everything below was run on OpenJDK 17 on 6 October 2026 with Commons Validator 1.11.0, Hibernate Validator 9.1.4.Final and Jakarta Mail API 2.1.5. The tables are the actual output.

Three ways Java developers validate email

They answer different questions: Commons Validator checks a format, @Email checks a format the Bean Validation way, and InternetAddress parses a header.

<dependency>
  <groupId>commons-validator</groupId>
  <artifactId>commons-validator</artifactId>
  <version>1.11.0</version>
</dependency>
// 1. Apache Commons Validator
boolean ok = EmailValidator.getInstance().isValid(email);

// 2. Jakarta Bean Validation (Hibernate Validator is the reference implementation)
record Signup(@NotBlank @Email String email) {}

// 3. Jakarta Mail, which is a parser, not a validator
new InternetAddress(email, true).validate(); // throws AddressException

The Commons Validator Javadoc offers getInstance(), getInstance(boolean allowLocal) and getInstance(boolean allowLocal, boolean allowTld), and is candid about its limits: "This implementation is not guaranteed to catch all possible errors in an email address." The Jakarta Mail Javadoc says validate() "checks many, but not all, syntax rules" of RFC 822.

What each one accepted

Commons Validator was the strictest and the closest to what a signup form needs. InternetAddress accepted almost anything with an @ in it.

Input Commons EmailValidator Hibernate @Email InternetAddress strict
jane+news@example.com valid valid valid
o'brien@example.ie valid valid valid
user@münchen.de valid valid valid
用户@例子.广告 invalid valid valid
"john doe"@example.com valid valid valid
john..doe@example.com invalid invalid invalid
.john@example.com invalid invalid invalid
john@-example.com invalid invalid valid
65-character local part invalid invalid valid
john@example (no TLD) invalid valid valid
jane@example.invalidtld invalid valid valid
jane@[192.168.0.1] valid valid valid
jane@example.com (leading space) invalid invalid valid, parsed as jane@example.com
Jane Doe <jane@example.com> invalid not tested valid, parsed as jane@example.com
jane\doe@example.com valid invalid not tested
empty string invalid valid not tested
null invalid valid not tested

What stands out:

  • @Email passes null and "". Bean Validation constraints each check one thing, so @Email leaves emptiness to @NotNull or @NotBlank. A form field annotated only with @Email accepts a blank submission.
  • @Email accepts john@example. A hostname with no dot is valid syntax, but nobody on the public internet has that address.
  • InternetAddress parses display names and whitespace. "Jane Doe <jane@example.com>" is a valid RFC 822 address header, which is what the class is for. If you use it as a validator, store getAddress(), not the raw input.
  • Commons Validator checks TLDs against a list built into the jar. That's why example.invalidtld fails, and it also caught the typo gmial.con. The catch is that a TLD added after the release fails until you upgrade.
  • Commons accepted an unquoted backslash. jane\doe@example.com isn't valid without quotes (why). The function below rejects it explicitly.

Checking the domain: MX lookup with JNDI

The JDK can query DNS directly through JNDI. Set the timeouts yourself, because the defaults can stall a request for half a minute.

The JDK DNS provider starts with a 1,000 ms timeout and, if no server responds, "doubles the timeout period and repeats the process", up to 4 retries by default. With one DNS server configured, that's 1 + 2 + 4 + 8 + 16 = 31 seconds before it gives up. The code below uses one retry, about 3 seconds in the worst case.

This class combines both layers and returns a status instead of a boolean:

import java.net.IDN;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import org.apache.commons.validator.routines.EmailValidator;

public final class EmailCheck {

  public enum Status { INVALID, NO_MAIL, DELIVERABLE_DOMAIN, UNKNOWN }

  public record Result(Status status, String reason) {}

  private static final EmailValidator SYNTAX = EmailValidator.getInstance();

  public static Result check(String raw) {
    if (raw == null) return new Result(Status.INVALID, "empty");
    String email = raw.strip();
    if (!SYNTAX.isValid(email)) return new Result(Status.INVALID, "syntax");
    if (email.indexOf('"') >= 0 || email.indexOf('\\') >= 0) {
      return new Result(Status.INVALID, "quoted or escaped local parts not accepted");
    }

    String domain = email.substring(email.lastIndexOf('@') + 1);
    if (domain.startsWith("[")) return new Result(Status.INVALID, "address literals not accepted");

    String ascii;
    try {
      ascii = IDN.toASCII(domain, IDN.USE_STD3_ASCII_RULES).toLowerCase(Locale.ROOT);
    } catch (IllegalArgumentException e) {
      return new Result(Status.INVALID, "domain is not a valid IDN");
    }

    try {
      List<String> mx = lookup(ascii, "MX");
      if (mx.size() == 1 && mx.get(0).matches("\\d+ \\.?")) {
        return new Result(Status.NO_MAIL, "null MX (RFC 7505)");
      }
      if (!mx.isEmpty()) {
        mx.sort(Comparator.comparingInt(r -> Integer.parseInt(r.split(" ")[0])));
        return new Result(Status.DELIVERABLE_DOMAIN, mx.get(0));
      }
      if (!lookup(ascii, "A").isEmpty() || !lookup(ascii, "AAAA").isEmpty()) {
        return new Result(Status.DELIVERABLE_DOMAIN, "implicit MX (A/AAAA only)");
      }
      return new Result(Status.NO_MAIL, "no MX, A or AAAA records");
    } catch (NameNotFoundException e) {
      return new Result(Status.NO_MAIL, "domain does not exist");
    } catch (NamingException e) {
      return new Result(Status.UNKNOWN, "DNS error: " + e.getClass().getSimpleName());
    }
  }

  private static List<String> lookup(String domain, String type) throws NamingException {
    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", "1000");
    env.put("com.sun.jndi.dns.timeout.retries", "1");
    DirContext ctx = new InitialDirContext(env);
    try {
      Attributes attrs = ctx.getAttributes(domain, new String[] {type});
      Attribute attr = attrs.get(type);
      List<String> values = new ArrayList<>();
      if (attr != null) {
        for (int i = 0; i < attr.size(); i++) values.add(attr.get(i).toString());
      }
      return values;
    } finally {
      ctx.close();
    }
  }
}

Results from our run:

Input Status Reason
jane@gmail.com DELIVERABLE_DOMAIN 5 gmail-smtp-in.l.google.com.
jane@@gmail.com INVALID syntax
jane@gmial.con INVALID syntax (unknown TLD)
jane@example.com NO_MAIL null MX (RFC 7505)
jane@nonexistent-domain-sv-test-9a8b7c.com NO_MAIL domain does not exist
user@münchen.de DELIVERABLE_DOMAIN implicit MX (A/AAAA only)
jane@[192.168.0.1] INVALID address literals not accepted
"john doe"@example.com INVALID quoted or escaped local parts not accepted

The decisions behind it:

  1. strip() first. Commons rejects a leading space, and pasted addresses often have one. strip() handles Unicode whitespace, which trim() doesn't.
  2. Null MX means no mail. RFC 7505 lets a domain publish 0 . to refuse all mail. JNDI returned it as the string 0 . for example.com.
  3. No MX but an A or AAAA record still passes. That's the implicit MX in RFC 5321 section 5.1.
  4. The domain is converted with IDN.toASCII before the lookup, so münchen.de is queried as xn--mnchen-3ya.de. More on that in internationalized email addresses.
  5. DNS errors other than "name not found" are UNKNOWN. A resolver timeout says nothing about the user. Let them through and re-check later.
  6. MX records are sorted by preference. DNS doesn't guarantee any order, so sort before reporting or using the primary host.

In a Spring or Jakarta EE form

Annotate for the quick check, and run the DNS check in the service layer where you can control timeouts.

public record SignupRequest(@NotBlank @Email @Size(max = 254) String email) {}

Then call EmailCheck.check(request.email()) in the service before creating the account. You could wrap the DNS lookup in a custom ConstraintValidator, but a network call hidden inside an annotation is easy to forget about. It runs on every validation, including in tests, and its failures surface as validation errors rather than as "DNS was down". Keep it explicit.

Cache results per domain for a few hours. Busy sign-up forms see the same handful of domains over and over, so a small in-memory cache saves most lookups.

What this doesn't tell you

A deliverable domain doesn't mean the mailbox exists.

asdfghjk@gmail.com passes both layers. Confirming the mailbox takes an SMTP RCPT TO conversation. That needs outbound port 25, which most cloud providers block, so it's usually done through a verification service. If you call one from Java, set HttpClient connect and request timeouts, and treat a timeout as UNKNOWN. The same layers in other languages are covered in validating email in JavaScript and validating email in Python.

The takeaway

  • Use Commons Validator's EmailValidator for syntax, after strip(). Upgrade it regularly, because of its built-in TLD list.
  • If you use @Email, add @NotBlank. On its own it accepts empty strings, null and dotless domains.
  • Use InternetAddress to parse headers, not to validate signups.
  • Look up MX with JNDI, handle null MX and the A-record fallback, and set timeout.initial and timeout.retries explicitly.
  • To see what a full mailbox-level check returns for one address, try the free email verifier.

Common questions

What is the best way to validate an email address in Java?

Use Apache Commons Validator's EmailValidator for syntax, strip whitespace first, and follow it with an MX lookup through JNDI's DNS provider to confirm the domain accepts mail. Avoid using Jakarta Mail's InternetAddress as a validator: it's a parser, and it accepted inputs like 'Jane Doe <jane@example.com>' and 65-character local parts in our test.

Does @Email in Jakarta Bean Validation reject empty strings?

No. With Hibernate Validator 9.1.4, @Email treated both null and the empty string as valid. That's how Bean Validation constraints generally work: each checks one thing. Combine @Email with @NotBlank, or @NotNull, when the field is required.

How do I check MX records in Java without a library?

Use JNDI with the DNS provider: create an InitialDirContext with java.naming.factory.initial set to com.sun.jndi.dns.DnsContextFactory and call getAttributes(domain, new String[] {"MX"}). Set com.sun.jndi.dns.timeout.initial and timeout.retries explicitly. The defaults (1,000 ms, doubled across 4 retries) can make one lookup wait about 31 seconds.

Does Apache Commons EmailValidator check that the domain exists?

No. It checks the format and that the top-level domain is on the list of TLDs built into that release, which is why it rejected gmial.con. It doesn't look up DNS, so jane@nonexistent-domain-12345.com passes. You need a separate MX lookup for that.

Check your list before you send

Real mailbox checks over SMTP, and nothing is sent to the address. Sign in with Google and your first 100 email checks are free, no card. Plans for bigger lists are on the pricing page.