Para validar una dirección de correo electrónico en Django ejecutas tres comprobaciones, no una, y el framework solo trae la primera de serie. Los validadores de Django confirman que una dirección tiene la forma correcta; nunca preguntan si su dominio puede recibir correo ni si el buzón existe. La validación de verdad va por capas: sintaxis, después una consulta DNS y luego un sondeo del buzón por SMTP. Esta guía construye cada capa como un validador de Django que puedes incorporar a un formulario o a un serializador de Django REST Framework, y muestra dónde toma el relevo una API de verificación. Retoma el hilo donde lo deja la guía de Python, así que el razonamiento sobre DNS y SMTP de allí sigue siendo válido aquí sin cambios.
La respuesta corta#
Usa validate_email (o cualquier EmailField) para la sintaxis, dnspython
para la consulta MX y una API de verificación para la comprobación del buzón por
SMTP, conectados como tres validadores, del más barato al más caro y
cortocircuitando en cuanto uno sea decisivo. No abras conexiones SMTP desde tu
proceso de Django para sondear buzones: el puerto 25 de salida está bloqueado en
la mayoría de los hosts, y la respuesta depende de la reputación de la IP emisora
y del greylisting que no querrás reimplementar. Cada capa descarta direcciones de
forma más barata que la anterior; solo la API puede dar por buena una. Si esta
canalización es nueva para ti, qué es la verificación de
correo explica primero los términos.
Capa 1: validación de formato con los validadores de Django#
Django ya se encarga de la primera capa. django.core.validators.validate_email
es una instancia de EmailValidator que lanza ValidationError ante una
dirección mal formada y devuelve None ante una correcta:
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
def is_valid_syntax(email: str) -> bool:
try:
validate_email(email)
return True
except ValidationError:
return False
Rara vez lo llamas a mano. Cada forms.EmailField, models.EmailField y
serializers.EmailField de DRF adjunta EmailValidator automáticamente, de modo
que declarar un campo te da la validación de sintaxis gratis:
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # EmailValidator runs during clean()
Ten claro qué te ofrece esto exactamente. EmailValidator comprueba la forma
contra una gramática derivada de los RFC: nunca resuelve el DNS, nunca abre un
socket y no tiene ni idea de si example.com existe. definitely-fake@gmail.com
pasa. info@company-that-folded.com pasa. typo@gmial.com pasa. Las tres son
inentregables, y ningún validador que solo lea la cadena te lo dirá jamás, por la
misma razón por la que la validación de correo con regex
falla: sintaxis y entregabilidad son
preguntas distintas. Una es un hecho sobre la cadena; la otra es un hecho sobre
internet.
Capa 2: ¿acepta correo el dominio?#
Esta es la primera capa que Django no te da hecha, y es barata de añadir. Un
dominio sin registros MX no puede aceptar correo para nadie, así que una sola
consulta DNS elimina los dominios muertos, los nombres de empresa mal escritos y
los TLD inventados. Django no incluye un resolver de MX, así que recurre a
dnspython y envuélvelo en una función corriente:
import dns.resolver # pip install dnspython
def has_mail_route(domain: str) -> bool:
try:
return len(dns.resolver.resolve(domain, "MX")) > 0
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return False
Un validador de Django no es más que un invocable que lanza ValidationError
ante una entrada incorrecta, así que asciende la comprobación a validador y encaja
directamente en la lista validators de cualquier campo:
from django.core.exceptions import ValidationError
def validate_deliverable_domain(email: str) -> None:
domain = email.rsplit("@", 1)[-1]
if not has_mail_route(domain):
raise ValidationError("This domain can't receive email.")
validate_deliverable_domain("jane@gmail.com") # passes
validate_deliverable_domain("jane@company-that-folded.com") # raises ValidationError
Si quieres respetar los dominios que aceptan correo en un registro A sin MX,
recurre a resolver A/AAAA cuando el conjunto de MX esté vacío, pero una
comprobación de MX cubre la inmensa mayoría de las direcciones reales.
Capa 3: ¿existe realmente el buzón?#
Las capas uno y dos solo pueden descartar una dirección. Un dominio puede
publicar registros MX perfectos y aun así no tener ningún buzón en la dirección
que manejas: noreply-9f2x@gmail.com tiene una sintaxis válida sobre una ruta de
correo activa, y sigue siendo un buzón que nunca se creó. Confirmar un buzón
concreto implica la conversación de entrega SMTP: conectarse al host de correo,
emitir RCPT TO, leer la respuesta y desconectarse antes de enviar nada.
En principio podrías programar eso desde Django con smtplib. En la práctica no
deberías ejecutarlo desde el servidor de tu aplicación: la mayoría de los hosts en
la nube bloquean el puerto 25 de salida, la respuesta depende de la reputación de
la IP desde la que te conectas, y los servidores receptores aplican greylisting
y limitan el ritmo de los remitentes desconocidos, de modo que un sondeo que
funciona en una prueba local falla en silencio, o hace que te incluyan en una
lista de bloqueo, en producción. Cómo funciona la verificación de
correo recorre la canalización completa, con
dominios catch-all incluidos. Esta es la capa que merece la pena delegar.
El endpoint de verificación de Qualisend ejecuta toda la canalización —sintaxis,
DNS y el sondeo del buzón por SMTP— desde una infraestructura con reputación
gestionada y devuelve un veredicto. Llámalo desde un validador con requests,
leyendo tu clave del entorno:
import os
import requests # pip install requests
QUALISEND_VERIFY_URL = "https://api.qualisend.com/v1/verify"
def verify(email: str) -> dict:
res = requests.post(
QUALISEND_VERIFY_URL,
headers={"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"},
json={"email": email},
timeout=10,
)
res.raise_for_status()
return res.json()["result"] # {"status", "score", "reason", "sub_flags"}
La respuesta es un sobre { "result": { ... } }. Lee result["status"]
—deliverable, risky, undeliverable o unknown— junto a un score, un
reason y sub_flags para rasgos como las direcciones de rol o desechables.
Consulta la referencia de la API para conocer la forma exacta de
los campos; aquí solo necesitas el estado para lanzar un error ante una dirección
que no se va a entregar:
def validate_mailbox(email: str) -> None:
if verify(email)["status"] == "undeliverable":
raise ValidationError("We couldn't confirm a mailbox at this address.")
También podrías lanzar un error ante risky, o guardar el score y dejarla
pasar: eso es una decisión de política. Rechazar undeliverable es la opción
segura por defecto; la guía de registro serverless
explica cuán estricto conviene ser en el punto de recogida.
Cómo juntar las capas para validar una dirección de correo en Django#
El lugar idiomático para componer las tres capas en un formulario es un método
clean_<field>. Django llama a clean_email solo después de que el propio
EmailValidator del campo haya pasado, así que la sintaxis ya está resuelta; tú
añades las comprobaciones de dominio y de buzón en orden, cada una lanzando el
error cuanto antes:
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # layer 1 for free
def clean_email(self):
email = self.cleaned_data["email"] # syntax already passed
validate_deliverable_domain(email) # layer 2 — cheap, local
validate_mailbox(email) # layer 3 — the API round-trip
return email
Ese orden es todo el truco: el EmailValidator del campo cortocircuita la basura
antes de que se ejecute tu código, la comprobación DNS local descarta gratis los
dominios muertos, y solo se llama a la API para las direcciones que superaron
ambas. Validar en el orden equivocado —o saltarse las capas baratas— gasta un
crédito en cada errata.
Una cosa que decidir de antemano: qué pasa cuando falla la propia llamada a la
API. Un tiempo de espera de red o una excepción de requests dentro de
clean_email no debería devolverle a un cliente real un error de validación que
no puede corregir. Captura el fallo de la petición por separado del
ValidationError, y trata un servicio inalcanzable como unknown en lugar de
undeliverable: deja pasar el registro y vuelve a verificar la dirección más
tarde, en vez de bloquear el alta por una caída transitoria. Las capas de sintaxis
y MX ya se ejecutaron localmente, así que solo estás suavizando la capa que
depende de la red.
Como ambos validadores personalizados lanzan el ValidationError de Django, esas
mismas dos funciones se incorporan sin cambios a un serializador de Django REST
Framework. El hook validate_<field> de DRF captura esa excepción y la convierte
en un 400 limpio:
from rest_framework import serializers
class SignupSerializer(serializers.Serializer):
email = serializers.EmailField() # layer 1 for free
def validate_email(self, value):
validate_deliverable_domain(value) # layer 2
validate_mailbox(value) # layer 3
return value
Esta es la misma estructura de tres capas que encontrarás en las versiones para Node.js y Python de esta guía: es el sistema de capas, no el framework, lo que hace que la validación funcione. Cuando elijas qué servicio de verificación respalda la tercera capa, la comparativa de APIs pone las opciones una al lado de la otra.
Preguntas frecuentes#
¿Basta el EmailValidator de Django para validar una dirección de correo?#
Para la sintaxis, sí: validate_email (y todo EmailField que lo usa) es el
control de primera capa adecuado y mejor que una expresión regular hecha a mano.
Pero valida la forma, no la entregabilidad: nunca resuelve el DNS ni contacta
con un servidor de correo, así que un aprobado significa "parece un correo", no "se
va a entregar". Combínalo con una consulta MX y una comprobación del buzón por SMTP
antes de fiarte de la dirección.
¿Cómo escribo un validador de correo personalizado en Django?#
Un validador es cualquier invocable que recibe el valor y lanza
django.core.exceptions.ValidationError en caso de fallo. Define una función como
validate_deliverable_domain(email) que lance el error cuando haya un problema y
devuelva None en caso contrario, y luego pásala en la lista validators=[...]
del campo o llámala desde el método clean_email de un formulario. El mismo
invocable funciona en el hook validate_email de un serializador de DRF, porque
DRF también captura el ValidationError de Django.
¿Puedo comprobar si existe un buzón desde Django sin una API?#
En parte. dnspython confirma que el dominio acepta correo, lo que descarta gratis
los dominios muertos y no necesita nada más que una dependencia pequeña. Confirmar
el buzón implica una conversación SMTP que puedes intentar con smtplib pero que
no deberías ejecutar desde el servidor de tu aplicación: el puerto 25 está bloqueado
en muchos sitios y el resultado depende de la reputación de tu IP. Esa es la capa
que un servicio de verificación existe para gestionar.
¿Debo ejecutar estas comprobaciones en el registro o al limpiar una lista?#
Ambas cosas, con profundidades distintas. Ejecuta las capas de sintaxis y MX de
forma síncrona en clean_email —son lo bastante rápidas para bloquear la petición
y dar respuesta al instante— y actúa también ahí sobre el veredicto de la API.
Reserva la verificación por lotes, más pesada, para la limpieza de listas y el
trabajo de trastienda, donde la latencia da igual y puedes procesar direcciones en
bloque.
¿Listo para añadir la capa SMTP? Introduce una dirección sintácticamente perfecta
en el comprobador de correo gratuito para ver cómo una
cadena aprobada por EmailField vuelve como undeliverable, y luego conecta ese
mismo veredicto a tus formularios con la referencia de la API, con
ejemplos para copiar y pegar incluidos.