Skip to content
Commencez avec 100 crédits de vérification gratuits
Qualisend
Tous les articles
Ingénierie / 14 juin 2026

Comment valider une adresse e-mail en Java

11 min de lecture

Qualisend team
Comment valider une adresse e-mail en Java

Valider une adresse e-mail en Java, c'est en réalité trois contrôles sous un seul nom. La plupart des tutoriels dégainent une regex, confirment que la chaîne a la forme d'une adresse, et s'arrêtent là — mais la forme est la moins utile des trois choses que vous voulez vraiment savoir. La vraie validation est en couches : un contrôle de format bon marché, une recherche DNS de la route de messagerie du domaine, et une sonde de boîte aux lettres par SMTP. La bibliothèque standard de Java et Jakarta Mail couvrent proprement les deux premières ; la troisième est un problème réseau qu'il vaut mieux déléguer. Ce guide construit chaque couche avec du code fonctionnel, et montre exactement où chacune s'arrête.

La réponse courte#

Utilisez jakarta.mail.internet.InternetAddress (ou Apache Commons Validator) pour la syntaxe, le fournisseur DNS JNDI intégré au JDK pour la recherche MX, et une API de vérification pour le contrôle de boîte aux lettres par SMTP — le moins cher d'abord, en court-circuitant dès que l'un est décisif. N'ouvrez pas de connexions SMTP depuis votre application pour sonder les boîtes aux lettres vous-même : le port 25 sortant est bloqué sur la plupart des hébergeurs, et la réponse dépend de la réputation de l'IP d'envoi et du greylisting que vous ne voulez pas réimplémenter. Chaque couche écarte des adresses à moindre coût que la précédente ; seule la dernière peut retenir une adresse comme valide.

Couche 1 : la syntaxe avec Jakarta Mail#

Jakarta Mail — la bibliothèque renommée depuis javax.mail lors de son passage dans l'espace de noms Jakarta EE — embarque un analyseur d'adresses sur lequel vous pouvez vous appuyer plutôt que de bricoler une regex. Construisez un InternetAddress et appelez validate() : le constructeur analyse l'adresse, et validate() fait respecter la syntaxe RFC 822, en levant AddressException sur tout ce qui est mal formé.

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;
    }
}

Cette dernière comparaison comble une lacune que la plupart des exemples ignorent. D'après sa documentation, InternetAddress analyse aussi la forme Personal Name <user@host.domain>, et validate() ne contrôle que la partie adresse, si bien que Jane <jane@example.com> passe les deux étapes. Sans cette vérification, la couche 2 recevrait le domaine example.com>. Exiger que getAddress() soit égal à la saisie brute rejette tout ce qui porte un nom ou des chevrons.

Une chose à savoir : validate() est permissif quant au domaine. Il accepte jane@localhost et jane@example — pas de point requis — parce que la RFC 822 autorise les domaines sans point. C'est très bien ici. La couche 1 est un filtre de format, pas un contrôle de délivrabilité, et c'est la recherche DNS de la couche 2 qui décide réellement si le domaine peut recevoir du courrier. Gardez ce contrôle permissif et laissez le DNS faire l'élimination ; courir après la perfection du format avec une regex plus grosse est de toute façon une partie perdue d'avance.

Si vous êtes sur un ancien jar javax.mail, le code est identique au nom du paquet près. Et si vous dépendez déjà d'Apache Commons Validator, son EmailValidator est une bonne alternative de couche 1 — il est un brin plus strict (il rejette les domaines sans point par défaut) :

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

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

Préférez Jakarta Mail si vous l'utilisez déjà pour envoyer du courrier — une dépendance de moins. Dans tous les cas, chaque bibliothèque ne répond jamais qu'à la question de la syntaxe. Il en va de même pour @Email de Bean Validation, que traite le guide Spring Boot.

Regex de validation d'e-mail en Java (si vous y tenez)#

Si vous ne pouvez pas ajouter de bibliothèque, ou s'il vous faut une seule règle partagée avec le front-end, gardez la regex permissive et compilez-la une seule fois :

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();
}

