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

Como validar um endereço de e-mail em Ruby

9 min de leitura

Qualisend team
Como validar um endereço de e-mail em Ruby

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.

Sua reputação, protegida.

Limpe sua primeira lista em minutos. 100 créditos grátis, sem cartão.

Começar