Para validar um endereço de e-mail no Flask você executa três checagens, não uma —
e o próprio Flask não traz nenhuma delas, porque o Flask não tem uma camada de
formulários própria. O que ele tem é um ecossistema: o WTForms (normalmente pelo
Flask-WTF) para o tratamento de formulários e o pacote email-validator em que o
WTForms se apoia. Juntos, eles respondem às duas primeiras das três perguntas da
validação — o endereço está formatado corretamente e o domínio dele consegue
receber mensagens — mas nunca à terceira: a caixa postal realmente existe. A
validação de verdade é feita em camadas: sintaxe, depois uma consulta DNS e, então,
uma sondagem SMTP da caixa postal. Este guia constrói cada camada como um validador
do WTForms que você pode encaixar em um formulário Flask-WTF ou em uma rota JSON
simples, e mostra onde uma API de verificação assume o controle. Ele continua de
onde o guia de Python parou, então o
raciocínio de DNS e SMTP de lá permanece válido na íntegra.
A resposta curta#
Use o validador Email() do WTForms para a sintaxe, sua opção
check_deliverability=True (ou o dnspython) para a consulta MX e uma API de
verificação para a checagem SMTP da caixa postal — ligados do mais barato ao mais
caro, para que cada camada faça curto-circuito antes de a próxima rodar. Não abra
conexões SMTP a partir do seu processo Flask para sondar caixas postais: a porta 25
de saída é bloqueada na maioria dos hosts, e a resposta depende da reputação do IP
remetente e do greylisting, que você não vai querer reimplementar. Cada camada
descarta endereços fora de forma mais barata que a anterior; só a API consegue
aprovar um dentro. Se esse fluxo é novo para você,
o que é a verificação de e-mail explica os termos
primeiro.
Camada 1: validação de formato com o validador Email do WTForms#
O WTForms é dono da camada um, e não a faz na mão. O wtforms.validators.Email
delega ao pacote email-validator, então declarar um validador Email() em um campo
te dá uma checagem de sintaxe de verdade, derivada da RFC — não um regex — de graça:
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email
class SignupForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
O validador Email() precisa do pacote instalado (pip install email-validator,
ou pip install wtforms[email]); sem ele, o WTForms levanta um erro no momento da
validação avisando disso. Por baixo dos panos, ele chama
email_validator.validate_email, que você também pode usar de forma isolada — fora
de um formulário, ou em uma camada de serviço — para obter o mesmo veredito como um
booleano:
from email_validator import validate_email, EmailNotValidError
def is_valid_syntax(email: str) -> bool:
try:
validate_email(email, check_deliverability=False)
return True
except EmailNotValidError:
return False
Saiba exatamente o que isso te garante. Com check_deliverability=False, a checagem
lê apenas o formato — ela nunca resolve DNS, nunca abre um socket e não tem ideia se
example.com existe. definitely-fake@gmail.com passa. info@company-that-folded.com
passa. typo@gmial.com passa. Todos os três são inentregáveis, e nenhum validador que
apenas lê a string jamais vai te dizer isso, pelo mesmo motivo que
a validação de e-mail por regex falha: sintaxe
e entregabilidade são perguntas diferentes. Uma é um fato sobre a string; a outra é um
fato sobre a internet.
Camada 2: o domínio aceita mensagens?#
Um domínio sem registros MX não consegue aceitar mensagens para ninguém, então uma
consulta DNS elimina domínios mortos, nomes de empresas com erro de digitação e TLDs
inventados. Aqui a stack do Flask faz algo que a do Django não faz de fábrica: o
email-validator pode executar a consulta para você. Ative o check_deliverability e
ele resolve os registros MX do domínio (recorrendo a A/AAAA) com o dnspython,
levantando EmailUndeliverableError — uma subclasse de EmailNotValidError — quando o
domínio não consegue receber mensagens. Em um formulário, isso é uma opção no validador
que você já tem:
email = StringField("Email", validators=[DataRequired(), Email(check_deliverability=True)])
Essa única flag dobra as camadas um e dois em um só validador. De forma isolada, a mesma chamada retorna um objeto de endereço normalizado quando o domínio passa na checagem:
from email_validator import validate_email, EmailNotValidError
def has_mail_route(email: str) -> bool:
try:
validate_email(email, check_deliverability=True) # syntax + live MX lookup
return True
except EmailNotValidError:
return False
Se você preferir rodar a checagem de DNS por conta própria — para armazenar
resultados em cache, apontar para o seu próprio resolver ou definir um timeout curto —
vá direto ao dnspython:
import dns.resolver # pip install dnspython
def domain_has_mx(domain: str) -> bool:
try:
return len(dns.resolver.resolve(domain, "MX")) > 0
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return False
Camada 3: a caixa postal realmente existe?#
As camadas um e dois só conseguem descartar um endereço fora. Um domínio pode
publicar registros MX perfeitos e mesmo assim não ter caixa postal no endereço que
você tem em mãos — noreply-9f2x@gmail.com é sintaxe válida em uma rota de e-mail
ativa, e ainda assim é uma caixa postal que nunca foi criada. Confirmar uma caixa
postal específica significa a conversa de entrega SMTP: conectar-se ao host de e-mail,
emitir RCPT TO, ler a resposta e desconectar antes de enviar qualquer coisa.
Em princípio, você poderia programar isso a partir do Flask com o smtplib. Na
prática, você não deveria executá-lo a partir do seu servidor de aplicação: a maioria
dos hosts em nuvem bloqueia a porta 25 de saída, a resposta depende da reputação do IP
de onde você conecta e os servidores receptores fazem greylisting e limitam a taxa de
remetentes desconhecidos — então uma sondagem que funciona em um teste local falha
silenciosamente, ou te coloca em uma blocklist, em produção.
Como funciona a verificação de e-mail percorre o
pipeline completo, domínios catch-all e tudo mais. Esta é a camada que vale a pena
delegar.
O endpoint de verificação do Qualisend executa o pipeline inteiro — sintaxe, DNS e a
sondagem SMTP da caixa postal — a partir de uma infraestrutura com reputação gerenciada
e retorna um veredito. Chame-o com o requests, lendo sua chave a partir do ambiente:
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"}
A resposta é um envelope { "result": { ... } }. Leia result["status"] —
deliverable, risky, undeliverable ou unknown — junto com um score, um
reason e sub_flags para características como endereços de função (role) ou
descartáveis. Consulte a referência da API para os formatos exatos dos
campos; aqui você só precisa do status para decidir. Rejeitar undeliverable é o
padrão seguro; você também pode rejeitar risky, ou armazenar o score e deixar
passar — essa é uma decisão de política, e o
guia de cadastro serverless trata do quão
rigoroso ser no ponto de coleta.
Juntando as camadas para validar um endereço de e-mail no Flask#
O lugar idiomático para adicionar uma checagem por campo no Flask-WTF é um método
inline validate_<fieldname> no formulário. O WTForms anexa esse método à cadeia de
validadores do campo e o executa depois dos validadores da lista, então, no momento
em que ele dispara, o Email(check_deliverability=True) já resolveu a sintaxe e a
consulta MX. Proteja com field.errors para que a ida e volta à API só aconteça para
endereços que passaram nas duas camadas mais baratas:
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email, ValidationError
class SignupForm(FlaskForm):
email = StringField("Email", validators=[
DataRequired(),
Email(check_deliverability=True), # layers 1 + 2
])
def validate_email(self, field): # layer 3 — runs after the list above
if field.errors: # syntax or MX already failed
return # don't spend an API credit
if verify(field.data)["status"] == "undeliverable":
raise ValidationError("We couldn't confirm a mailbox at this address.")
Aquela proteção field.errors é o truque inteiro. Um endereço malformado ou um domínio
morto registra um erro na cadeia antes de o método inline rodar, então a proteção impede
a chamada de verificação em endereços que nunca tiveram chance — validação fora de ordem,
ou pular a proteção, gasta um crédito em cada erro de digitação. Em uma view, o
validate_on_submit roda o pipeline inteiro antes de o corpo do seu handler ser
executado:
@app.route("/signup", methods=["POST"])
def signup():
form = SignupForm()
if form.validate_on_submit():
# every layer passed — safe to persist
create_account(form.email.data)
return redirect(url_for("welcome"))
return render_template("signup.html", form=form), 400
Uma coisa a decidir de antemão: o que acontece quando a própria chamada de API falha.
Um timeout de rede ou uma exceção do requests dentro do validate_email não deveria
retornar 500 na requisição nem entregar a um cliente de verdade um erro que ele não
consegue resolver. Capture requests.RequestException separadamente de
ValidationError e trate um serviço inacessível como unknown em vez de
undeliverable — deixe o cadastro passar e reverifique o endereço depois, em vez de
bloquear o registro por causa de uma indisponibilidade transitória. As camadas de
sintaxe e MX já rodaram, então você só está amenizando a camada que depende da rede.
Nem todo app Flask usa o WTForms. Para uma API JSON, as mesmas três camadas ficam
diretamente em uma rota, retornando um 400 em um endereço ruim em vez de levantar
ValidationError:
from flask import Flask, request, jsonify
from email_validator import validate_email, EmailNotValidError
app = Flask(__name__)
@app.post("/api/signup")
def api_signup():
email = (request.get_json(silent=True) or {}).get("email", "")
try:
info = validate_email(email, check_deliverability=True) # layers 1 + 2
except EmailNotValidError as exc:
return jsonify(error=str(exc)), 400
if verify(info.normalized)["status"] == "undeliverable": # layer 3
return jsonify(error="Email address appears undeliverable."), 400
# ... create the account
return jsonify(ok=True), 201
info.normalized é o endereço canonicalizado que o email-validator devolve, que é o
que você deveria armazenar. Este é o mesmo formato de três camadas que você vai
encontrar nas versões deste guia em Node.js e
Python — é o encadeamento das camadas, não o
framework, que faz a validação funcionar. Quando estiver escolhendo qual serviço de
verificação vai dar suporte à camada três, a
comparação de APIs alinha as opções.
Perguntas frequentes#
O validador Email do WTForms é suficiente para validar um endereço de e-mail?#
Para a sintaxe, sim — o Email() delega ao pacote email-validator e é uma checagem
de primeira camada melhor do que qualquer regex feito à mão. Mas, por padrão, ele valida
o formato, não a entregabilidade. Adicione check_deliverability=True e ele também
executa uma consulta MX ao vivo, que descarta domínios mortos; mesmo assim, ele nunca
contata a caixa postal, então um resultado positivo significa "o domínio aceita e-mails",
não "este endereço existe". Combine-o com uma verificação SMTP da caixa postal antes de
confiar no endereço.
Como adiciono um validador de e-mail personalizado no Flask-WTF?#
Duas opções idiomáticas. Para uma checagem pontual, adicione um método inline chamado
validate_<fieldname> ao formulário — por exemplo, validate_email(self, field) — e
levante wtforms.validators.ValidationError quando o valor falhar; o WTForms o executa
depois dos validadores listados do campo. Para uma regra reutilizável, escreva um
callable que recebe (form, field) e levanta o mesmo erro, e então passe-o na lista de
validators do campo. Qualquer uma das duas é o lugar da chamada de verificação do
Qualisend.
Dá para verificar se uma caixa postal existe no Flask sem uma API?#
Em parte. O email-validator com check_deliverability=True (ou o dnspython
diretamente) confirma que o domínio aceita e-mails, o que descarta domínios mortos de
graça e não precisa de nada além de uma pequena dependência. Confirmar a caixa postal
significa uma conversa SMTP que você pode tentar com o smtplib, mas não deveria
executar a partir do seu servidor de aplicação — a porta 25 é amplamente bloqueada e o
resultado depende da reputação do seu IP. Essa é a camada que um serviço de verificação
existe para resolver.
Devo validar e-mails no cadastro ou na hora de limpar uma lista?#
Ambos, em profundidades diferentes. Execute as camadas de sintaxe e MX de forma síncrona no formulário ou na rota — elas são rápidas o suficiente para bloquear a requisição e dar feedback instantâneo — e aja com base no veredito da API ali mesmo. Reserve a verificação em lote, mais pesada, para a limpeza de listas e o trabalho de back-office, onde a latência não importa e você pode processar endereços em massa, em vez de um envio de formulário por vez.
Pronto para adicionar a camada SMTP? Jogue um endereço aprovado pelo Email() no
verificador de e-mail gratuito para ver uma string válida no
WTForms voltar como undeliverable e, então, ligue o mesmo veredito no seu formulário
com a referência da API — exemplos de copiar e colar incluídos.