C'est le même motif permissif que recommande pourquoi la validation d'e-mail par regex échoue : le point est exclu des séquences de la partie locale, si bien que a..b@example.com, .a@example.com et a.@example.com échouent tous. Il n'a besoin ni de ^ ni de $, car matches() doit couvrir toute l'entrée ; find() cherche une sous-chaîne qui correspond, et accepterait donc junk jane@example.com junk. UNICODE_CHARACTER_CLASS fait correspondre \s aux espaces Unicode, comme en JavaScript.

Évitez deux pièges courants : un plafond de TLD comme [a-zA-Z]{2,7}, qui rejette de vrais TLD comme .photography, et les motifs « RFC 5322 » tentaculaires qui acceptent des formes dont aucun formulaire d'inscription ne veut. Une correspondance prouve la forme, rien de plus.

Couche 2 : le domaine peut-il recevoir du courrier ?#

Un domaine sans route de messagerie ne peut accepter de courrier pour personne, si bien qu'une recherche DNS rapide (d'abord le MX, puis l'adresse propre du domaine en repli) élimine les domaines morts, les noms d'entreprise mal orthographiés et les TLD inventés. Java le fait sans aucune dépendance externe : le JDK embarque un fournisseur de service DNS pour JNDI, vous résolvez donc l'attribut MX à travers un 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) { }
        }
    }
}

Séparez le domaine de l'adresse au dernier @ pour que les parties locales entre guillemets ne vous fassent pas trébucher :

String domain = email.substring(email.lastIndexOf('@') + 1);

hasMailRoute("gmail.com");               // true
hasMailRoute("company-that-folded.com"); // false

Un attribut MX absent ou vide signifie que le domaine ne publie aucun enregistrement MX ; une NameNotFoundException (une sous-classe de NamingException) signifie qu'il ne se résout pas du tout. Certains domaines acceptent le courrier sur un enregistrement A ou AAAA avec un MX implicite : le code se replie donc sur une interrogation de "A" et de "AAAA" lorsque l'ensemble MX revient vide, un type par appel, car JNDI transforme une demande portant sur plusieurs types en requête ANY, à laquelle de nombreux serveurs DNS ne répondent plus que par un enregistrement minimal. Pour l'immense majorité des adresses réelles, un contrôle MX est le bon filtre. Comme la recherche bloque sur un résolveur DNS, conservez les délais d'attente ci-dessus dans un parcours d'inscription et traitez un échec transitoire comme « à revérifier plus tard », et non comme un rejet ferme — un hoquet DNS ne devrait pas faire fuir un vrai client. C'est pourquoi toute autre NamingException, comme un délai dépassé, renvoie true et laisse la couche 3 trancher. Pour contrôler un domaine à la main, utilisez la recherche d'enregistrements MX.

Couche 3 : la boîte aux lettres existe-t-elle vraiment ?#

Les couches 1 et 2 ne peuvent qu'écarter une adresse. Un domaine peut publier des enregistrements MX parfaits et n'avoir malgré tout aucune boîte aux lettres à l'adresse que vous détenez — noreply-9f2x@gmail.com est syntaxiquement valide sur un domaine doté d'une route de messagerie active, et c'est pourtant une boîte aux lettres qui n'a jamais été créée. Confirmer qu'une boîte aux lettres spécifique existe implique la conversation de livraison SMTP : se connecter à l'hôte de messagerie, émettre RCPT TO, lire le code de réponse, et se déconnecter avant d'envoyer quoi que ce soit. Il y a plus subtil encore — les domaines catch-all acceptent chaque adresse et déjouent une sonde naïve — le fonctionnement de la vérification d'e-mail parcourt le pipeline complet.

Vous pouvez scripter SMTP en Java avec un Socket brut, mais vous ne devriez pas l'exécuter depuis votre application. La plupart des fournisseurs cloud bloquent le port 25 sortant, la réponse dépend de la réputation de l'IP depuis laquelle vous vous connectez, et les serveurs récepteurs mettent en greylist et limitent le débit des expéditeurs inconnus — de sorte qu'une sonde qui passe dans un test local échoue silencieusement, ou vous fait blacklister, en production. C'est la couche qu'il vaut la peine de déléguer.

