Validar um endereço de e-mail em Java são, na verdade, três checagens sob um mesmo nome. A maioria dos tutoriais recorre a uma regex, confirma que a string tem o formato de um endereço e considera pronto — mas o formato é a menos útil das três coisas que você realmente quer saber. A validação de verdade é em camadas: uma checagem barata de formato, uma consulta DNS pela rota de e-mail do domínio e uma sondagem SMTP da caixa postal. A biblioteca padrão do Java e o Jakarta Mail cobrem as duas primeiras de forma limpa; a terceira é um problema de rede que vale a pena delegar. Este guia constrói cada camada com código funcional e mostra exatamente onde cada uma para.
A resposta curta#
Use jakarta.mail.internet.InternetAddress (ou o Apache Commons Validator) para
a sintaxe, o provedor DNS JNDI embutido no JDK para a consulta de MX e uma API de
verificação para a checagem da caixa postal por SMTP — a mais barata primeiro,
interrompendo assim que uma delas for decisiva. Não abra conexões SMTP a partir
da sua aplicação para sondar caixas postais por conta própria: a porta 25 de
saída é bloqueada na maioria dos hosts, e a resposta depende da reputação do IP
de envio e de greylisting que você não vai querer reimplementar. Cada camada
descarta endereços de forma mais barata que a anterior; só a última consegue
aprovar um endereço.
Camada 1: sintaxe com o Jakarta Mail#
O Jakarta Mail — a biblioteca renomeada de javax.mail quando migrou para o
namespace Jakarta EE — traz um parser de endereços em que você pode se apoiar em
vez de escrever uma regex na mão. Construa um InternetAddress e chame
validate(): o construtor faz o parsing do endereço, e validate() impõe a
sintaxe da RFC 822, lançando AddressException em qualquer coisa malformada.
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();
// Rejects "Jane <jane@example.com>": the parser keeps only the address part.
return email.equals(address.getAddress());
} catch (AddressException e) {
return false;
}
}
Essa última comparação fecha uma brecha que a maioria dos exemplos deixa
passar. Pela documentação, o InternetAddress também faz o parsing da forma
Personal Name <user@host.domain>, e validate() checa só a parte do
endereço, então Jane <jane@example.com> passa nas duas etapas. Sem essa
checagem, a camada 2 receberia o domínio example.com>. Exigir que
getAddress() seja igual à entrada original rejeita tudo o que trouxer um nome
ou sinais de menor e maior.
Um detalhe importante: validate() é permissivo quanto ao domínio. Ele aceita
jane@localhost e jane@example — sem exigir ponto — porque a RFC 822 permite
domínios sem ponto. E tudo bem aqui. A camada 1 é um portão de formato, não uma
checagem de entregabilidade, e é a consulta DNS da camada 2 que de fato decide
se o domínio consegue receber e-mail. Mantenha essa checagem permissiva e deixe
o DNS fazer o descarte; perseguir a perfeição do formato com uma regex maior é
uma batalha perdida de qualquer forma.
Se você estiver em um jar javax.mail mais antigo, o código é idêntico, exceto
pelo nome do pacote. E se você já depende do Apache Commons Validator, o
EmailValidator dele é uma boa alternativa para a camada um — é um pouco mais
rígido (rejeita domínios sem ponto por padrão):
import org.apache.commons.validator.routines.EmailValidator;
boolean valid = EmailValidator.getInstance().isValid(email);
Prefira o Jakarta Mail se você já o usa para enviar e-mail — uma dependência a
menos. Qualquer uma das bibliotecas só responde à pergunta da sintaxe. O mesmo
vale para o @Email do Bean Validation, que o
guia de Spring Boot cobre.
Regex de validação de e-mail em Java (se não tiver jeito)#
Se você não pode adicionar uma biblioteca, ou precisa de uma única regra compartilhada com o front-end, mantenha a regex permissiva e compile-a uma vez só:
import java.util.regex.Pattern;
// Dot-separated runs with no whitespace, @ or quotes, one @,
// then a domain with at least one dot.
private static final Pattern EMAIL = Pattern.compile(
"[^\\s@.\"]+(?:\\.[^\\s@.\"]+)*@[^\\s@.]+(?:\\.[^\\s@.]+)+",
Pattern.UNICODE_CHARACTER_CLASS);
public static boolean looksLikeEmail(String email) {
return email != null && email.length() <= 320 && EMAIL.matcher(email).matches();
}
É o mesmo padrão permissivo que o guia
por que a validação de e-mail com regex falha
recomenda: o ponto fica fora das sequências da parte local, então
a..b@example.com, .a@example.com e a.@example.com falham. Ele não precisa
de ^ nem de $ porque matches() precisa casar com a entrada inteira;
find() procura uma substring que case, então aceitaria
junk jane@example.com junk. O UNICODE_CHARACTER_CLASS faz o \s casar com
espaços Unicode, como acontece no JavaScript.
Evite duas armadilhas comuns: um limite de TLD como [a-zA-Z]{2,7}, que rejeita
TLDs reais como .photography, e padrões "RFC 5322" gigantescos que aceitam
formas que nenhum formulário de cadastro quer. Um match prova o formato, nada
mais.
Camada 2: o domínio consegue receber e-mail?#
Um domínio sem rota de e-mail não consegue aceitar e-mail para ninguém, então
uma consulta DNS rápida (primeiro o MX e, como alternativa, o próprio endereço
do domínio) elimina domínios mortos, nomes de empresas com erro de digitação e
TLDs inventados. O Java faz isso sem nenhuma dependência externa: o JDK inclui
um provedor de serviço DNS para o JNDI, então você resolve o atributo MX
através de um DirContext.
import java.util.Hashtable;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
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);
Attribute mx = ctx.getAttributes(domain, new String[] { "MX" }).get("MX");
if (mx != null && mx.size() > 0) {
return true;
}
// No MX: mail goes to the domain's own address (A or AAAA), the implicit MX.
// One type per call: asking for several at once sends an ANY query.
for (String type : new String[] { "A", "AAAA" }) {
Attribute addr = ctx.getAttributes(domain, new String[] { type }).get(type);
if (addr != null && addr.size() > 0) {
return true;
}
}
return false;
} catch (NameNotFoundException e) {
return false; // NXDOMAIN: the domain doesn't exist
} catch (NamingException e) {
return true; // timeout or resolver failure: can't tell, so don't reject
} finally {
if (ctx != null) {
try { ctx.close(); } catch (NamingException ignored) { }
}
}
}
Separe o domínio do endereço no último @ para que partes locais entre aspas
não te atrapalhem:
String domain = email.substring(email.lastIndexOf('@') + 1);
hasMailRoute("gmail.com"); // true
hasMailRoute("company-that-folded.com"); // false
Um atributo MX ausente ou vazio significa que o domínio não publica registro
MX; uma NameNotFoundException (subclasse de NamingException) significa que
ele não resolve de forma alguma. Alguns domínios aceitam e-mail em um registro A
ou AAAA com um MX implícito, então o código recorre à consulta de "A" e
"AAAA" quando o conjunto de MX volta vazio, um tipo por chamada: o JNDI
transforma um pedido de vários tipos em uma consulta ANY, que muitos
servidores DNS respondem só com um registro mínimo. Para a esmagadora maioria
dos endereços reais, uma checagem de MX é o filtro certo. Como a consulta
bloqueia esperando um resolver DNS, mantenha os timeouts acima em um fluxo de
cadastro e trate uma falha transitória como "verificar depois", não como uma
rejeição definitiva — um soluço do DNS não deveria afastar um cliente real. Por
isso qualquer outra NamingException, como um timeout, retorna true e deixa a
camada 3 decidir. Para checar um domínio na mão, use a
consulta de registros MX.
Camada 3: a caixa postal realmente existe?#
As camadas 1 e 2 só conseguem descartar um endereço. Um domínio pode publicar
registros MX perfeitos e ainda assim não ter caixa postal no endereço que você
tem em mãos — noreply-9f2x@gmail.com tem sintaxe válida em um domínio com rota
de e-mail ativa, e ainda assim é uma caixa postal que nunca foi criada. Confirmar
que uma caixa postal específica existe exige a conversa de entrega SMTP:
conectar ao host de e-mail, emitir RCPT TO, ler o
código de resposta e desconectar antes de enviar
qualquer coisa. Há mais nuances —
domínios catch-all aceitam qualquer endereço
e derrotam uma sondagem ingênua —
como funciona a verificação de e-mail
percorre o pipeline completo.
Você pode roteirizar SMTP em Java com um Socket cru, mas não deveria rodá-lo a
partir da sua aplicação. A maioria dos provedores de nuvem bloqueia a porta 25 de
saída, a resposta depende da reputação do IP a partir do qual você conecta, e os
servidores de recebimento aplicam greylisting e limitam a taxa de remetentes
desconhecidos — então uma sondagem que passa em um teste local silenciosamente
falha, ou te coloca em uma blocklist, em produção. Esta é a camada que vale a
pena delegar.
A API de verificação da Qualisend roda esse pipeline a partir de uma
infraestrutura com reputação gerenciada, feita para isso. O POST /verify
devolve um veredito instantâneo das checagens locais (sintaxe, DNS, detecção de
endereços descartáveis e de erros de digitação) mais um job_id; se a sondagem
da caixa postal ainda estiver pendente, o veredito dela chega a esse job. O Java
11+ já traz java.net.http.HttpClient, então você não precisa de nenhuma
dependência HTTP; combine-o com o Jackson (ou o Gson) para ler o JSON. Guarde
sua chave em uma variável de ambiente e nunca a deixe embutida no código:
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 {
private static final String BASE_URL = "https://app.qualisend.com/api/v1";
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
private final String apiKey = System.getenv("QUALISEND_API_KEY"); // holds YOUR_API_KEY
/** POST /verify: a job_id plus the instant verdict under "result". */
public JsonNode verify(String email) throws IOException, InterruptedException {
String payload = mapper.writeValueAsString(Map.of("email", email));
return send(HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/verify"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload)));
}
/** GET /jobs/{id}: the job's status and, once completed, its results. */
public JsonNode job(String jobId) throws IOException, InterruptedException {
return send(HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/jobs/" + jobId + "?include=results"))
.GET());
}
private JsonNode send(HttpRequest.Builder builder) throws IOException, InterruptedException {
HttpRequest request = builder
.header("Authorization", "Bearer " + apiKey)
.timeout(Duration.ofSeconds(10)) // throws HttpTimeoutException past this
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2) {
throw new ApiException(response.statusCode(),
response.headers().firstValue("Retry-After").orElse(null));
}
return mapper.readTree(response.body());
}
/** A non-2xx reply. It extends IOException, like HttpTimeoutException. */
public static class ApiException extends IOException {
public final int status;
public final String retryAfter; // seconds, sent with a 429
ApiException(int status, String retryAfter) {
super("Qualisend responded " + status);
this.status = status;
this.retryAfter = retryAfter;
}
}
}
Leia os campos que te interessam a partir do nó result:
JsonNode body = client.verify("jane@example.com");
JsonNode result = body.get("result");
String status = result.get("status").asText(); // deliverable | risky | undeliverable | unknown
int score = result.path("score").asInt(); // 0–100 confidence
String smtp = result.path("smtp").asText(); // pending | cached | skipped
String reason = result.hasNonNull("reason")
? result.get("reason").asText() // machine-readable reason, null while pending
: null;
boolean disposable = result.path("sub_flags").path("disposable").asBoolean();
String jobId = body.path("job_id").asText(); // where the mailbox verdict lands
O formato exato da resposta — cada chave de sub_flags e cada código de reason
— está na referência da API. Para a validação de cadastro ao vivo,
o status geralmente basta para agir: rejeite undeliverable, trate risky e
unknown conforme sua política e sinalize endereços disposable já na entrada.
Obtendo o veredito SMTP: consulte o job#
Cheque smtp antes de tratar status como definitivo. Com cached (um
resultado recente foi reaproveitado) ou skipped (as checagens locais foram
decisivas), o result é definitivo e reason vem preenchido. Com pending, a
sondagem da caixa postal entra na fila e roda depois que a resposta volta, então
status é um unknown provisório e reason é null: não rejeite por causa
disso. Servidores que aplicam greylisting só
respondem a um remetente desconhecido em uma nova tentativa, então o veredito da
caixa postal pode levar minutos, não segundos.
No cadastro, o veredito instantâneo já barrou sintaxe inválida, domínios mortos
e endereços descartáveis, então deixe passar um unknown pendente. Quando você
precisar da resposta sobre a caixa postal, consulte o job a partir de um worker
em segundo plano, nunca da thread da requisição, e pare depois de um número
fixo de tentativas:
// Runs in a background worker, never on the request thread.
public JsonNode awaitMailboxVerdict(String jobId, int maxAttempts)
throws InterruptedException {
for (int attempt = 0; attempt < maxAttempts; attempt++) {
try {
JsonNode job = client.job(jobId);
String status = job.path("status").asText();
if (status.equals("completed")) {
return job.path("results").path(0); // the post-SMTP verdict
}
if (status.equals("canceled") || status.equals("failed")) {
break;
}
} catch (IOException e) {
// 429, 5xx or a timeout: try again on the next pass
}
// queued or processing: back off (5, 10, 20, 40, then 60 s), then ask again
Thread.sleep(Math.min(60_000L, 5_000L << Math.min(attempt, 4)));
}
return null; // gave up: treat the address as unknown and re-check later
}
A linha traz os mesmos campos status e reason do resultado instantâneo. Cada
consulta é uma chamada à API e conta para o mesmo limite de taxa por espaço de
trabalho que o /verify, então aumente o intervalo entre as tentativas em vez
de consultar em um intervalo curto e fixo. Não chame o /verify de novo para
atualizar um resultado pendente; isso inicia uma nova checagem (e gasta mais um
crédito) em vez de concluir esta.
Timeouts, limites de taxa e fail open#
O verify() lança uma exceção para qualquer resposta que não seja 2xx. Isso
serve para um job em lote, não para um formulário de cadastro: ali, trate cada
falha de um jeito, deixe o cliente passar quando o problema estiver do lado da
API e verifique de novo mais tarde. O
guia de cadastro serverless usa o
mesmo padrão de fail open:
private final QualisendClient client = new QualisendClient();
private static final JsonNode UNKNOWN =
new ObjectMapper().createObjectNode().put("status", "unknown");
public JsonNode verifyForSignup(String email) throws InterruptedException {
try {
JsonNode body = client.verify(email);
JsonNode result = body.get("result");
if ("pending".equals(result.path("smtp").asText())) {
queueMailboxCheck(body.path("job_id").asText()); // a worker runs awaitMailboxVerdict
}
return result;
} catch (QualisendClient.ApiException e) {
if (e.status == 429) {
recheckLater(email, e.retryAfter); // rate limited: retry after Retry-After seconds
} else if (e.status == 402) {
alertOps("Qualisend: out of credits"); // insufficient_credits
recheckLater(email, null);
} else if (e.status >= 500) {
recheckLater(email, null); // an outage, not the customer's problem
} else {
alertOps("Qualisend rejected the request: " + e.status); // 400/401/403: fix the call or key
}
} catch (IOException e) {
recheckLater(email, null); // HttpTimeoutException or a dropped connection
}
return UNKNOWN;
}
queueMailboxCheck, recheckLater e alertOps representam sua fila de jobs e
seus alertas. Capture ApiException primeiro: ela é uma IOException, assim
como HttpTimeoutException, o que permite que o último catch cubra timeouts e
conexões derrubadas. O limite de taxa vale por espaço de trabalho, não por
chave, então adicionar chaves não resolve um 429.
Juntando as camadas para validar um endereço de e-mail em Java#
A mais barata primeiro, pare assim que tiver uma resposta. Um pequeno record guarda o veredito (records são do Java 16+; no 11–15, use uma classe comum):
// mailboxPending: act on the local verdict now; the worker reports the mailbox result later.
public record Verdict(String status, String reason, boolean mailboxPending) {}
public Verdict validate(String email) throws InterruptedException {
if (!isValidSyntax(email)) {
return new Verdict("undeliverable", "invalid_email", false);
}
String domain = email.substring(email.lastIndexOf('@') + 1);
if (!hasMailRoute(domain)) {
return new Verdict("undeliverable", "invalid_domain", false);
}
JsonNode result = verifyForSignup(email); // fails open to "unknown"
return new Verdict(
result.path("status").asText("unknown"),
result.hasNonNull("reason") ? result.get("reason").asText() : null,
"pending".equals(result.path("smtp").asText()));
}
As duas camadas locais não custam nada e pegam a maior parte do lixo na hora; a API roda apenas nos endereços que valem a ida e volta pela rede. Essa ordem é o truque inteiro — o mesmo formato que você encontra nas versões em Node.js, Python, PHP, Go e C# deste guia, porque é o encadeamento das camadas, não a linguagem, que faz a validação funcionar.
Para uma lista inteira, não chame o /verify em loop sobre um CSV: envie-a ao
POST /verify/bulk (até 1.000.000 de linhas) ou faça o upload no app, como
descreve o guia de limpeza de listas.
Perguntas frequentes
01InternetAddress.validate() basta para validar um e-mail em Java?
Para a sintaxe, é a ferramenta certa da camada um: o jakarta.mail.internet.InternetAddress com validate() é mais bem testado que uma regex caseira e livra você de reimplementar a RFC 822. Mas ele valida apenas o formato. Nunca resolve DNS nem contata um servidor de e-mail, e é permissivo o bastante para aceitar domínios sem ponto, como jane@localhost. Ele também faz o parsing da forma com nome de exibição Jane <jane@example.com>, então confira se getAddress() é igual à entrada. Depois, combine-o com uma consulta de MX e uma verificação de caixa postal por SMTP antes de confiar no endereço.
02Qual regex usar para validar um endereço de e-mail em Java?
Uma curta e permissiva, compilada uma vez só como static final Pattern e checada com matcher(email).matches(), que precisa casar com a string inteira (find() aceita um match em qualquer ponto dela). O padrão deste guia exige um único @, nenhum espaço em branco, nenhuma parte vazia entre pontos e pelo menos um ponto no domínio. Não limite o tamanho do TLD com algo como {2,7}: TLDs reais como .photography são mais longos. Uma regex só checa o formato, então não consegue dizer se o domínio ou a caixa postal existem; por que a validação de e-mail com regex falha explica os limites.
03Como consultar registros MX em Java sem uma biblioteca externa?
Use o provedor DNS JNDI embutido no JDK. Crie um InitialDirContext com java.naming.factory.initial definido como com.sun.jndi.dns.DnsContextFactory, depois chame getAttributes(domain, new String[] {"MX"}) e verifique o atributo MX retornado. Se não houver MX, consulte também "A" e "AAAA" (um tipo por chamada), já que o e-mail pode recorrer aos registros de endereço do próprio domínio. Trate uma NameNotFoundException como um domínio que não existe, mas não rejeite por causa de um timeout ou de outra NamingException: isso é um problema do resolver, não um endereço ruim.
04Dá para verificar uma caixa postal em Java sem uma API?
Em parte. O Jakarta Mail e o JNDI confirmam que o endereço está bem formado e que o domínio aceita e-mail - ambos gratuitos e com poucas dependências. Confirmar a própria caixa postal exige uma conversa SMTP, que você pode tentar com um Socket cru, mas não deveria rodar do seu servidor de aplicação: a porta 25 é amplamente bloqueada e o resultado depende da reputação do seu IP de envio. Essa camada final é o que um serviço de verificação cuida.
05Devo validar e-mails no cadastro ou ao limpar uma lista?
Nos dois, com profundidades diferentes. Rode as camadas de sintaxe e MX de forma síncrona no cadastro - elas são rápidas o suficiente para bloquear a requisição e dar feedback instantâneo - e aja ali também sobre o veredito instantâneo da API (rejeite undeliverable, deixe passar um unknown pendente), deixando o veredito da caixa postal para um worker em segundo plano. Reserve o trabalho em lote mais profundo para a limpeza de listas; o guia de cadastro serverless mostra o padrão em tempo real, e o guia de limpeza de listas cobre o lado em lote.
Pronto para adicionar a camada SMTP? Passe qualquer endereço pelo
verificador de e-mail gratuito para ver uma string com
sintaxe perfeita voltar com um veredito real, ou leia a
referência da API para o endpoint /verify, o envelope result
completo e exemplos de copiar e colar em todas as linguagens acima.



