Skip to content
Empieza con 100 créditos de verificación gratis
Qualisend
Todos los artículos
Ingeniería / 12 de mayo de 2026

Cómo validar una dirección de correo electrónico en Rails

9 minutes read

Qualisend team
Una ventana de editor de código titulada user.rb con una marca roja de la vía de Rails y una píldora verde de resultado entregable, sobre el pie de foto validates a MX a buzón.

En el momento en que necesitas validar una dirección de correo electrónico en Rails, Active Record lo hace parecer trivial: escribes validates :email, ves pasar el test y sigues adelante. Pero esa única línea responde solo a la primera de las tres preguntas que plantea una validación de verdad. ¿Tiene la dirección la forma correcta? ¿Puede su dominio recibir correo? ¿Existe realmente el buzón? Rails ofrece una respuesta pulcra a la primera y nada en absoluto para las otras dos. Esta guía construye cada capa como una validación idiomática de Rails — una regla format y luego dos clases personalizadas de ActiveModel::EachValidator — y muestra exactamente dónde se detiene el framework. Amplía la guía de validación de correo en Ruby: Rails envuelve los mismos bloques de la biblioteca estándar en la API declarativa de validaciones.

La respuesta corta#

Usa validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } para la sintaxis, un validador personalizado respaldado por la biblioteca estándar resolv para la consulta MX y una API de verificación para la comprobación del buzón por SMTP: tres validaciones sobre un mismo atributo, la más barata primero, cada una saltándose cuando una capa más económica ya ha fallado. No abras una sesión SMTP desde tu aplicación para sondear buzones por tu cuenta: el puerto 25 saliente está bloqueado en la mayoría de los hosts, y la respuesta depende de la reputación de la IP de envío y del greylisting que no querrás reimplementar. Un detalle distingue a Rails de los frameworks con una palabra clave bail: Active Model ejecuta todos los validadores que declares y recopila todos los errores, así que el cortocircuito es una guarda que añades, no un flag que pasas. Cada capa descarta direcciones de forma más barata que la anterior; solo la última puede dar una dirección por válida.

Capa 1: validación de formato con una regla validates#

Rails se encarga de la capa uno, y no hace falta que escribas una expresión regular para conseguirlo. La biblioteca uri de Ruby — que Rails ya carga — incluye un patrón bien probado en URI::MailTo::EMAIL_REGEXP, así que pásaselo a una validación format en lugar de pegar algo de Stack Overflow:

app/models/user.rb

class User < ApplicationRecord
  validates :email,
            presence: true,
            format: { with: URI::MailTo::EMAIL_REGEXP }
end

Esa constante es la misma en la que se apoya la guía de Ruby. Ya está anclada con \A y \z, por lo que comprueba la cadena completa, y es deliberadamente permisiva: sigue la definición de WHATWG/HTML5, que es más laxa que la de RFC 5322 y acepta sin problemas jane@localhost porque no exige un punto en el dominio. Eso es una ventaja. El trabajo de la capa uno es detectar erratas de dedo torpe en la entrada, no litigar los RFC — lo cual no serviría de nada de todos modos.

Lo que importa es saber dónde se detiene Rails. Todos los recursos de correo integrados — la regla format, los scaffolds, el helper email_field — validan la forma. Ninguno resuelve DNS ni contacta con un servidor de correo, así que noreply-9f2x@gmail.com, typo@gmial.com y sales@company-that-folded.com pasan todos. Los tres son no entregables. Rails no incluye ninguna comprobación de entregabilidad.

Capa 2: ¿puede el dominio recibir correo?#

