Tutorial · Java & Spring Boot

Java email validation: from regex to mailbox check

Four checks with working Java 21 code: a format regex, three common libraries and where they disagree, an MX lookup with nothing but the JDK, and a mailbox check in a Spring Boot controller.

Updated October 10, 2026 · 11 min read

Java email validation: Hibernate’s @Email accepts jane@example and jane@localhost, Commons EmailValidator is stricter, and only the regex rejects every bad address

Java email validation has no single built-in answer: Jakarta Bean Validation’s @Email leaves the rules to the provider, Apache Commons Validator has its own EmailValidator, Jakarta Mail parses addresses with InternetAddress, and none of them knows whether a mailbox exists. This guide compares them on the same test addresses, adds a regex that rejects what they miss, and finishes with an MX lookup and a mailbox check in Spring Boot. All code was tested on Java 21.

1. Java email validation with a regex

This is the tested pattern from our email regex guide:

import java.util.regex.Pattern;

public final class EmailFormat {
    public static final String REGEX =
        "(?=.{1,254}\\z)(?=.{1,64}@)"
        + "[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*"
        + "@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+"
        + "(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})";

    private static final Pattern PATTERN = Pattern.compile(REGEX);

    public static boolean isValid(String value) {
        return value != null && PATTERN.matcher(value).matches();
    }
}

matches() requires the whole string to match, so the pattern needs no ^ and $. It enforces the length limits (64 characters before the @, 254 in total), allows plus tags and apostrophes, and rejects edge dots, double dots and domains without a top-level domain; the rules are explained in the guide to the email address format. REGEX is a compile-time constant on purpose: section 5 reuses it inside an annotation.

Before matching, strip() the input, and convert internationalized domains to punycode: java.net.IDN.toASCII("bücher.example.com") returns xn--bcher-kva.example.com, which the pattern accepts.

2. Java email address validator libraries

Jakarta Bean Validation @Email

The @Email constraint says only that the string “has to be a well-formed email address” and that the “exact semantics of what makes up a valid email address are left to Jakarta Validation providers”. null counts as valid. In practice the provider is Hibernate Validator, which Spring Boot pulls in with spring-boot-starter-validation:

public record Contact(@NotBlank @Email String email) {}

Hibernate Validator’s implementation splits at the last @, checks the local part (at most 64 characters, quoted strings allowed) and checks the domain as a host name or IP address. It doesn’t require a dot in the domain, and an empty string passes, which is why @NotBlank belongs next to it.

The Hibernate Validator reference documents the fix: the optional regexp (and flags) parameters add a regular expression the value must also match. Section 5 uses that with the pattern above.

Apache Commons Validator

EmailValidator from commons-validator:commons-validator (1.11.0, checked October 2026) is a standalone check:

import org.apache.commons.validator.routines.EmailValidator;

boolean ok = EmailValidator.getInstance().isValid(email);

Its own documentation says it “is not guaranteed to catch all possible errors in an email address”. What the source shows: it rejects any address ending in a dot, accepts quoted local parts and bracketed IP addresses, and validates the top-level domain against a list of IANA top-level domains compiled into the library. So jane@example is rejected, and a top-level domain added after your library version would be too. getInstance(true) (allow local addresses) accepted jane@example and jane@localhost in our test. Unlike @Email, it treats null as invalid.

Jakarta Mail InternetAddress

If your project already sends mail with Jakarta Mail (jakarta.mail:jakarta.mail-api, 2.1.5 in our test), InternetAddress can parse an address. Like .NET’s MailAddress, it parses the address header syntax, so it also takes Jane Doe <jane@example.com>. Its API documentation says the syntax check “checks many, but not all, syntax rules” of RFC 822:

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

public final class MailAddressCheck {
    public static boolean isValid(String input) {
        if (input == null) {
            return false;
        }
        try {
            // strict = true runs the same syntax checks as validate()
            InternetAddress address = new InternetAddress(input, true);
            // "Jane Doe <jane@example.com>" and " jane@example.com " parse too: compare with the input
            return address.getAddress().equals(input);
        } catch (AddressException e) {
            return false;
        }
    }
}

