No momento em que você precisa validar um endereço de e-mail no Rails, o Active
Record faz isso parecer trivial — digite validates :email, veja o teste passar,
siga em frente. Mas essa única linha responde apenas a primeira das três perguntas
que uma validação de verdade faz. O endereço tem o formato correto? O domínio dele
consegue receber e-mail? A caixa postal realmente existe? O Rails traz uma resposta
elegante para a primeira e absolutamente nada para as outras duas. Este guia
constrói cada camada como uma validação idiomática do Rails — uma regra de
format, depois duas classes personalizadas ActiveModel::EachValidator — e mostra
exatamente onde o framework para. Ele estende o
guia de validação de e-mail em Ruby: o Rails
envolve os mesmos blocos de construção da biblioteca padrão na API declarativa de
validações.
A resposta curta#
Use validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } para a
sintaxe, um validador personalizado apoiado na biblioteca padrão resolv para a
consulta de MX e uma API de verificação para a checagem de caixa postal via SMTP —
três validações em um mesmo atributo, da mais barata para a mais cara, cada uma
pulando quando uma camada mais barata já falhou. Não abra uma sessão SMTP a partir
da sua aplicação para sondar caixas postais por conta própria: a porta 25 de saída
é bloqueada na maioria dos hosts, e a resposta depende da reputação do IP de envio
e do greylisting que você não vai querer reimplementar. Um detalhe distingue o
Rails de frameworks com uma palavra-chave bail — o Active Model roda todo
validador que você declara e junta todos os erros, então o curto-circuito é uma
guarda que você adiciona, não uma flag que você passa. Cada camada descarta
endereços para fora de forma mais barata que a anterior; só a última consegue
confirmar um endereço como válido.
Camada 1: validação de formato com uma regra validates#
O Rails é dono da camada um, e você não precisa escrever uma regex para usá-la. A
biblioteca uri do Ruby — que o Rails já carrega — traz um padrão bem testado em
URI::MailTo::EMAIL_REGEXP, então entregue-o a uma validação de format em vez
de colar algo do Stack Overflow:
app/models/user.rb
class User < ApplicationRecord
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
end
Essa constante é a mesma em que o guia de Ruby
se apoia. Ela já vem ancorada com \A e \z, então testa a string inteira, e é
deliberadamente permissiva: segue a definição do WHATWG/HTML5, que é mais frouxa
que a RFC 5322 e aceita tranquilamente jane@localhost porque não exige um ponto
no domínio. Isso é uma característica intencional. O trabalho da camada um é pegar
erros de digitação na entrada, não julgar as RFCs — o que
não ajudaria de qualquer forma.
O que importa é saber onde o Rails para. Todo recurso nativo de e-mail — a regra
format, os scaffolds, o helper email_field — valida o formato. Nenhum deles
resolve DNS ou conversa com um servidor de e-mail, então noreply-9f2x@gmail.com,
typo@gmial.com e sales@company-that-folded.com todos passam. Os três são
inentregáveis. O Rails não traz nenhuma checagem de entregabilidade.
Camada 2: o domínio consegue receber e-mail?#
Esta é a primeira camada que o Rails não entrega pronta, e é barata de acoplar. Um
domínio sem rota de e-mail não consegue aceitar e-mail para ninguém, então uma
única consulta de MX elimina domínios mortos, nomes de empresa escritos errado e
TLDs inventados. Um validador do Rails é apenas uma classe que herda de
ActiveModel::EachValidator e implementa validate_each, então envolva a consulta
de MX da biblioteca resolv em um deles:
app/validators/deliverable_domain_validator.rb
require "resolv"
class DeliverableDomainValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? || record.errors[attribute].present?
domain = value.rpartition("@").last
return if domain.present? && mail_route?(domain)
record.errors.add(attribute, options[:message] || "can't receive email")
end
private
def mail_route?(domain)
records = Resolv::DNS.open do |dns|
dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
end
!records.empty?
rescue Resolv::ResolvError, Resolv::ResolvTimeout
false
end
end
Resolv::DNS.open te entrega um resolvedor e o fecha quando o bloco termina, e
getresources retorna um array de registros MX — vazio quando o domínio não
publica nenhum ou não existe, que é exatamente o caso "sem rota". O rescue cobre
o outro modo de falha; note que Resolv::ResolvTimeout não é subclasse de
Resolv::ResolvError, então nomeie os dois se quiser que um servidor de nomes
lento seja tratado de forma limpa. Coloque-o no model pela chave inferida — o Rails
cameliza deliverable_domain para DeliverableDomainValidator e encontra a classe
que o Zeitwerk fez o autoload a partir de app/validators:
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP },
deliverable_domain: true
A guarda return if ... record.errors[attribute].present? é todo o truque. O
Active Model roda todo validador que você declara e agrega os erros — não existe
bail. Mas as validações rodam na ordem em que você as declara, então quando esta
aqui dispara, uma falha de format já está registrada em record.errors[:email],
e a guarda pula a consulta de DNS por completo. Um endereço malformado nunca gasta
um round-trip até o servidor de nomes.
Camada 3: a caixa postal realmente existe?#
As camadas um e dois só conseguem descartar um endereço para fora. Um domínio
pode publicar registros MX perfeitos e ainda assim não ter nenhuma 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 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. Há mais coisa envolvida do que isso —
como funciona a verificação de e-mail
percorre o pipeline completo, incluindo domínios catch-all que aceitam todo
endereço e derrotam uma sondagem ingênua.
O net/smtp do Ruby abrirá essa conversa sem problema. O problema não é o código;
é a rede. Rode uma sondagem SMTP a partir do seu servidor de aplicação e três
coisas dão errado: a maioria dos provedores de nuvem bloqueia a porta 25 de saída
por completo, 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 no seu notebook falha
silenciosamente, ou te coloca em uma blocklist, em produção. Esta é a camada que
vale a pena delegar.
Chamando a Qualisend a partir de um validador personalizado#
A delegação continua limpa porque uma API de verificação é só mais um validador. O
endpoint de verificação da Qualisend roda o pipeline inteiro — sintaxe, DNS e a
sondagem de caixa postal via SMTP — a partir de uma infraestrutura com reputação
gerenciada, e retorna um único veredito. É um POST JSON simples, então a net/http
da biblioteca padrão dá conta dele sem gems. Leia a chave a partir do ambiente em
vez de deixá-la fixa no código:
app/validators/deliverable_email_validator.rb
require "net/http"
require "json"
require "uri"
class DeliverableEmailValidator < ActiveModel::EachValidator
VERIFY_URL = URI("https://api.qualisend.com/v1/verify")
def validate_each(record, attribute, value)
return if value.blank? || record.errors[attribute].present?
result = verify(value)
return if result.nil? # our outage shouldn't block a real signup
if result["status"] == "undeliverable"
record.errors.add(attribute, options[:message] || "appears to be undeliverable")
end
end
private
def verify(email)
request = Net::HTTP::Post.new(VERIFY_URL)
request["Authorization"] = "Bearer #{ENV.fetch('QUALISEND_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = JSON.generate(email: email)
response = Net::HTTP.start(
VERIFY_URL.hostname, VERIFY_URL.port,
use_ssl: true, open_timeout: 5, read_timeout: 10
) { |http| http.request(request) }
return nil unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)["result"]
rescue StandardError
nil # unreachable or slow — treat as unknown and re-verify later
end
end
Esse endpoint é um placeholder — consulte a referência da API para
saber a URL base exata, o formato da requisição e como emitir uma chave com escopo.
A resposta envolve um objeto result:
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "catch_all": false }
}
}
status é um de deliverable, risky, undeliverable ou unknown. O validador
acima só reprova em definitivo undeliverable e deixa risky e unknown passarem,
então você pode decidir mais adiante o que fazer com eles — condicionar ao score
ou ramificar em uma entrada de sub_flags como disposable — em vez de transformar
um endereço limítrofe em um erro de formulário. E quando a própria requisição falha,
verify retorna nil e o validador não adiciona nenhum erro: as camadas de
sintaxe e de MX já rodaram localmente, então suavizar só a camada de rede evita que
uma indisponibilidade transitória bloqueie um cadastro de verdade. Trate o JSON
acima como o formato, não como o contrato; a lista completa de campos está na
documentação para desenvolvedores.
Juntando as camadas para validar um endereço de e-mail no Rails#
Declare as três validações no atributo, da mais barata primeiro. A ordem de declaração é a ordem de execução, e cada guarda pula quando uma camada mais barata já falhou, então a regra composta se lê de cima para baixo exatamente da forma como executa:
app/models/user.rb
class User < ApplicationRecord
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP },
deliverable_domain: true,
deliverable_email: true
end
Agora user.save só chega à API em endereços que já passaram pela sintaxe e pela
checagem de MX — um erro de digitação falha em format e nunca gasta um round-trip
ao servidor de nomes nem um crédito de API, e um domínio morto falha em
deliverable_domain antes da chamada de rede. user.valid? roda a cadeia inteira,
e user.errors[:email] carrega qualquer camada que tenha objetado. É essa
ordenação, não o framework, que torna a validação confiável — o mesmo formato da
versão em Ruby, expresso como validações
declarativas em vez de um método encadeado à mão.
Como essas são validações comuns, toda opção padrão funciona. Escope as duas
camadas de rede com if: :email_changed? para que um user.update(name: ...) sem
relação não rode de novo uma consulta de DNS nem gaste de novo um crédito em um
endereço que você já verificou. Mantenha no caminho síncrono as camadas locais
rápidas mais o veredito imediato da API no cadastro; qualquer coisa mais pesada
pertence a um background job. O
guia de cadastro serverless mostra o
padrão em tempo real, e como limpar uma lista de e-mails
cobre o lado do processamento em lote.
Perguntas frequentes#
O Rails tem alguma forma nativa de checar se um e-mail é entregável?#
Não. Todo recurso nativo de e-mail — a validação de format com
URI::MailTo::EMAIL_REGEXP, os scaffolds, o helper email_field — valida apenas o
formato. Nenhum deles resolve DNS ou conversa com um servidor de e-mail, então um
domínio inventado ou uma caixa postal inexistente passa. Adicione uma consulta de
MX e uma checagem de caixa postal via SMTP como validadores personalizados para
cobrir a entregabilidade, já que o Rails não traz nenhuma das duas.
Como escrevo um validador de e-mail personalizado no Rails?#
Herde de ActiveModel::EachValidator e implemente
validate_each(record, attribute, value), chamando record.errors.add(attribute, message) quando o valor falhar. Salve a classe em app/validators para que o
Zeitwerk faça o autoload dela e então acople-a pela chave inferida — um
DeliverableDomainValidator se conecta como deliverable_domain: true. Ele aceita
as mesmas opções de qualquer validação, então if:, on: e um message:
personalizado funcionam sem mudança.
Dá para verificar se uma caixa postal existe no Rails sem uma API?#
Em parte. A biblioteca padrão resolv confirma que o domínio aceita e-mail, o que
descarta domínios mortos de graça de dentro de um validador personalizado.
Confirmar a caixa postal significa uma conversa SMTP que você pode abrir com
net/smtp, mas não deveria rodar a partir do seu servidor de aplicação — a porta
25 de saída é amplamente bloqueada e o resultado depende da reputação do seu IP de
envio. Essa camada final é o que um serviço de verificação roda a partir de uma
infraestrutura com reputação gerenciada, construída para isso.
Devo validar e-mails no cadastro ou ao limpar uma lista?#
Ambos, em profundidades diferentes. Rode as camadas de sintaxe e de MX de forma
síncrona no cadastro — elas são rápidas o suficiente para bloquear a requisição e
dar feedback instantâneo — e aja também sobre o veredito imediato da API ali mesmo.
Reserve a verificação mais profunda e em lote para
limpar uma lista existente, onde a latência não
importa e a minúcia importa. Escopar os validadores de rede com
if: :email_changed? evita que atualizações comuns reverifiquem um endereço que
você já checou.
Pronto para adicionar a camada de caixa postal? A referência da API
tem o endpoint /verify, chaves com escopo e trechos de copiar e colar, ou cole um
endereço no verificador de e-mail gratuito e veja uma string
sintaticamente perfeita voltar como inentregável.