Esta es la primera capa que Rails no te da hecha, y es barata de acoplar. Un dominio sin ruta de correo no puede aceptar correo para nadie, así que una sola consulta MX elimina dominios muertos, nombres de empresa mal escritos y TLD inventados. Un validador de Rails no es más que una clase que hereda de ActiveModel::EachValidator e implementa validate_each, así que envuelve la consulta MX de la biblioteca resolv en uno:

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 un resolutor y lo cierra cuando el bloque termina, y getresources devuelve un array de registros MX — vacío cuando el dominio no publica ninguno o no existe, que es exactamente el caso de "sin ruta". El rescue cubre el otro modo de fallo; ten en cuenta que Resolv::ResolvTimeout no es una subclase de Resolv::ResolvError, así que nombra ambos si quieres que un servidor de nombres lento pase de largo limpiamente. Colócalo en el modelo mediante su clave inferida — Rails convierte en camel case deliverable_domain a DeliverableDomainValidator y encuentra la clase que Zeitwerk cargó automáticamente desde app/validators:

validates :email,
          presence: true,
          format: { with: URI::MailTo::EMAIL_REGEXP },
          deliverable_domain: true

La guarda return if ... record.errors[attribute].present? es todo el truco. Active Model ejecuta todos los validadores que declares y agrega los errores — no hay bail. Pero las validaciones se ejecutan en el orden en que las declaras, así que para cuando esta se dispara, un fallo de format ya está en record.errors[:email], y la guarda se salta la consulta DNS por completo. Una dirección malformada nunca gasta una ida y vuelta al servidor de nombres.

Capa 3: ¿existe realmente el buzón?#

Las capas uno y dos solo pueden descartar una dirección. Un dominio puede publicar registros MX perfectos y aun así no tener buzón en la dirección que manejas — noreply-9f2x@gmail.com es sintaxis válida en una ruta de correo activa, y sigue siendo un buzón que nunca se creó. Confirmar un buzón concreto significa la conversación de entrega SMTP: conectar con el host de correo, emitir RCPT TO, leer la respuesta y desconectar antes de enviar nada. Hay más que eso — cómo funciona la verificación de correo recorre el pipeline completo, incluidos los dominios catch-all que aceptan todas las direcciones y derrotan un sondeo ingenuo.

net/smtp de Ruby abrirá encantado esa conversación. El problema no es el código; es la red. Ejecuta un sondeo SMTP desde tu servidor de aplicación y tres cosas salen mal: la mayoría de los proveedores de nube bloquean el puerto 25 saliente por completo, la respuesta depende de la reputación de la IP desde la que conectas, y los servidores receptores aplican greylisting y limitan la tasa a remitentes desconocidos — de modo que un sondeo que funciona en tu portátil falla en silencio, o te mete en una lista de bloqueo, en producción. Esta es la capa que vale la pena delegar.

Llamar a Qualisend desde un validador personalizado#

La delegación se mantiene limpia porque una API de verificación no es más que otro validador. El endpoint de verificación de Qualisend ejecuta el pipeline completo — sintaxis, DNS y el sondeo del buzón por SMTP — desde infraestructura con reputación gestionada, y devuelve un único veredicto. Es un simple POST JSON, así que el net/http de la biblioteca estándar lo cubre sin gems. Lee la clave del entorno en lugar de dejarla codificada a mano:

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

Ese endpoint es un marcador de posición — consulta la referencia de la API para conocer la URL base exacta, la forma de la petición y cómo generar una clave con alcance limitado. La respuesta envuelve un objeto result:

{
  "result": {
    "status": "deliverable",
    "score": 95,
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "catch_all": false }
  }
}

status es uno de deliverable, risky, undeliverable o unknown. El validador de arriba solo falla en firme con undeliverable y deja pasar risky y unknown, de modo que puedes decidir aguas abajo qué hacer con ellos — filtrar por score, o ramificar según una entrada de sub_flags como disposable — en lugar de convertir una dirección dudosa en un error de formulario. Y cuando la propia petición falla, verify devuelve nil y el validador no añade ningún error: las capas de sintaxis y MX ya se ejecutaron localmente, así que ablandar únicamente la capa de red evita que una caída transitoria bloquee un registro real. Trata el JSON de arriba como la forma, no como el contrato; la lista completa de campos vive en la documentación para desarrolladores.

Juntar las capas para validar una dirección de correo en Rails#