Use the two-argument constructor: the one-argument new InternetAddress(input) skips the detailed checks and accepted a bare jane in our test. Even in strict mode it accepted jane@example, jane@-example.com and a local part of 65 characters.

What each check accepts

Every address below went through the regex, @Email (Hibernate Validator 9.1.4.Final), Commons EmailValidator 1.11.0 with default settings and the InternetAddress check above. ✓ means accepted, ✗ rejected.

AddressShould pass?Regex@EmailCommons EmailValidatorInternetAddress
jane@example.comYes✓✓✓✓
jane.doe+news@example.comYes✓✓✓✓
o'brien@example.ieYes✓✓✓✓
jane@exampleNo✗✓✗✓
jane@example.cNo✗✓✗✓
jane.@example.comNo✗✗✗✗
.jane@example.comNo✗✗✗✗
jane..doe@example.comNo✗✗✗✗
jane@example.com.No✗✗✗✗
jane@-example.comNo✗✗✗✓
jane@exa_mple.comNo✗✓✗✗
jane doe@example.comNo✗✗✗✗
"jane doe"@example.comNo (RFC-valid)✗✓✓✓
jane@[192.0.2.10]No (RFC-valid)✗✓✓✓
josé@example.comNo (needs SMTPUTF8)✗✓✓✓
jane@bücher.example.comAfter IDN conversion✗✓✓✓
jane@xn--bcher-kva.example.comYes✓✓✓✓
Jane Doe <jane@example.com>No (display name)✗✗✗✗
jane@example.com with spaces around itAfter trimming✗✗✗✗
jane@example.com + line breakNo✗✗✗✗
aaa…a@example.com (65 a’s)No✗✗✗✓
jane@localhostNo✗✓✗✓

@Email and InternetAddress are the loosest on domains, Commons Validator is the strictest, and only the regex rejects every row marked “No”. The request record in section 5 (@Email plus the regex, after strip()) accepted the “Yes” rows plus the two rows with spaces or a trailing line break, which it stores without them; it rejected everything else.

3. MX lookup with JNDI

The JDK can query DNS without extra libraries through its JNDI DNS provider. With the provider URL dns:, it uses the servers configured on the machine (on Linux, from /etc/resolv.conf):

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

public final class MailDomain {

    /** True if the domain has MX records (not a null MX) or, without MX, an A/AAAA record. */
    public static boolean acceptsMail(String domain) throws NamingException {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL, "dns:"); // the system's DNS servers
        env.put("com.sun.jndi.dns.timeout.initial", "2000");
        env.put("com.sun.jndi.dns.timeout.retries", "1");

        DirContext dns = new InitialDirContext(env);
        try {
            Attribute mx = dns.getAttributes(domain, new String[] {"MX"}).get("MX");
            if (mx != null) {
                // A null MX ("0 .", RFC 7505) means the domain accepts no email.
                return !(mx.size() == 1 && "0 .".equals(mx.get(0).toString().trim()));
            }
            // No MX record: mail goes to the A/AAAA record (implicit MX).
            // One type per call: several IDs make the JDK send a single ANY query.
            return dns.getAttributes(domain, new String[] {"A"}).size() > 0
                    || dns.getAttributes(domain, new String[] {"AAAA"}).size() > 0;
        } catch (NameNotFoundException e) {
            return false; // NXDOMAIN: the domain doesn't exist
        } finally {
            dns.close();
        }
    }
}

Points that matter in production:

Against a local test DNS server, the final version returned true for a domain with MX records, false for a null MX, true for a domain with only an A or only an AAAA record and false for a non-existent domain; a SERVFAIL answer threw javax.naming.ServiceUnavailableException. Background in the MX record guide; to check a domain by hand, use the MX lookup tool.

4. Check that the mailbox exists

