23 años desde Estocolmo
Notas del operador · Desde Estocolmo

Fundamentos de SMTP para developers — del comando EHLO al sistema que entrega de verdad

Por Wilfred Teague · Publicado el 11 de mayo de 2011 · Lectura aproximada 6 min

La mayoría de developers tropieza con SMTP la primera vez no porque el protocolo sea complicado — no lo es — sino porque las librerías esconden los detalles. Cuando algo falla en producción, ese vacío se vuelve caro: errores opacos, mensajes que se quedan en cola sin razón clara, retries que duplican entregas. Esta es una introducción al protocolo desde la perspectiva del developer: lo que realmente pasa entre su aplicación y el servidor SMTP, qué hábitos separan un script que envía emails de un sistema que entrega.

SMTP en una conversación de cinco actos

SMTP (Simple Mail Transfer Protocol, RFC 5321) es un protocolo en texto plano sobre TCP. La aplicación abre conexión a un servidor SMTP en puerto 587 (recomendado) o 465 (TLS implícito) y mantiene una conversación que sigue siempre la misma estructura. Cualquier librería SMTP de su lenguaje preferido — PHPMailer, Symfony Mailer, Nodemailer, ActionMailer, javax.mail, smtplib — ejecuta esta misma secuencia internamente.

El acto primero es EHLO. El cliente saluda y declara su identidad: EHLO mi-servidor.example.com. El servidor responde con un listado de extensiones soportadas (STARTTLS, AUTH, SIZE, PIPELINING, etc.). Este es el momento donde se descubren las capacidades del peer; las librerías serias parsean esta respuesta para saber qué pueden negociar después.

Acto segundo, STARTTLS. Si el servidor anuncia STARTTLS y el cliente lo soporta, ambos elevan la conexión a TLS. Esto es crítico: sin STARTTLS la conversación SMTP completa va en claro por la red, incluyendo credenciales y contenido del mensaje. En 2026 ningún sistema serio debería enviar SMTP sin TLS.

Acto tercero, AUTH. Tras TLS, el cliente autentica con un mecanismo soportado (LOGIN, PLAIN, CRAM-MD5, XOAUTH2). El servidor valida y queda en estado autenticado para el resto de la sesión. Sin autenticación, los relays serios rechazan envío directo.

Acto cuarto, el sobre del mensaje: MAIL FROM:<noreply@example.com> declara el remitente del sobre (Return-Path), RCPT TO:<destinatario@gmail.com> declara cada destinatario uno por uno. Importante: el sobre SMTP es independiente de las cabeceras From/To que verá el receptor — es perfectamente válido tener un MAIL FROM distinto al From visible (esto se llama "envelope vs header sender" y tiene implicaciones para SPF y bounce handling).

Acto quinto, DATA. El cliente envía DATA, el servidor responde con código 354 invitando a transmitir el mensaje. El cliente envía las cabeceras (From, To, Subject, Date, Message-ID, MIME-Version, Content-Type, etc.) seguidas de una línea en blanco y el cuerpo. Termina con una línea que contiene solo un punto (".") en línea propia. El servidor responde con código 250 si acepta el mensaje, 4xx si hay error transitorio (reintentar), 5xx si hay error permanente (no reintentar).

Códigos de respuesta SMTP — el lenguaje real del protocolo

Los códigos SMTP son tres dígitos con significado estructurado. El primer dígito indica clase: 2xx success, 3xx más información esperada, 4xx error transitorio, 5xx error permanente. Esta distinción 4xx/5xx es el dato operativo: 4xx se debe reintentar (típicamente con backoff exponencial); 5xx no se debe reintentar (mensaje perdido o destino inválido).

Algunos códigos que un developer de aplicación encontrará repetidamente:

  • 250 OK — el servidor aceptó el comando o el mensaje. Todo bien.
  • 421 Service not available — error transitorio del servidor (sobrecarga, mantenimiento). Reintentar más tarde.
  • 450 Mailbox unavailable — buzón temporalmente inaccesible (greylist, full quota). Reintentar.
  • 451 Local error — error transitorio del propio servidor SMTP. Reintentar.
  • 452 Insufficient system storage — espacio temporal agotado. Reintentar.
  • 521 Server does not accept mail — algunos relays rechazan ciertos remitentes. Permanente.
  • 550 Mailbox unavailable — buzón no existe (hard bounce típico). Permanente.
  • 551 User not local — relay rechaza forwarding. Permanente.
  • 554 Transaction failed — rechazo final por contenido, política o blacklist. Permanente.