Declara las tres validaciones sobre el atributo, la más barata primero. El orden de declaración es el orden de ejecución, y cada guarda se salta cuando una capa más económica ya ha fallado, así que la regla compuesta se lee de arriba abajo exactamente como se ejecuta:

app/models/user.rb

class User < ApplicationRecord
  validates :email,
            presence: true,
            format: { with: URI::MailTo::EMAIL_REGEXP },
            deliverable_domain: true,
            deliverable_email: true
end

Ahora user.save solo llega a la API con direcciones que ya han superado la sintaxis y la comprobación MX — una errata falla en format y nunca gasta una ida y vuelta al servidor de nombres ni un crédito de API, y un dominio muerto falla en deliverable_domain antes de la llamada de red. user.valid? ejecuta toda la cadena, y user.errors[:email] carga la capa que haya objetado. Ese orden, y no el framework, es lo que hace fiable la validación — la misma forma que la versión en Ruby, expresada como validaciones declarativas en lugar de un método encadenado a mano.

Como son validaciones ordinarias, funcionan todas las opciones estándar. Limita las dos capas de red con if: :email_changed? para que un user.update(name: ...) sin relación no vuelva a ejecutar una consulta DNS ni a gastar un crédito en una dirección que ya verificaste. Mantén la ruta síncrona en las capas locales rápidas más el veredicto inmediato de la API en el registro; cualquier cosa más pesada pertenece a un trabajo en segundo plano. La guía de registro serverless muestra el patrón en tiempo real, y cómo limpiar una lista de correo cubre el lado por lotes.

Preguntas frecuentes#

¿Tiene Rails una forma integrada de comprobar si un correo es entregable?#

No. Todos los recursos de correo integrados — la validación format con URI::MailTo::EMAIL_REGEXP, los scaffolds, el helper email_field — validan solo la forma. Ninguno resuelve DNS ni contacta con un servidor de correo, así que un dominio inventado o un buzón inexistente pasan. Añade una consulta MX y una comprobación del buzón por SMTP como validadores personalizados para cubrir la entregabilidad, ya que Rails no incluye ninguna de las dos.

¿Cómo escribo un validador de correo personalizado en Rails?#

Hereda de ActiveModel::EachValidator e implementa validate_each(record, attribute, value), llamando a record.errors.add(attribute, message) cuando el valor falle. Guarda la clase en app/validators para que Zeitwerk la cargue automáticamente y, después, conéctala mediante su clave inferida — un DeliverableDomainValidator se engancha como deliverable_domain: true. Acepta las mismas opciones que cualquier validación, así que if:, on: y un message: personalizado funcionan sin cambios.

¿Puedo verificar que existe un buzón en Rails sin una API?#

En parte. La biblioteca estándar resolv confirma que el dominio acepta correo, lo que descarta gratis los dominios muertos desde dentro de un validador personalizado. Confirmar el buzón implica una conversación SMTP que puedes abrir con net/smtp pero que no deberías ejecutar desde tu servidor de aplicación — el puerto 25 saliente está bloqueado en muchos sitios y el resultado depende de la reputación de tu IP de envío. Esa capa final es lo que ejecuta un servicio de verificación desde infraestructura con reputación gestionada creada para ello.

¿Debo validar los correos en el registro o al limpiar una lista?#

Ambas cosas, con distinta profundidad. Ejecuta las capas de sintaxis y MX de forma síncrona en el registro — son lo bastante rápidas para bloquear la petición y dar respuesta instantánea — y actúa allí también sobre el veredicto inmediato de la API. Reserva la verificación más profunda y por lotes para limpiar una lista existente, donde la latencia no importa y la exhaustividad sí. Limitar los validadores de red con if: :email_changed? evita que las actualizaciones normales vuelvan a verificar una dirección que ya has comprobado.


¿Listo para añadir la capa del buzón? La referencia de la API tiene el endpoint /verify, las claves con alcance limitado y fragmentos para copiar y pegar, o pega una dirección en el comprobador de correo gratuito y observa cómo una cadena sintácticamente perfecta vuelve como no entregable.

Your reputation, protected.

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

Get started