The MX lookup proves the domain takes email, not that anna@example.org exists. That needs a conversation with the receiving mail server, which is hard to run from an application host (port 25 is often blocked, catch-all domains accept everything). The email validation API does it in one GET request. This client uses the JDK’s HttpClient and Jackson (com.fasterxml.jackson.core:jackson-databind) for the JSON:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
public record EmailResult(
        String email,
        @JsonProperty("format_valid") boolean formatValid,
        @JsonProperty("mx_found") boolean mxFound,
        @JsonProperty("smtp_check") boolean smtpCheck,
        boolean disposable,
        boolean role,
        boolean free,
        double score,
        String state,
        String reason,
        @JsonProperty("did_you_mean") String didYouMean) {
}
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

public final class EmailValidationClient {
    private static final String API_KEY = System.getenv("EMAILVALIDATION_API_KEY");
    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();
    private static final ObjectMapper JSON = new ObjectMapper();

    public static EmailResult validate(String email) throws IOException, InterruptedException {
        if (API_KEY == null || API_KEY.isBlank()) {
            throw new IOException("EMAILVALIDATION_API_KEY is not set");
        }
        URI uri = URI.create("https://api.emailvalidation.io/v1/info?email="
                + URLEncoder.encode(email, StandardCharsets.UTF_8));
        HttpRequest request = HttpRequest.newBuilder(uri)
                .header("apikey", API_KEY)
                .timeout(Duration.ofSeconds(10))
                .GET()
                .build();

        HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new IOException("Email validation API returned HTTP " + response.statusCode());
        }
        return JSON.readValue(response.body(), EmailResult.class);
    }
}

The key comes from the EMAILVALIDATION_API_KEY environment variable; keep it out of source control and out of any front-end code. URLEncoder.encode turns the + in jane+news@example.com into %2B, so the tag survives. A wrong key comes back as HTTP 401 and an exhausted quota or rate limit as 429; the client turns both, and an unset variable, into an IOException. The 10-second request timeout and 5-second connect timeout cap how long a sign-up waits for the API.

How to act on the result:

stateTypical reasonWhat to do
deliverablevalid_mailboxAccept.
deliverablelow_deliverabilityCatch-all domain: every address is accepted, so the mailbox can’t be confirmed. Accept, or flag for review.
undeliverableinvalid_mailbox, invalid_mx, invalid_formatReject and ask the user to correct the address.
unknowntimeout, no_connectNo clear answer from the mail server. Accept.

Check disposable() as well, and offer didYouMean() when it isn’t empty (it suggests gmail.com for gmial.com). Why catch-all domains can’t be verified per mailbox is explained in catch-all email. The free plan includes 100 checks a month; only successful calls count against the quota.

5. Spring Boot @Valid with an API check

Spring validates a @RequestBody annotated with @Valid before your method runs; by default a failure raises MethodArgumentNotValidException, which Spring turns into a 400 response. Spring Boot needs spring-boot-starter-validation on the classpath for this, which provides Hibernate Validator.

The request record combines @Email with the regex and strips whitespace in its compact constructor, which Jackson calls before validation:

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record SignupRequest(
        @NotBlank
        @Email(regexp = EmailFormat.REGEX, message = "Please enter a valid email address.")
        String email) {

    public SignupRequest {
        email = email == null ? null : email.strip(); // people paste addresses with spaces
    }
}

The controller then asks the API and fails open when it can’t get an answer:

