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

Cómo validar una dirección de correo en Ruby

8 minutes read

Qualisend team
Una ventana de editor de código titulada validate.rb con una marca de gema ruby roja y una etiqueta verde de resultado deliverable, sobre el texto de sintaxis a MX a buzón.

Validar una dirección de correo en Ruby son tres tareas escondidas tras el nombre de un solo método. La mayoría de los tutoriales te entregan una regex, la ven pasar y dan la dirección por válida, pero una coincidencia de patrón solo te dice que la cadena tiene la forma correcta, no que el correo vaya a llegar alguna vez. La validación de verdad es por capas: una comprobación de sintaxis barata, una consulta DNS de los servidores de correo del dominio y una sonda SMTP del buzón real. La biblioteca estándar de Ruby te da las dos primeras sin ninguna gema; la tercera es un problema de red que conviene delegar. Esta guía construye cada capa con código funcional y muestra exactamente dónde se detiene la biblioteca estándar.

La respuesta corta#

Usa URI::MailTo::EMAIL_REGEXP de la biblioteca uri para la sintaxis, Resolv::DNS de la biblioteca resolv para la consulta MX y una API de verificación para la comprobación del buzón por SMTP: lo más barato primero, cortando en cuanto una capa sea concluyente. No recurras a net/smtp para sondear buzones desde tu propia aplicación: el puerto 25 saliente está bloqueado en la mayoría de los alojamientos, y la respuesta depende de la reputación de tu IP de envío y de un comportamiento de greylisting que no quieres reimplementar. Cada capa descarta direcciones de forma más barata que la anterior; solo la última puede dar por buena una dirección. Ese último paso es el hueco que la verificación de correo existe para cerrar.

Capa 1: sintaxis#

La regex pertenece a este punto y a ningún otro, y en Ruby ni siquiera tienes que escribir una. La biblioteca uri, parte de la biblioteca estándar, trae un patrón bien probado en URI::MailTo::EMAIL_REGEXP, así que recurre a eso en lugar de pegar algo de 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

Hay dos cosas que conviene saber sobre esa constante. Ya está anclada con \A y \z, así que match? comprueba la cadena entera y no una subcadena: no necesitas envolverla ni anclarla tú. Y es deliberadamente permisiva: sigue la definición de correo válido de WHATWG/HTML5, más laxa que la RFC 5322 y que, por ejemplo, acepta tan tranquila jane@localhost porque no exige un punto en el dominio. Eso es una característica, no un fallo. La labor de la primera capa es cazar erratas de dedo en la entrada, no litigar sobre las RFC, algo que no ayudaría de todos modos. El límite de 320 caracteres es por precaución: nada más largo puede ser una dirección real, y sale más barato rechazar pronto que pasar una cadena gigante aguas abajo. Un resultado positivo aquí significa "vale la pena comprobarla", nunca "es válida".

Capa 2: ¿puede el dominio recibir correo?#

Aquí es donde la biblioteca estándar de Ruby se gana el sueldo. 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. La biblioteca resolv resuelve registros sin ninguna gema:

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

Resolv::DNS.open te entrega un resolver y lo cierra cuando el bloque termina, y getresources devuelve un array de registros MX: un array vacío cuando el dominio no publica ninguno o no existe, que es justo el caso "sin ruta" que quieres cazar. El rescue se ocupa del otro modo de fallo: un resolver que da error o agota el tiempo. Fíjate en que Resolv::ResolvTimeout no es una subclase de Resolv::ResolvError, así que nombra ambas si quieres que un servidor de nombres lento se descarte limpiamente en lugar de reventar la llamada.

Cuando quieras los servidores de correo en sí y no un sí/no —para registrarlos o inspeccionar prioridades—, cada registro lleva exchange y preference, así que ordena 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

Algunos dominios aceptan correo en un registro A con un MX implícito; si necesitas respetar ese caso límite, recurre a getresources(domain, Resolv::DNS::Resource::IN::A) cuando el conjunto MX venga vacío. Para la inmensa mayoría de las direcciones reales, una comprobación MX es el filtro adecuado.

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 ningún buzón en la dirección que manejas: noreply-9f2x@gmail.com tiene sintaxis válida sobre una ruta de correo viva, y sigue siendo un buzón que nunca se creó. Confirmar que existe un buzón concreto implica la conversación de entrega SMTP: conectarse al servidor de correo, emitir RCPT TO, leer la respuesta y desconectarse antes de enviar nada. Hay más de lo que parece: cómo funciona la verificación de correo recorre toda la tubería, incluidos los dominios catch-all que aceptan cualquier dirección y derrotan a una sonda ingenua.

