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

Comment valider une adresse e-mail en Go

8 minutes read

Qualisend team
Un schéma en couches d'une adresse e-mail Go traversant la validation de syntaxe net/mail.ParseAddress, une recherche MX net.LookupMX et une sonde SMTP de boîte aux lettres, se resserrant vers un unique verdict de délivrabilité

Valider une adresse e-mail en Go, ce sont trois tâches sous un seul nom. Les tutoriels qui vous montrent une regex et s'arrêtent là n'en ont résolu qu'une seule — et la moins utile qui soit. La vraie validation se fait en couches : un contrôle de syntaxe peu coûteux, une recherche DNS et une sonde SMTP de la boîte aux lettres, du moins cher au plus cher. La bibliothèque standard de Go couvre proprement les deux premières avec net/mail et net, sans aucune dépendance ; la troisième est un problème réseau qu'il vaut mieux déléguer. Ce guide construit chaque couche avec du Go fonctionnel et idiomatique, et montre précisément où une API de vérification prend le relais.

La réponse courte#

Utilisez net/mail.ParseAddress pour la syntaxe, net.LookupMX pour la recherche MX et une API de vérification pour le contrôle SMTP de la boîte aux lettres — dans cet ordre, du moins cher au plus cher, en court-circuitant dès qu'une couche est décisive. Ne vous tournez pas vers net/smtp pour sonder les boîtes aux lettres depuis votre propre serveur : le port 25 sortant est bloqué sur la plupart des hébergeurs et, même là où il ne l'est pas, la réponse dépend de la réputation de l'IP d'envoi et d'un comportement de 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 valider une adresse.

Couche 1 : la syntaxe avec net/mail#

Go n'a aucun besoin d'une regex maison ici. Le paquet net/mail de la bibliothèque standard analyse les adresses RFC 5322, et ParseAddress renvoie une erreur non nil sur tout ce qui est malformé — ce qui est exactement le signal oui/non dont a besoin un filtre de syntaxe :

package main

import "net/mail"

func isValidSyntax(email string) bool {
	if len(email) > 320 {
		return false
	}
	addr, err := mail.ParseAddress(email)
	return err == nil && addr.Address == email
}

La comparaison addr.Address == email est la partie que la plupart des exemples oublient. ParseAddress est conçu pour lire une ligne de boîte aux lettres complète, il accepte donc volontiers un nom affiché : mail.ParseAddress("Jane Doe <jane@example.com>") ne renvoie aucune erreur, avec addr.Address défini à jane@example.com. Pour un champ d'inscription, vous voulez l'adresse nue et rien d'autre : comparez donc l'Address analysée à l'entrée et rejetez tout ce qui portait un nom, des chevrons ou des espaces en fin de chaîne :

isValidSyntax("jane@example.com")            // true
isValidSyntax("Jane Doe <jane@example.com>") // false — not a bare address
isValidSyntax("not-an-email")                // false — ParseAddress errors
isValidSyntax("a@@b.com")                    // false

Un succès ici signifie « vaut la peine d'être vérifiée », pas « valide ». Il vous indique que la chaîne a la forme d'une adresse ; il ne dit rien sur la question de savoir si le courrier arrivera. C'est le même mur contre lequel se heurte toute vérification d'e-mail par regex, et net/mail se trouve du même côté — c'est un analyseur, pas un oracle de délivrabilité. Toutes les couches ci-dessous supposent que la syntaxe est déjà saine.

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

C'est ici que la bibliothèque standard fait ses preuves, sans aucune dépendance. Un domaine qui ne publie aucun enregistrement MX n'a nulle part où le courrier puisse atterrir, si bien qu'une seule recherche élimine les domaines morts, les noms d'entreprise mal orthographiés et les TLD inventés. net.LookupMX les résout :

package main

import "net"

func hasMailRoute(domain string) bool {
	mxs, err := net.LookupMX(domain)
	if err != nil {
		return false
	}
	return len(mxs) > 0
}
hasMailRoute("gmail.com")               // true
hasMailRoute("company-that-folded.com") // false

