Validating an email address in Java is really three checks wearing one name. Most tutorials reach for a regex, confirm the string is shaped like an address, and call it done — but shape is the least useful of the three things you actually want to know. Real validation is layered: a cheap format check, a DNS lookup for the domain's mail route, and an SMTP mailbox probe. Java's standard library and Jakarta Mail cover the first two cleanly; the third is a network problem worth handing off. This guide builds each layer with working code, and shows exactly where each one stops.
The short answer#
Use jakarta.mail.internet.InternetAddress (or Apache Commons Validator) for
syntax, the JDK's built-in JNDI DNS provider for the MX lookup, and a
verification API for the SMTP mailbox check — cheapest first, short-circuiting
as soon as one is decisive. Don't open SMTP connections from your app to probe
mailboxes yourself: outbound port 25 is blocked on most hosts, and the answer
depends on sending-IP reputation and greylisting you don't want to reimplement.
Each layer rules addresses out more cheaply than the last; only the final one
can rule an address in.
Layer 1: syntax with Jakarta Mail#
Jakarta Mail — the library renamed from javax.mail when it moved to the
Jakarta EE namespace — ships an address parser you can lean on instead of
hand-rolling a regex. Construct an InternetAddress and call validate(): the
constructor parses the address, and validate() enforces RFC 822 syntax,
throwing AddressException on anything malformed.
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
public static boolean isValidSyntax(String email) {
if (email == null || email.length() > 320) {
return false;
}
try {
InternetAddress address = new InternetAddress(email);
address.validate();
return true;
} catch (AddressException e) {
return false;
}
}
One thing to know: validate() is permissive about the domain. It accepts
jane@localhost and jane@example — no dot required — because RFC 822 allows
dotless domains. That is fine here. Layer 1 is a format gate, not a
deliverability check, and layer 2's DNS lookup is what actually decides whether
the domain can receive mail. Keep this check permissive and let DNS do the
ruling-out; chasing format perfection with a bigger regex is a
losing game anyway.
If you're on an older javax.mail jar, the code is identical bar the package
name. And if you already depend on Apache Commons Validator, its EmailValidator
is a fine layer-one alternative — it's a touch stricter (it rejects dotless
domains by default):
import org.apache.commons.validator.routines.EmailValidator;
boolean valid = EmailValidator.getInstance().isValid(email);
Prefer Jakarta Mail if you're already using it to send mail — one fewer dependency. Either library only ever answers the syntax question.
Layer 2: can the domain receive mail?#
A domain with no MX records can't accept mail for anyone, so a single MX lookup
eliminates dead domains, misspelled company names, and invented TLDs. Java does
this with no external dependency at all: the JDK bundles a DNS service provider
for JNDI, so you resolve the MX attribute through a DirContext.
import java.util.Hashtable;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
public static boolean hasMailRoute(String domain) {
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", "2000"); // don't hang on a slow resolver
env.put("com.sun.jndi.dns.timeout.retries", "1");
DirContext ctx = null;
try {
ctx = new InitialDirContext(env);
Attributes attrs = ctx.getAttributes(domain, new String[] { "MX" });
Attribute mx = attrs.get("MX");
return mx != null && mx.size() > 0;
} catch (NamingException e) {
// NameNotFoundException → domain doesn't exist; no MX attribute → no mail route
return false;
} finally {
if (ctx != null) {
try { ctx.close(); } catch (NamingException ignored) { }
}
}
}
Split the domain off the address at the last @ so quoted local parts don't
trip you up:
String domain = email.substring(email.lastIndexOf('@') + 1);
hasMailRoute("gmail.com"); // true
hasMailRoute("company-that-folded.com"); // false
A missing or empty MX attribute means the domain publishes no mail route; a
NameNotFoundException (a NamingException subclass) means it doesn't resolve
at all. Some domains accept mail on an A record with an implicit MX — if you
want to honour that edge case, fall back to querying "A" when the MX set comes
back empty. For the overwhelming majority of real addresses, an MX check is the
right filter. Because the lookup blocks on a DNS resolver, keep the timeouts
above in a signup path and treat a transient failure as "check later," not a
hard reject — a DNS hiccup shouldn't turn a real customer away.
Layer 3: does the mailbox actually exist?#
Layers 1 and 2 can only rule an address out. A domain can publish perfect MX
records and still have no mailbox at the address you're holding —
noreply-9f2x@gmail.com is valid syntax on a domain with a live mail route, and
it's still a mailbox that was never created. Confirming that a specific mailbox
exists means the SMTP delivery conversation: connect to the mail host, issue
RCPT TO, read the reply, and disconnect before sending anything. There's more
to it — catch-all domains accept every address and defeat a naive probe —
how email verification works walks the
full pipeline.
You can script SMTP in Java with a raw Socket, but you shouldn't run it from
your application. Most cloud providers block outbound port 25, the answer
depends on the reputation of the IP you connect from, and receiving servers
greylist and rate-limit unfamiliar senders — so a probe that passes in a local
test quietly fails, or gets you blocklisted, in production. This is the layer
worth delegating.
Qualisend's POST /verify runs the whole pipeline — syntax, DNS,
and the SMTP mailbox probe — from reputation-managed infrastructure built for
it, and returns a verdict. Java 11+ ships java.net.http.HttpClient, so you need
no HTTP dependency; pair it with Jackson (or Gson) to read the JSON. Keep your
key in an environment variable and never hard-code it:
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class QualisendClient {
// Placeholder endpoint — check /developers for the current base URL and shape.
private static final String ENDPOINT = "https://api.qualisend.com/v1/verify";
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
public JsonNode verify(String email) throws IOException, InterruptedException {
String apiKey = System.getenv("QUALISEND_API_KEY"); // holds YOUR_API_KEY
String payload = mapper.writeValueAsString(Map.of("email", email));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(ENDPOINT))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IOException("Qualisend responded " + response.statusCode());
}
// The verdict lives inside a `result` envelope.
return mapper.readTree(response.body()).get("result");
}
}
Read the fields you care about off the result node:
JsonNode result = client.verify("jane@example.com");
String status = result.get("status").asText(); // deliverable | risky | undeliverable | unknown
int score = result.path("score").asInt(); // 0–100 confidence
String reason = result.hasNonNull("reason")
? result.get("reason").asText() // machine-readable reason, may be null
: null;
boolean disposable = result.path("sub_flags").path("disposable").asBoolean();
The exact response shape — every sub_flags key and reason code — is in the
API reference. For live signup validation the status is usually
enough to act on: reject undeliverable, treat risky and unknown by policy,
and flag disposable addresses at the input.
Putting the layers together to validate an email address in Java#
Cheapest first, stop as soon as you have an answer. A small record holds the verdict (records are Java 16+; on 11–15 use a plain class):
public record Verdict(String status, String reason) {}
public Verdict validate(String email) throws IOException, InterruptedException {
if (!isValidSyntax(email)) {
return new Verdict("undeliverable", "invalid_email");
}
String domain = email.substring(email.lastIndexOf('@') + 1);
if (!hasMailRoute(domain)) {
return new Verdict("undeliverable", "invalid_domain");
}
JsonNode result = client.verify(email); // client is a QualisendClient
return new Verdict(
result.get("status").asText(),
result.hasNonNull("reason") ? result.get("reason").asText() : null);
}
The two local layers cost nothing and catch most junk instantly; the API runs only on addresses worth the network round-trip. That ordering is the whole trick — the same shape you'll find in the Node.js, Python, and PHP versions of this guide, because the layering, not the language, is what makes validation work.
Frequently asked questions#
Is InternetAddress.validate() enough to validate an email in Java?#
For syntax, it's the right layer-one tool — jakarta.mail.internet.InternetAddress
with validate() is better tested than a hand-rolled regex and saves you from
reimplementing RFC 822. But it validates shape only: it never resolves DNS or
contacts a mail server, and it's permissive enough to accept dotless domains like
jane@localhost. Pair it with an MX lookup and an SMTP mailbox check before you
trust the address.
How do I check MX records in Java without an external library?#
Use the JDK's built-in JNDI DNS provider. Create an InitialDirContext with
java.naming.factory.initial set to com.sun.jndi.dns.DnsContextFactory, then
call getAttributes(domain, new String[] {"MX"}) and check the returned MX
attribute. It needs no third-party dependency and cleanly tells you whether a
domain publishes a mail route. Catch NamingException and treat a missing MX as
undeliverable.
Can I verify a mailbox in Java without an API?#
Partly. Jakarta Mail and JNDI confirm the address is well-formed and the domain
accepts mail — both free and dependency-light. Confirming the mailbox itself
means an SMTP conversation, which you can attempt with a raw Socket but
shouldn't run from your app server: port 25 is widely blocked and the result
depends on your sending IP's reputation. That final layer is what a verification
service handles.
Should I validate emails at signup or when cleaning a list?#
Both, at different depths. Run the syntax and MX layers synchronously at signup —
they're fast enough to block the request and give instant feedback — and act on
the API's status there too. Reserve deeper batch work for list cleaning; the
serverless signup guide shows the
real-time pattern, and the
list-cleaning guide covers the batch side.
Ready to add the SMTP layer? Run any address through the
free email checker to watch a syntactically perfect
string come back with a real verdict, or read the API reference
for the /verify endpoint, the full result envelope, and copy-paste examples
in every language above.