Verificación de email en Go
Este tutorial añade verificación de email a un servidor HTTP en Go usando primitivas de la biblioteca estándar y un cliente REST ligero de Mailbeam.
Requisitos previos
- Go 1.21+
- Una clave de API de Mailbeam (regístrate gratis)
El cliente de 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: estado inesperado %d", resp.StatusCode)
}
var result VerifyResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}Middleware de verificación
// 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 lee el email del cuerpo JSON y lo verifica.
// Falla en abierto ante errores de la 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":"cuerpo de petición no válido"}`, http.StatusBadRequest)
return
}
email := strings.ToLower(strings.TrimSpace(body.Email))
if email == "" {
http.Error(w, `{"error":"el campo email es obligatorio"}`, http.StatusBadRequest)
return
}
// Mira en la caché
if cached, ok := cache.Load(email); ok {
result := cached.(cachedResult).result
if !result.Valid || result.Score < 60 {
writeError(w, result.Reason)
return
}
// Devuelve el cuerpo por el contexto
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), emailKey{}, email),
))
return
}
// Llama a Mailbeam
result, err := mbClient.Verify(r.Context(), email)
if err != nil {
log.Printf("[mailbeam] error de verificación: %v", err)
// Falla en abierto: continúa sin bloquear
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 := "Escribe una dirección de email válida."
if reason != nil {
switch *reason {
case "disposable_domain":
msg = "Usa un email permanente, no uno temporal."
case "no_mx_records":
msg = "Este dominio no puede recibir correo."
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}Regístralo en tu handler de registro
// 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())
// crea el usuario con el email verificado...
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"email": email})
}Buenas prácticas
sync.Mapes seguro para uso concurrente, pero no tiene límite: añade un TTL con una limpieza periódica o usa una caché de verdad comoristretto- Mantén el tiempo de espera en 5 segundos para que ningún registro se eternice
- Falla siempre en abierto: registra los errores pero llama a
next.ServeHTTP