Verificación de email en Django
Este tutorial muestra cómo añadir verificación de email en tiempo real a un proyecto Django. Construirás un ayudante reutilizable y lo conectarás tanto a un formulario clásico de Django como a un serializer de Django REST Framework (DRF), de forma que las direcciones inválidas, desechables y no entregables se rechacen antes de crear ningún usuario.
Qué vas a construir
- Un ayudante
verify_emailque envuelve la API de Mailbeam y falla en abierto ante errores - Un validador
validate_email_deliverablepara formularios de Django - Un validador de campo de serializer de DRF para proyectos orientados a API
Requisitos previos
- Python 3.9+ y Django 4.2 o posterior
- Una clave de API de Mailbeam (regístrate gratis)
python-dotenv(o el cargador de settings que prefieras)
Paso 1 — Añade un cliente HTTP
No hay ningún SDK que instalar. Mailbeam es un único endpoint HTTP, así que la integración de abajo es todo el cliente que necesitas. Los SDK oficiales están en la hoja de ruta.
pip install httpx # o usa requests, si ya está en el proyectoPaso 2 — Configura los settings
Deja la clave fuera del control de versiones. Cárgala desde el entorno en settings.py:
# settings.py
import os
MAILBEAM_KEY = os.environ["MAILBEAM_KEY"]
# Puntuación mínima de calidad para aceptar (0-100). 60 es un valor razonable.
MAILBEAM_MIN_SCORE = int(os.environ.get("MAILBEAM_MIN_SCORE", "60"))# .env
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxxPaso 3 — Escribe un ayudante de verificación reutilizable
Pon la integración en un solo sitio para que formularios, serializers y vistas compartan el mismo comportamiento.
# accounts/verification.py
import logging
import httpx
from django.conf import settings
logger = logging.getLogger(__name__)
ENDPOINT = "https://api.mailbeam.dev/v1/verify"
def verify_email(email: str) -> tuple[bool, str | None]:
"""
Devuelve (es_aceptable, codigo_de_motivo).
Falla EN ABIERTO (devuelve True) ante errores de la API de Mailbeam, para
que una caída no bloquee nunca registros legítimos.
"""
try:
response = httpx.post(
ENDPOINT,
json={"email": email.strip().lower()},
headers={"Authorization": f"Bearer {settings.MAILBEAM_KEY}"},
timeout=3.0,
)
response.raise_for_status()
result = response.json()
except httpx.HTTPError as exc:
logger.error("Ha fallado la verificación de Mailbeam: %s", exc)
return True, None # falla en abierto
if not result["valid"] or result["score"] < settings.MAILBEAM_MIN_SCORE:
return False, result["reason"] or "invalid_email"
return True, NonePaso 4 — Úsalo en un formulario de Django
# accounts/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .verification import verify_email
class SignupForm(forms.Form):
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
def clean_email(self):
email = self.cleaned_data["email"]
ok, reason = verify_email(email)
if not ok:
raise ValidationError(
"Escribe una dirección de email válida y entregable.",
code=reason,
)
return emailDjango ejecuta clean_email automáticamente durante form.is_valid(), así que tu vista no cambia:
# accounts/views.py
from django.shortcuts import render, redirect
from .forms import SignupForm
def signup(request):
if request.method == "POST":
form = SignupForm(request.POST)
if form.is_valid():
create_user(email=form.cleaned_data["email"])
return redirect("welcome")
else:
form = SignupForm()
return render(request, "accounts/signup.html", {"form": form})Paso 5 — Úsalo en un serializer de DRF
En proyectos orientados a API, valida dentro del serializer:
# accounts/serializers.py
from rest_framework import serializers
from .verification import verify_email
class SignupSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
def validate_email(self, value):
ok, reason = verify_email(value)
if not ok:
raise serializers.ValidationError(
f"Email rechazado: {reason}"
)
return value# accounts/views.py (DRF)
from rest_framework.generics import CreateAPIView
from .serializers import SignupSerializer
class SignupView(CreateAPIView):
serializer_class = SignupSerializer
# DRF devuelve un 400 con los errores de campo automáticamente si falla la validaciónPaso 6 — Añade caché
Evita reverificar la misma dirección (por ejemplo, cuando alguien reintenta el formulario). El framework de caché de Django encaja bien:
# accounts/verification.py
from django.core.cache import cache
def verify_email(email: str) -> tuple[bool, str | None]:
normalized = email.strip().lower()
cached = cache.get(f"mb:{normalized}")
if cached is not None:
return cached
try:
result = _client.verify_sync(normalized)
ok = result.valid and result.score >= settings.MAILBEAM_MIN_SCORE
outcome = (ok, None if ok else (result.reason or "invalid_email"))
cache.set(f"mb:{normalized}", outcome, timeout=86_400) # 24 h
return outcome
except mailbeam.APIError as exc:
logger.error("Ha fallado la verificación de Mailbeam: %s", exc)
return True, None # falla en abierto (no caches los fallos)Pruebas
Los dominios de prueba de Mailbeam devuelven resultados deterministas y no consumen cuota:
# accounts/tests.py
from django.test import TestCase
from .forms import SignupForm
class SignupFormTests(TestCase):
def test_valid_email_passes(self):
form = SignupForm(data={
"email": "user@valid.mailbeam-test.dev",
"password": "pass1234",
})
self.assertTrue(form.is_valid())
def test_disposable_email_rejected(self):
form = SignupForm(data={
"email": "temp@disposable.mailbeam-test.dev",
"password": "pass1234",
})
self.assertFalse(form.is_valid())
self.assertIn("email", form.errors)Buenas prácticas
| Práctica | Por qué |
|---|---|
Centralizar en verification.py | Formularios, serializers y vistas se comportan igual |
Fallar en abierto ante mailbeam.APIError | Una caída no debería romper los registros |
Normalizar (strip().lower()) | Caché coherente y menos consultas duplicadas |
| Cachear ~24 h | Reduce las llamadas a la API en reintentos y reenvíos |
| Usar dominios de prueba | Tests deterministas y sin consumo de cuota |
Checklist de producción
-
MAILBEAM_KEYcargada desde el entorno, no subida al repositorio - Registro de errores configurado para
mailbeam.APIError - Backend de caché definido (Redis o Memcached en producción)
-
MAILBEAM_MIN_SCOREajustado a tu embudo - El mensaje de error que ve el usuario es amable en tus plantillas