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

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

9 minutes read

Qualisend team
Uma janela de editor de código com o título user.rb, uma marca vermelha da trilha Rails e uma pílula verde de resultado entregável, acima da legenda validates para MX para caixa postal.

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.

Your reputation, protected.

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

Get started