Validar um endereço de e-mail em Ruby são três tarefas escondidas atrás de um único nome de método. A maioria dos tutoriais te entrega um regex, vê ele passar e declara o endereço válido — mas uma correspondência de padrão só te diz que a string está com o formato certo, não que o e-mail um dia vai chegar até ela. A validação de verdade é feita em camadas: uma verificação barata de sintaxe, uma consulta DNS aos servidores de e-mail do domínio e uma sondagem SMTP da própria caixa postal. A biblioteca padrão do Ruby te dá as duas primeiras sem nenhuma gem; a terceira é um problema de rede que vale a pena repassar. Este guia monta cada camada com código funcional e mostra exatamente onde a biblioteca padrão para.
A resposta curta#
Use o URI::MailTo::EMAIL_REGEXP da biblioteca uri para a sintaxe, o
Resolv::DNS da biblioteca resolv para a consulta de MX e uma API de
verificação para a checagem SMTP da caixa postal — do mais barato ao mais caro,
interrompendo assim que uma camada for decisiva. Não recorra ao net/smtp para
sondar caixas postais a partir do seu próprio app: a porta 25 de saída fica
bloqueada na maioria dos provedores, e a resposta depende da reputação do seu IP
de envio e de um comportamento de greylisting que você não vai querer
reimplementar. Cada camada descarta endereços de forma mais barata que a
anterior; só a última consegue aprovar um endereço. Esse último passo é a
lacuna que a
verificação de e-mail existe para fechar.
Camada 1: sintaxe#
O regex pertence a este lugar e a nenhum outro — e em Ruby você nem precisa
escrever um. A biblioteca uri, parte da biblioteca padrão, traz um padrão bem
testado em URI::MailTo::EMAIL_REGEXP, então recorra a ele em vez de colar algo
do Stack Overflow:
require "uri"
def valid_syntax?(email)
email.is_a?(String) &&
email.length <= 320 &&
email.match?(URI::MailTo::EMAIL_REGEXP)
end
valid_syntax?("jane@example.com") # => true
valid_syntax?("not-an-email") # => false
valid_syntax?("a@@b.com") # => false
Duas coisas valem a pena saber sobre essa constante. Ela já vem ancorada com
\A e \z, então match? testa a string inteira em vez de um trecho — você não
precisa envolvê-la nem ancorá-la por conta própria. E ela é deliberadamente
permissiva: segue a definição de e-mail válido da WHATWG/HTML5, que é mais
flexível que a RFC 5322 e, por exemplo, aceita alegremente jane@localhost
porque não exige um ponto no domínio. Isso é um recurso, não um defeito. A função
da primeira camada é pegar erros de digitação na entrada, não julgar as RFCs — o
que nem ajudaria de qualquer forma. A
proteção de 320 caracteres é uma dupla garantia: nada mais longo pode ser um
endereço real, e é mais barato rejeitar cedo do que repassar uma string gigante
adiante. Uma aprovação aqui significa "vale a pena checar", nunca "válido".
Camada 2: o domínio consegue receber e-mail?#
É aqui que a biblioteca padrão do Ruby mostra seu valor. Um domínio sem rota de
e-mail não consegue aceitar mensagens para ninguém, então uma única consulta de
MX elimina domínios mortos, nomes de empresas escritos errado e TLDs inventados.
A biblioteca resolv resolve os registros sem nenhuma gem:
require "resolv"
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
mail_route?("gmail.com") # => true
mail_route?("company-that-folded.com") # => false
O Resolv::DNS.open te entrega um resolvedor e o fecha quando o bloco termina, e
o getresources retorna um array de registros MX — um array vazio quando o
domínio não publica nenhum ou não existe, que é exatamente o caso de "sem rota"
que você quer pegar. O rescue trata o outro modo de falha: um resolvedor que dá
erro ou estoura o tempo limite. Note que Resolv::ResolvTimeout não é uma
subclasse de Resolv::ResolvError, então nomeie os dois se você quiser que um
servidor de nomes lento passe de forma limpa em vez de derrubar a chamada.
Quando você quiser os servidores de e-mail em si em vez de um sim/não — para
registrá-los ou inspecionar as prioridades — cada registro carrega exchange e
preference, então ordene por este último:
def mail_hosts(domain)
records = Resolv::DNS.open do |dns|
dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
end
records.sort_by(&:preference).map { |mx| mx.exchange.to_s }
# => ["gmail-smtp-in.l.google.com", "alt1.gmail-smtp-in.l.google.com", ...]
rescue Resolv::ResolvError, Resolv::ResolvTimeout
[]
end
Alguns domínios aceitam e-mail em um registro A com um MX implícito; se você
precisar honrar esse caso extremo, recorra a getresources(domain, Resolv::DNS::Resource::IN::A) quando o conjunto de MX voltar vazio. Para a
esmagadora maioria dos endereços reais, uma checagem de MX é o filtro certo.
Camada 3: a caixa postal existe de fato?#
As camadas um e dois só conseguem descartar um endereço. Um domínio pode
publicar registros MX perfeitos e ainda 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 mesmo assim é uma caixa que nunca foi criada. Confirmar que uma
caixa postal específica existe significa a conversa de entrega SMTP: conectar
ao servidor de e-mail, emitir RCPT TO, ler a resposta e desconectar antes de
enviar qualquer coisa. Há mais coisa nisso do que parece —
como funciona a verificação de e-mail
percorre o pipeline completo, incluindo os domínios catch-all, que aceitam todo
endereço e derrotam uma sondagem ingênua.
A biblioteca padrão do Ruby vai abrir alegremente essa conversa para você com o
net/smtp. 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 inteiramente 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 no seu notebook falha silenciosamente, ou te coloca em blocklist, em
produção. Esta é a camada que vale a pena delegar.
Fazendo a checagem completa com uma API#
O endpoint de verificação da Qualisend roda o pipeline inteiro — sintaxe, DNS e a
sondagem SMTP da caixa postal — a partir de uma infraestrutura com reputação
gerenciada, criada para isso, e retorna um único veredito. Como é um simples POST
em JSON, o net/http da biblioteca padrão dá conta sem nenhuma gem. Leia sua
chave a partir do ambiente em vez de deixá-la fixa no código:
require "net/http"
require "json"
require "uri"
VERIFY_URL = URI("https://api.qualisend.com/v1/verify")
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) do |http|
http.request(request)
end
raise "Qualisend responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body, symbolize_names: true)
end
Esse endpoint é um espaço reservado — confira a
referência da API para a URL base exata, o formato da requisição e
como gerar 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 }
}
}
Alcance dentro do envelope e aja sobre o status:
body = verify("jane@example.com")
result = body[:result]
result[:status] # "deliverable" | "risky" | "undeliverable" | "unknown"
result[:score] # confidence, 0–100
result[:reason] # machine-readable reason, or nil
result[:sub_flags] # { role:, disposable:, catch_all:, ... }
O net/http mantém isso livre de dependências, o que é conveniente em uma tarefa
em segundo plano ou em um serviço pequeno. Se o seu app já se apoia no Faraday ou
no HTTParty, a mesma requisição fica algumas linhas mais curta — o envelope que
você lê de volta é idêntico.
Uma forma completa de validar um endereço de e-mail em Ruby#
Encadeie as três camadas do mais barato para o mais caro e pare no momento em que uma delas for decisiva:
def validate_email(email)
return { status: "undeliverable", reason: "invalid_email" } unless valid_syntax?(email)
domain = email.rpartition("@").last
return { status: "undeliverable", reason: "invalid_domain" } unless mail_route?(domain)
verify(email)[:result] # deliverable | risky | undeliverable | unknown
end
O rpartition("@") divide no último @, então você sempre pega o domínio real
mesmo com partes locais entre aspas de forma estranha. As duas camadas locais não
custam nada e pegam a maior parte do lixo na hora; a API roda só nos endereços que
valem a ida e volta pela rede. Essa ordenação — e não a linguagem — é o que torna
a validação confiável, e é o mesmo formato que você vai encontrar nas versões em
Node.js e
Python deste guia.
Quem usa Rails pode encaixar tudo isso em um model como uma validação personalizada, para que um endereço ruim nunca chegue ao banco de dados:
class User < ApplicationRecord
validate :email_must_be_deliverable
private
def email_must_be_deliverable
return if email.blank?
verdict = validate_email(email)
errors.add(:email, "doesn't look deliverable") if verdict[:status] == "undeliverable"
end
end
Mantenha no caminho síncrono apenas as duas camadas locais rápidas mais o veredito imediato da API; qualquer coisa mais lenta pertence a uma tarefa em segundo plano. Mas o núcleo é Ruby puro, e ele se comporta da mesma forma em uma rota do Sinatra, em uma tarefa rake ou em um script avulso — a estratégia em camadas é o que faz o trabalho.
Perguntas frequentes#
Consigo validar um endereço de e-mail em Ruby sem nenhuma gem?#
Em duas das três camadas, sim. O URI::MailTo::EMAIL_REGEXP da biblioteca uri
cuida da sintaxe, e o Resolv::DNS da biblioteca resolv resolve os registros
MX — ambos já vêm com o Ruby, então uma verificação de sintaxe mais domínio não
exige nada instalado. A terceira camada, confirmar a própria caixa postal, exige
uma sondagem SMTP que você não deveria executar a partir do seu próprio servidor.
É justamente essa parte que um serviço de verificação resolve para você.
O URI::MailTo::EMAIL_REGEXP é bom o bastante sozinho?#
É a verificação certa para a primeira camada e uma aposta muito melhor do que um
regex feito à mão — mas ele valida o formato, não a entregabilidade. Ele nunca
resolve DNS nem contata um servidor de e-mail, e é permissivo por design (aceita
jane@localhost), então uma aprovação significa "parece um e-mail", não "vai ser
entregue". Combine-o com uma consulta de MX e uma verificação da caixa postal
antes de confiar no endereço.
Por que não usar net/smtp para checar se uma caixa postal existe?#
O net/smtp consegue abrir a conversa SMTP, mas executá-lo a partir do seu
servidor de aplicação é pouco confiável e arriscado: a maioria dos provedores
bloqueia a porta 25 de saída, a resposta depende da reputação do seu IP de envio,
e sondar em qualquer volume faz você levar limite de taxa ou entrar em blocklist.
Uma API de verificação executa a sondagem a partir de uma infraestrutura com
reputação gerenciada, criada exatamente para isso.
Devo validar e-mails no cadastro ou ao limpar uma lista?#
Nos dois, com profundidades diferentes. Rode a sintaxe e a consulta de MX de forma síncrona no cadastro — elas são rápidas o suficiente para bloquear a requisição e dar retorno imediato — e aja também ali sobre o veredito imediato da API. Reserve o trabalho mais profundo e mais lento para a limpeza de listas e tarefas de bastidores; o guia de cadastro serverless mostra o padrão ao vivo de ponta a ponta.
Pronto para adicionar a camada da 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 não entregável.