Skip to content
Comece com 100 créditos de verificação grátis
Qualisend
Todos os artigos
Engenharia / 10 de maio de 2026

Como validar um endereço de e-mail no Flask

9 minutes read

Qualisend team
Janela de código intitulada forms.py mostrando três camadas de validação do Flask que vão afunilando — Email(), uma consulta MX com check_deliverability e um POST para a API de verificação — terminando em um selo verde de entregável.

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.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started