LookupMX renvoie une tranche de *net.MX triée par préférence, ou une *net.DNSError lorsque le domaine ne se résout pas. Traiter toute erreur — ou une tranche vide — comme « aucune route de messagerie » est la lecture prudente pour un validateur. Si vous voulez tenir compte des domaines qui acceptent le courrier sur un enregistrement A sans MX explicite (ce que l'on appelle le MX implicite), repliez-vous sur net.LookupHost(domain) lorsque la tranche MX revient vide. Pour l'écrasante majorité des adresses réelles, cependant, un contrôle MX est le bon filtre.

Une note opérationnelle : LookupMX se bloque sur votre résolveur DNS, si bien qu'un serveur de noms lent ou injoignable fige la goroutine. Dans un chemin de requête, utilisez le résolveur tenant compte du contexte — (&net.Resolver{}).LookupMX(ctx, domain) — avec une échéance, et traitez un dépassement de délai comme « à revérifier plus tard », pas comme un rejet ferme, afin qu'un incident DNS passager ne fasse jamais fuir un vrai client.

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 tout de même aucune boîte aux lettres à l'adresse que vous détenez — noreply-9f2x@gmail.com a une syntaxe valide sur une route de messagerie active et n'a jamais été une véritable boîte aux lettres. Confirmer une boîte aux lettres précise implique la conversation de livraison SMTP : se connecter au serveur de messagerie, émettre RCPT TO, lire la réponse et se déconnecter avant d'envoyer quoi que ce soit.

Go fournit les outils pour essayer. net/smtp vous donne smtp.Dial et un Client doté des méthodes Mail, Rcpt et Close, et vous pourriez en principe conduire vous-même cette poignée de main. En pratique, vous ne devriez pas l'exécuter depuis votre serveur applicatif. La plupart des fournisseurs cloud bloquent purement et simplement le port 25 sortant, si bien que la connexion expire tout simplement en production, même si elle fonctionnait sur votre ordinateur portable. Là où le port est ouvert, la réponse dépend de la réputation de l'IP depuis laquelle vous vous connectez, et les serveurs destinataires font du greylisting et limitent le débit des expéditeurs inconnus — si bien qu'une sonde naïve se voit différée, étranglée ou mise sur liste de blocage. Il y a plus qu'un seul aller-retour, aussi : les domaines catch-all acceptent toutes les adresses et déjouent un unique RCPT TO. Comment fonctionne la vérification d'e-mails parcourt tout le pipeline. C'est la couche qu'il vaut la peine de déléguer.

Effectuer le contrôle complet avec une API#

L'API en temps réel de Qualisend exécute tout le pipeline — syntaxe, DNS et la sonde SMTP de la boîte aux lettres — depuis une infrastructure à réputation gérée conçue pour cela, et renvoie un verdict. C'est un simple POST JSON, donc net/http et encoding/json de la bibliothèque standard sont tout ce dont vous avez besoin. Décodez l'enveloppe result directement dans une structure :

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

type VerifyResult struct {
	Status   string          `json:"status"` // deliverable | risky | undeliverable | unknown
	Score    int             `json:"score"`
	Reason   string          `json:"reason"`
	SubFlags map[string]bool `json:"sub_flags"`
}

type verifyResponse struct {
	Result VerifyResult `json:"result"`
}

func verify(ctx context.Context, email string) (VerifyResult, error) {
	body, err := json.Marshal(map[string]string{"email": email})
	if err != nil {
		return VerifyResult{}, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://api.qualisend.com/v1/verify", bytes.NewReader(body))
	if err != nil {
		return VerifyResult{}, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("QUALISEND_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return VerifyResult{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return VerifyResult{}, fmt.Errorf("qualisend: unexpected status %s", res.Status)
	}

	var payload verifyResponse
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		return VerifyResult{}, fmt.Errorf("qualisend: decode response: %w", err)
	}
	return payload.Result, nil
}

Le point de terminaison et la clé ci-dessus sont des valeurs d'exemple — lisez votre clé à portée limitée depuis la variable d'environnement QUALISEND_API_KEY plutôt que de la coder en dur, et consultez la référence de l'API sur la page développeurs pour connaître la forme exacte de la requête et de la réponse. L'objet result porte un status valant deliverable, risky, undeliverable ou unknown, un score numérique, un reason et une carte sub_flags (adresse de rôle, jetable, fournisseur gratuit, catch-all, et ainsi de suite) — les champs sur lesquels vous vous appuyez réellement pour brancher votre logique.

Comme verify prend un context.Context, l'appelant est maître du délai et de l'annulation. Passez une échéance afin qu'une vérification lente ne fasse jamais patienter une requête d'inscription :

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

result, err := verify(ctx, "jane@example.com")
if err != nil {
	log.Printf("verify failed: %v", err)
}

Tout assembler pour valider une adresse e-mail en Go#

Du moins cher au plus cher, arrêtez-vous dès que vous avez une réponse. Les deux couches locales ne coûtent rien et rejettent instantanément la plupart des déchets ; l'appel à l'API ne s'exécute que sur les adresses qui leur ont survécu et qui valent l'aller-retour réseau :

package main

import (
	"context"
	"strings"
)

func validateEmail(ctx context.Context, email string) (VerifyResult, error) {
	if !isValidSyntax(email) {
		return VerifyResult{Status: "undeliverable", Reason: "invalid_email"}, nil
	}

	domain := email[strings.LastIndex(email, "@")+1:]
	if !hasMailRoute(domain) {
		return VerifyResult{Status: "undeliverable", Reason: "invalid_domain"}, nil
	}

	return verify(ctx, email)
}

Ensuite, branchez votre logique sur le statut selon les besoins de votre produit — rejetez les adresses non délivrables, laissez passer les adresses délivrables et décidez fonctionnalité par fonctionnalité quoi faire des verdicts plus gris :

result, err := validateEmail(ctx, "jane@example.com")
if err != nil {
	// network/API failure — fail open or retry, don't block a real user
}

switch result.Status {
case "deliverable":
	// accept
case "undeliverable":
	// reject at the form
default:
	// risky | unknown — flag for review, or allow with a soft warning
}

Cet ordonnancement est toute l'astuce, et c'est la même structure que vous trouverez dans les versions Node.js et Python de ce guide — c'est le découpage en couches, pas le langage, qui rend la validation fiable. Voyez comment fonctionne la vérification d'e-mails pour comprendre pourquoi chaque étape se situe là où elle est.

Foire aux questions#

net/mail.ParseAddress suffit-il pour valider une adresse e-mail en Go ?#

Pour la syntaxe, oui — net/mail.ParseAddress est le bon contrôle de premier niveau et un bien meilleur choix qu'une regex maison, car il analyse selon la RFC 5322 et renvoie une erreur sur une entrée malformée. Mais il valide la forme, pas la délivrabilité : il ne résout jamais le DNS et ne contacte aucun serveur de messagerie, si bien qu'une erreur nil signifie « ressemble à une adresse », pas « sera délivrée ». Comparez addr.Address à votre entrée pour rejeter les noms affichés, puis associez ce contrôle à une recherche MX et à une sonde SMTP de la boîte aux lettres.

Comment rechercher les enregistrements MX en Go ?#

Utilisez net.LookupMX(domain) de la bibliothèque standard. Elle renvoie une tranche d'enregistrements *net.MX triée par préférence, ou une *net.DNSError lorsque le domaine ne se résout pas. Traitez une erreur ou une tranche vide comme « aucune route de messagerie », et dans un chemin de requête utilisez la version tenant compte du contexte (*net.Resolver).LookupMX(ctx, domain) afin qu'un serveur de noms lent ne puisse pas bloquer la goroutine au-delà de votre échéance.

Puis-je vérifier une boîte aux lettres en Go sans service externe ?#

En partie. net.LookupMX confirme que le domaine accepte le courrier, ce qui écarte gratuitement les domaines morts et ne nécessite rien de plus que la bibliothèque standard. Confirmer la boîte aux lettres elle-même implique une conversation SMTP, que vous pouvez scripter avec net/smtp mais que vous 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 la couche qu'un service de vérification existe pour prendre en charge ; vous pouvez constater la différence sur n'importe quelle adresse avec le vérificateur d'e-mails gratuit.

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

Les deux, à des profondeurs différentes. Exécutez la syntaxe et la recherche 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. Réservez le travail plus poussé, confirmé par SMTP, au nettoyage des listes et aux tâches de back-office ; le guide d'inscription serverless montre le schéma en temps réel de bout en bout.


Prêt à ajouter la couche SMTP à votre service Go ? La référence de l'API Qualisend propose le point de terminaison /verify avec des clés à portée limitée et des exemples à copier-coller, et le vérificateur d'e-mails gratuit vous laisse observer une adresse syntaxiquement parfaite revenir undeliverable avant même que vous n'écriviez une ligne de code.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started