Vérification d'e-mails en Go
Ce tutoriel ajoute la vérification d'e-mails à un serveur HTTP Go, à partir des primitives de la bibliothèque standard et d'un client REST Mailbeam léger.
Prérequis
- Go 1.21+
- Une clé d'API Mailbeam (inscription gratuite)
Le client Mailbeam
// internal/mailbeam/client.go
package mailbeam
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
const baseURL = "https://api.mailbeam.dev"
type Client struct {
apiKey string
httpClient *http.Client
}
type VerifyResult struct {
Valid bool `json:"valid"`
Score int `json:"score"`
Disposable bool `json:"disposable"`
CatchAll bool `json:"catchAll"`
Reason *string `json:"reason"`
LatencyMs int `json:"latency_ms"`
}
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
httpClient: &http.Client{Timeout: 5 * time.Second},
}
}
func (c *Client) Verify(ctx context.Context, email string) (*VerifyResult, error) {
body, _ := json.Marshal(map[string]string{"email": email})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/v1/verify", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("mailbeam: statut inattendu %d", resp.StatusCode)
}
var result VerifyResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}Le middleware de vérification
// internal/middleware/verify_email.go
package middleware
import (
"context"
"encoding/json"
"log"
"net/http"
"strings"
"sync"
"myapp/internal/mailbeam"
)
type cachedResult struct {
result *mailbeam.VerifyResult
}
var (
cache sync.Map
mbClient *mailbeam.Client
)
func Init(apiKey string) {
mbClient = mailbeam.NewClient(apiKey)
}
// VerifyEmailMiddleware lit l'adresse depuis le corps JSON et la vérifie.
// Échec permissif sur les erreurs d'API.
func VerifyEmailMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
return
}
email := strings.ToLower(strings.TrimSpace(body.Email))
if email == "" {
http.Error(w, `{"error":"email is required"}`, http.StatusBadRequest)
return
}
// Consulte le cache
if cached, ok := cache.Load(email); ok {
result := cached.(cachedResult).result
if !result.Valid || result.Score < 60 {
writeError(w, result.Reason)
return
}
// Repasse l'adresse par le contexte
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), emailKey{}, email),
))
return
}
// Appelle Mailbeam
result, err := mbClient.Verify(r.Context(), email)
if err != nil {
log.Printf("[mailbeam] erreur de vérification : %v", err)
// Échec permissif — on continue sans bloquer
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), emailKey{}, email),
))
return
}
cache.Store(email, cachedResult{result: result})
if !result.Valid || result.Score < 60 {
writeError(w, result.Reason)
return
}
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), emailKey{}, email),
))
})
}
type emailKey struct{}
func EmailFromContext(ctx context.Context) string {
v, _ := ctx.Value(emailKey{}).(string)
return v
}
func writeError(w http.ResponseWriter, reason *string) {
msg := "Merci de fournir une adresse e-mail valide."
if reason != nil {
switch *reason {
case "disposable_domain":
msg = "Merci d'utiliser une adresse permanente, pas une adresse temporaire."
case "no_mx_records":
msg = "Ce domaine ne peut pas recevoir de courrier."
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}Le brancher sur votre handler d'inscription
// main.go
package main
import (
"encoding/json"
"net/http"
"os"
"myapp/internal/middleware"
)
func main() {
middleware.Init(os.Getenv("MAILBEAM_KEY"))
mux := http.NewServeMux()
mux.Handle("POST /api/auth/signup",
middleware.VerifyEmailMiddleware(http.HandlerFunc(signupHandler)),
)
http.ListenAndServe(":8080", mux)
}
func signupHandler(w http.ResponseWriter, r *http.Request) {
email := middleware.EmailFromContext(r.Context())
// création de l'utilisateur avec l'adresse vérifiée…
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"email": email})
}Bonnes pratiques
sync.Mapest sûr en accès concurrent mais sans borne : ajoutez un TTL avec un nettoyage périodique, ou passez à un vrai cache commeristretto- Gardez le délai d'expiration à 5 secondes, pour éviter des inscriptions interminables
- Échouez toujours en mode permissif : journalisez l'erreur, mais appelez
next.ServeHTTP