L'API de vérification de Qualisend exécute ce pipeline depuis une infrastructure à réputation gérée conçue pour cela. POST /verify renvoie un verdict immédiat issu des contrôles locaux (syntaxe, DNS, détection des adresses jetables et des fautes de frappe) ainsi qu'un job_id ; si la sonde de boîte aux lettres est encore en attente, son verdict arrive sur ce job. Java 11+ embarque java.net.http.HttpClient, vous n'avez donc besoin d'aucune dépendance HTTP ; associez-le à Jackson (ou Gson) pour lire le JSON. Gardez votre clé dans une variable d'environnement et ne la codez jamais en dur :

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;
        }
    }
}

Lisez les champs qui vous intéressent sur le nœud 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

La forme exacte de la réponse — chaque clé sub_flags et chaque code reason — figure dans la référence de l'API. Pour la validation d'inscription en direct, le status suffit généralement pour agir : rejetez undeliverable, traitez risky et unknown selon votre politique, et signalez les adresses disposable dès la saisie.

Obtenir le verdict SMTP : interroger le job#

Vérifiez smtp avant de considérer status comme définitif. Avec cached (un résultat récent a été réutilisé) ou skipped (les contrôles locaux ont suffi), le result est définitif et reason est renseigné. Avec pending, la sonde de boîte aux lettres est mise en file d'attente et s'exécute après le retour de la réponse : status est donc un unknown provisoire et reason vaut null. Ne rejetez pas l'adresse pour autant. Les serveurs qui pratiquent le greylisting ne répondent à un expéditeur inconnu qu'à une nouvelle tentative, si bien que le verdict de boîte aux lettres peut prendre des minutes, pas des secondes.

À l'inscription, le verdict immédiat a déjà écarté les syntaxes invalides, les domaines morts et les adresses jetables : laissez donc passer un unknown en attente. Quand vous avez besoin de la réponse sur la boîte aux lettres, interrogez le job depuis un worker en arrière-plan, jamais depuis le thread de la requête, et arrêtez-vous après un nombre fixe de tentatives :

// 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
}

La ligne porte les mêmes champs status et reason que le résultat immédiat. Chaque interrogation est un appel d'API et compte dans la même limite de débit par espace de travail que /verify : espacez donc progressivement les tentatives au lieu d'interroger à intervalle court et fixe. N'appelez pas /verify à nouveau pour rafraîchir un résultat en attente ; cela lance une nouvelle vérification (et consomme un crédit de plus) au lieu de terminer celle-ci.

Délais d'attente, limites de débit et fail open#

verify() lève une exception pour toute réponse autre qu'un 2xx. Cela convient à un traitement par lots, pas à un formulaire d'inscription : là, distinguez les cas selon l'échec, laissez passer le client quand le problème vient de l'API, et revérifiez plus tard. Le guide sur l'inscription serverless applique le même principe 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 et alertOps représentent votre file de tâches et vos alertes. Interceptez ApiException en premier : c'est une IOException, tout comme HttpTimeoutException, ce qui permet au dernier catch de couvrir les délais dépassés et les connexions coupées. La limite de débit s'applique par espace de travail, pas par clé : ajouter des clés ne lèvera donc pas un 429.

Assembler les couches pour valider une adresse e-mail en Java#

Le moins cher d'abord, arrêtez-vous dès que vous avez une réponse. Un petit record porte le verdict (les records sont disponibles à partir de Java 16 ; sur 11–15, utilisez une classe ordinaire) :

// 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()));
}

Les deux couches locales ne coûtent rien et attrapent l'essentiel des déchets instantanément ; l'API ne s'exécute que sur les adresses qui valent l'aller-retour réseau. Cet ordonnancement est toute l'astuce — la même structure que vous retrouverez dans les versions Node.js, Python, PHP, Go et C# de ce guide, parce que c'est le découpage en couches, et non le langage, qui fait fonctionner la validation.

Pour une liste entière, ne bouclez pas sur /verify ligne par ligne d'un CSV : envoyez-la à POST /verify/bulk (jusqu'à 1 000 000 de lignes) ou importez-la dans l'application, comme le décrit le guide sur le nettoyage de liste.

