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.