El error que un script básico mal escrito interpreta peor es 421 o 451: la aplicación cree que falló el envío y devuelve error al usuario, cuando lo correcto era encolar y reintentar. Por eso las librerías serias y los relays especializados gestionan retries con backoff y persistencia.

El sistema vs el script — qué cambia entre desarrollo y producción

El código mínimo para enviar un email cabe en cinco líneas en cualquier lenguaje moderno. Eso funciona en desarrollo. En producción la diferencia entre "envía emails" y "entrega emails" se mide en una serie de hábitos:

Cola persistente con reintentos. Nunca enviar SMTP sincrónicamente desde el handler HTTP de la aplicación. El usuario no debería esperar a que un servidor remoto acepte el mensaje. Encolar el envío en Redis, RabbitMQ, PostgreSQL queue, Sidekiq o lo que use el stack, y procesarlo asincrónicamente con worker dedicado. Si falla con código 4xx, reintentar con backoff exponencial (1 minuto, 5 minutos, 15 minutos, 1 hora, 4 horas, 24 horas — abandonar después de 72h típicamente).

Idempotency keys. Si el worker se reinicia entre el envío SMTP exitoso y el ack al queue, el mismo mensaje puede entrar dos veces. Generar un Message-ID único por intent (no por intento) y persistirlo permite detectar duplicados antes de re-enviar.

Bounce processing. Los hard bounces (5xx) deben aplicarse a una lista de supresión inmediatamente. Un destinatario que rebotó hard nunca debe recibir más mensajes — no solo es ineficiente, daña la reputación del sender frente al ISP. Los soft bounces (4xx) se monitorizan: si un destinatario rebota 4xx repetidamente durante días, suele acabar como hard bounce de facto.

Logging granular. Cada intento de envío debe loguearse con timestamp, destinatario, código de respuesta y mensaje del servidor. Sin esto, depurar problemas de deliverability es imposible. Las herramientas profesionales (relays SMTP serios, ESPs) entregan este logging por defecto en su dashboard; el código self-hosted debe construirlo.

Separación de flujos. Los emails transaccionales críticos (reset de contraseña, confirmación de pago) no deben compartir cola ni reputación con notificaciones de baja prioridad. Si la cola de notifications product se atasca por algún problema, los password resets siguen funcionando porque van por canal separado.

SMTP no es solo el protocolo — es relación con receptores

Esta es la lección menos visible para developers: aceptar un mensaje vía SMTP no garantiza que el mensaje llegue a la bandeja del destinatario. El servidor SMTP del relay puede aceptar el mensaje (250 OK), encolarlo, intentar entregarlo al MTA del destinatario, y ese MTA puede aceptarlo y aún así enviarlo a la carpeta de spam o silenciarlo en la "promotions" tab.

La diferencia entre 250 OK y "llegar a inbox" es el dominio de la deliverability: reputación de IP, autenticación SPF/DKIM/DMARC, calidad de listas, contenido del mensaje, ratio de complaints, ratio de engagement. SMTP es solo la entrada del sistema. Lo que separa un sistema que envía de uno que entrega es todo lo que pasa después.

Por eso el siguiente paso natural tras dominar SMTP es entender autenticación de email (SPF, DKIM, DMARC) y reputación del remitente. Hay otros posts en este blog que cubren cada uno de esos temas en profundidad — los enlaces están al pie del post.

Resumen práctico para empezar

Si está integrando envío de email desde código por primera vez, empiece simple y construya hábitos correctos desde el inicio:

  • Use librería SMTP estándar de su lenguaje, no construya el protocolo a mano
  • Use puerto 587 con STARTTLS (o 465 con TLS implícito si la librería lo prefiere)
  • Encole los envíos asincrónicamente desde día uno, nunca síncrono al handler HTTP
  • Implemente backoff exponencial para 4xx y supresión inmediata para 5xx
  • Genere Message-ID único por mensaje y persista para idempotency
  • Logue cada intento con timestamp, destinatario, código respuesta
  • Configure SPF, DKIM, DMARC desde el dominio que envía (otro post en este blog)
  • Para volúmenes serios, use proveedor SMTP especializado en lugar de relay propio

Si llega a un punto donde el envío crece (más de 50.000 mensajes mensuales) o donde la deliverability empieza a ser problema operativo, vale la pena evaluar relay SMTP especializado o API REST de proveedor con IPs dedicadas. Para entonces, este post ya cumplió su trabajo: dejó claro qué pasa por debajo cuando su código llama a send().

¿Necesita ayuda con infraestructura email seria?

Authorize Hosting opera infraestructura email para equipos que se toman en serio sus envíos desde 2003. Relay SMTP, Email API, PowerMTA gestionado, servidores dedicados y deliverability gestionada con soporte directo en español.