Skip to content
Comece com 100 créditos de verificação grátis
Qualisend
Todos os artigos
Engenharia / 14 de junho de 2026

Como validar um endereço de e-mail em Java

11 min de leitura

Qualisend team
Como validar um endereço de e-mail em Java

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.

Sua reputação, protegida.

Limpe sua primeira lista em minutos. 100 créditos grátis, sem cartão.

Começar