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. Ele responde na hora com um veredito das checagens
rápidas, mais um id de job para o veredito final da caixa postal enquanto a
sondagem ainda está rodando. 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://app.qualisend.com/api/v1/verify")
# A timeout, 402 (out of credits), 429 (rate limited) or 5xx: no verdict right now.
class VerifyUnavailable < StandardError; end
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) do |http|
http.request(request)
end
if %w[402 429].include?(response.code) || response.is_a?(Net::HTTPServerError)
raise VerifyUnavailable, "Qualisend responded #{response.code}"
end
raise "Qualisend responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body, symbolize_names: true)
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise VerifyUnavailable, e.message
end
A referência da API explica o formato da requisição e
como gerar uma chave com escopo. A resposta traz um job_id e envolve um objeto
result:
{
"job_id": "8d1f0c2e-5b7a-4c3e-9f61-2a4b6c8d0e13",
"result": {
"status": "unknown",
"score": 61,
"smtp": "pending",
"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:, ... }
result[:smtp] # "pending" while the mailbox probe runs, else "cached" | "skipped"
body[:job_id] # where the final verdict lands when smtp is "pending"
Quando result[:smtp] é "cached" ou "skipped", o result já é definitivo.
Quando é "pending", a sondagem da caixa postal ainda está rodando: o veredito
imediato vem das checagens rápidas, e o definitivo chega em
GET https://app.qualisend.com/api/v1/jobs/{job_id}. Não chame /verify de novo
para atualizá-lo. Guarde o job_id e deixe uma tarefa em segundo plano consultar
esse endpoint a cada poucos segundos até o job terminar.
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
rescue VerifyUnavailable
{ status: "unknown", reason: "unknown" } # fail open: let it through, re-verify later
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
Uma checagem no cadastro deve falhar aberta (fail open): o validate_email
transforma um timeout, um 402, um 429 ou um 5xx em unknown, então uma
queda da API, um limite de taxa ou um saldo de créditos zerado nunca bloqueia um
cliente de verdade. Verifique esses endereços de novo mais tarde.
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
01Consigo 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ê.
02O 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.
03Por 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.
04Devo 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.