Foire aux questions

01InternetAddress.validate() suffit-il pour valider un e-mail en Java ?

Pour la syntaxe, c'est le bon outil de couche 1 : jakarta.mail.internet.InternetAddress avec validate() est mieux éprouvé qu'une regex maison et vous épargne de réimplémenter la RFC 822. Mais il ne valide que la forme. Il ne résout jamais le DNS ni ne contacte de serveur de messagerie, et il est assez permissif pour accepter des domaines sans point comme jane@localhost. Il analyse aussi la forme avec nom d'affichage Jane <jane@example.com> : vérifiez donc que getAddress() est égal à la saisie. Associez-le ensuite à une recherche MX et à une vérification de boîte aux lettres par SMTP avant de faire confiance à l'adresse.

02Quelle regex utiliser pour valider une adresse e-mail en Java ?

Une regex courte et permissive, compilée une seule fois en static final Pattern et testée avec matcher(email).matches(), qui doit couvrir toute la chaîne (find() accepte une correspondance n'importe où à l'intérieur). Le motif de ce guide exige un seul @, aucun espace, aucune partie vide entre les points et au moins un point dans le domaine. Ne plafonnez pas la longueur du TLD avec quelque chose comme {2,7} : de vrais TLD comme .photography sont plus longs. Une regex ne vérifie que la forme, elle ne peut donc pas vous dire si le domaine ou la boîte aux lettres existe ; pourquoi la validation d'e-mail par regex échoue en explique les limites.

03Comment vérifier les enregistrements MX en Java sans bibliothèque externe ?

Utilisez le fournisseur DNS JNDI intégré au JDK. Créez un InitialDirContext avec java.naming.factory.initial défini sur com.sun.jndi.dns.DnsContextFactory, puis appelez getAttributes(domain, new String[] {"MX"}) et vérifiez l'attribut MX retourné. S'il n'y a pas de MX, interrogez aussi "A" et "AAAA" (un type par appel), car le courrier peut se replier sur les enregistrements d'adresse du domaine lui-même. Considérez une NameNotFoundException comme un domaine inexistant, mais ne rejetez pas l'adresse sur un délai dépassé ou une autre NamingException : c'est un problème de résolveur, pas une mauvaise adresse.

04Puis-je vérifier une boîte aux lettres en Java sans API ?

En partie. Jakarta Mail et JNDI confirment que l'adresse est bien formée et que le domaine accepte le courrier - le tout gratuitement et avec peu de dépendances. Confirmer la boîte aux lettres elle-même implique une conversation SMTP, que vous pouvez tenter avec un Socket brut mais ne devriez pas exécuter depuis votre serveur applicatif : le port 25 est largement bloqué et le résultat dépend de la réputation de votre IP d'envoi. C'est cette dernière couche que gère un service de vérification.

05Faut-il valider les e-mails à l'inscription ou lors du nettoyage d'une liste ?

Les deux, à des profondeurs différentes. Exécutez les couches syntaxe et MX de façon synchrone à l'inscription - elles sont assez rapides pour bloquer la requête et donner un retour instantané - et agissez aussi sur le verdict immédiat de l'API à cet endroit (rejetez undeliverable, laissez passer un unknown en attente), en confiant le verdict de boîte aux lettres à un worker en arrière-plan. Réservez le travail par lots plus poussé au nettoyage de liste ; le guide sur l'inscription serverless montre le motif en temps réel, et le guide sur le nettoyage de liste couvre le volet par lots.


Prêt à ajouter la couche SMTP ? Passez n'importe quelle adresse dans le vérificateur d'e-mails gratuit pour voir une chaîne syntaxiquement parfaite revenir avec un vrai verdict, ou lisez la référence de l'API pour l'endpoint /verify, l'enveloppe result complète, et des exemples prêts à copier-coller dans tous les langages ci-dessus.

Votre réputation, protégée.

Nettoyez votre première liste en quelques minutes. 100 crédits gratuits, sans carte bancaire.

Commencer