La biblioteca estándar de Ruby abrirá esa conversación por ti tan tranquila con net/smtp. El problema no es el código; es la red. Lanza una sonda 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 te conectas, y los servidores receptores hacen greylisting y limitan la tasa de remitentes desconocidos, así que una sonda que funciona en tu portátil falla en silencio, o te mete en listas de bloqueo, en producción. Esta es la capa que conviene delegar.

Hacer la comprobación completa con una API#

El endpoint de verificación de Qualisend ejecuta toda la tubería —sintaxis, DNS y la sonda SMTP del buzón— desde infraestructura con reputación gestionada, construida para ello, y devuelve un único veredicto. Como es un simple POST JSON, el net/http de la biblioteca estándar lo cubre sin ninguna gema. Lee tu clave del entorno en lugar de dejarla escrita en el 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

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 }
  }
}

Métete en el sobre y actúa sobre status:

body   = verify("jane@example.com")
result = body[:result]

result[:status]    # "deliverable" | "risky" | "undeliverable" | "unknown"
result[:score]     # confianza, 0–100
result[:reason]    # motivo legible por máquina, o nil
result[:sub_flags] # { role:, disposable:, catch_all:, ... }

net/http mantiene esto sin dependencias, algo cómodo en una tarea en segundo plano o en un servicio pequeño. Si tu aplicación ya se apoya en Faraday o HTTParty, la misma petición ocupa un par de líneas menos; el sobre que lees de vuelta es idéntico.

Una forma completa de validar una dirección de correo en Ruby#

Encadena las tres capas de la más barata a la más cara y detente en cuanto una sea concluyente:

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

rpartition("@") divide por el último @, así que siempre te quedas con el dominio real incluso con partes locales entrecomilladas de forma rara. Las dos capas locales no cuestan nada y cazan casi toda la basura al instante; la API se ejecuta solo sobre direcciones que merecen el viaje de ida y vuelta por la red. Ese orden —no el lenguaje— es lo que hace fiable la validación, y es la misma forma que encontrarás en las versiones de esta guía para Node.js y Python.

Quienes usan Rails pueden meter todo esto en un modelo como validación personalizada, de modo que una dirección mala nunca llegue a la base de datos:

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

Reserva la ruta síncrona para las dos capas locales rápidas más el veredicto inmediato de la API; cualquier cosa más lenta pertenece a una tarea en segundo plano. Pero el núcleo es Ruby puro, y se comporta igual en una ruta de Sinatra, una tarea rake o un script de usar y tirar: el trabajo lo hace el enfoque por capas.

Preguntas frecuentes#

¿Puedo validar una dirección de correo en Ruby sin ninguna gema?#

En dos de las tres capas, sí. URI::MailTo::EMAIL_REGEXP de la biblioteca uri se encarga de la sintaxis, y Resolv::DNS de la biblioteca resolv resuelve los registros MX; ambas vienen con Ruby, así que una comprobación de sintaxis más dominio no necesita instalar nada. La tercera capa, confirmar el buzón en sí, implica una sonda SMTP que no deberías lanzar desde tu propio servidor. Esa es la parte de la que se ocupa por ti un servicio de verificación.

¿Es suficiente URI::MailTo::EMAIL_REGEXP por sí sola?#

Es la comprobación correcta para la primera capa y una apuesta mucho mejor que una regex casera, pero valida la forma, no la entregabilidad. Nunca resuelve DNS ni contacta con un servidor de correo, y es permisiva por diseño (acepta jane@localhost), así que un resultado positivo significa "parece un correo", no "se entregará". Combínala con una consulta MX y una comprobación del buzón antes de fiarte de la dirección.

¿Por qué no usar net/smtp para comprobar si un buzón existe?#

net/smtp puede abrir la conversación SMTP, pero ejecutarla desde tu servidor de aplicación es poco fiable y arriesgado: la mayoría de los alojamientos bloquean el puerto 25 saliente, la respuesta depende de la reputación de tu IP de envío y sondear con cualquier volumen te acaba limitando la tasa o metiendo en listas de bloqueo. Una API de verificación ejecuta la sonda desde infraestructura con reputación gestionada, construida exactamente para eso.

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

Ambas cosas, con distinta profundidad. Ejecuta la sintaxis y la consulta MX de forma síncrona en el registro —son lo bastante rápidas como para bloquear la petición y dar respuesta inmediata— y actúa también ahí sobre el veredicto inmediato de la API. Reserva el trabajo más profundo y lento para la depuración de listas y las tareas de back-office; la guía de registro serverless muestra el patrón real de principio a fin.


¿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 mira 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