import jakarta.validation.Valid;
import java.io.IOException;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SignupController {
    private static final Logger log = LoggerFactory.getLogger(SignupController.class);

    @PostMapping("/signup")
    public ResponseEntity<?> signup(@Valid @RequestBody SignupRequest request) {
        try {
            EmailResult result = EmailValidationClient.validate(request.email());
            if ("undeliverable".equals(result.state())) {
                return ResponseEntity.badRequest()
                        .body(Map.of("email", "This email address doesn't exist. Please check it."));
            }
            if (result.disposable()) {
                return ResponseEntity.badRequest()
                        .body(Map.of("email", "Please use a permanent email address."));
            }
        } catch (IOException e) {
            // API unreachable, slow, out of quota or key not set: accept the address, don't block the sign-up
            log.warn("Email check skipped", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // create the account ...
        return ResponseEntity.ok().build();
    }
}

What we ran: JSON bodies deserialized into the record with Jackson and validated with Hibernate Validator 9.1.4, then the controller method against a local mock of the API. jane@example failed validation with “Please enter a valid email address.”; an address the API reported as undeliverable or disposable got a 400 with the matching message; a normal address got 200; and when the mock answered 429 or the key was not set, the sign-up went through with a logged warning. An empty string failed with both the @NotBlank and the format message. The Spring wiring itself (@RestController, @PostMapping, @Valid) was compile-checked, not run in a full Spring Boot application.

  1. In the browser, type="email" for instant feedback; the server still decides.
  2. On the server, strip the input and validate it with @NotBlank and @Email(regexp = …).
  3. On submit, the API check: block undeliverable and disposable addresses, accept everything else, and accept when the API is unreachable.
  4. For an existing database, check the whole list at once with the bulk email verifier rather than one request per row.

Want to see a result before writing code? Paste an address into the free email validator. The same steps in other languages: C# email validation and email validation in Go.

Sources

Checked October 10, 2026: @Email (Jakarta Validation 3.1.1 source); Hibernate Validator reference guide and AbstractEmailValidator (9.1.4.Final); Apache Commons Validator EmailValidator and its 1.11.0 source; Jakarta Mail 2.1 InternetAddress; JNDI DNS service provider and jdk.naming.dns (Oracle); Spring Framework: @RequestBody; Spring Boot: Validation; RFC 7505 (null MX); RFC 8482 (ANY queries). Code tested on OpenJDK 21 with Hibernate Validator 9.1.4.Final, Commons Validator 1.11.0, Jakarta Mail API 2.1.5 and Jackson databind 2.14.0 (use a current 2.x release; the client needs only ObjectMapper.readValue and two annotations) against a local DNS server and a local mock of the API.

Validate email in other languages

Frequently asked questions

How do I validate an email address in Java?

Trim the input and match it against a tested regex with Pattern.matcher(value).matches(). In Spring Boot, put @Email(regexp = …) on the request field. To know whether the mailbox exists, call an email validation API from the server.

What does @Email accept in Hibernate Validator?

More than most forms want: in our test with Hibernate Validator 9.1.4 it accepted jane@example, jane@localhost, jane@exa_mple.com and quoted local parts. Empty strings and null count as valid, so add @NotBlank.

Is Apache Commons EmailValidator better than @Email?

It is stricter about domains: it checks the top-level domain against the IANA list built into the library and rejects domains without one unless you allow local addresses. It still accepts quoted local parts and IP addresses, and new top-level domains need a library update.

How do I verify that an email address exists in Java?

Not reliably with the JDK alone. A direct SMTP check needs outbound port 25, which many hosts block, and catch-all domains accept every address. An email validation API runs the DNS, MX and mailbox checks and returns one result per address.

Email validation API

Validate emails in your app

emailvalidation.io checks syntax, MX records and the mailbox over SMTP, flags disposable, role and free addresses and returns a quality score, in one request.

/v1/info Email validation API Read the documentation

100 free validations every month. No credit card required.

GET https://api.emailvalidation.io/v1/info?email=support@emailvalidation.io

{
  "email": "support@emailvalidation.io",
  "user": "support",
  "tag": "",
  "domain": "emailvalidation.io",
  "format_valid": true,
  "mx_found": true,
  "smtp_check": true,
  "catch_all": null,
  "role": true,
  "disposable": false,
  "free": false,
  "score": 0.64,
  "state": "deliverable",
  "reason": "valid_mailbox",
  "did_you_mean": ""
}

Keep reading

Start using our email validation software today!

Get 100 validations per month for free