Ambos modos de conexión llevan el mismo mensaje a la misma cuenta de relay, y la diferencia está en dónde empieza TLS, no en qué se envía. Uno se abre en texto claro y se actualiza dentro de la conexión; el otro está cifrado desde el primer byte. Esa única diferencia decide qué aceptará una política de relay estricta, qué puede observar un dispositivo intermedio y qué mensaje de error pasarás una tarde leyendo. Así elijo entre ellos cuando una aplicación en una red privada entrega un mensaje a un servicio de envío interno.

Dónde divergen realmente los dos modos

La tarea es idéntica en ambos casos: autenticarse como una cuenta aprobada, indicar un remitente permitido, enumerar los destinatarios y esperar a que el relay asuma la responsabilidad. Lo que cambia es el orden de las operaciones. El puerto 587 empieza como una sesión SMTP en texto claro, anuncia STARTTLS en su respuesta EHLO y solo se actualiza si el cliente lo pide. El puerto 465 espera un handshake TLS antes de que se pronuncie cualquier comando SMTP.

El envío y el relay son trabajos distintos, por eso los números de puerto no son hábitos intercambiables. RFC 6409 describe el envío de mensajes como un servicio propio, y RFC 8314 cubre la variante de TLS implícito que la mayoría de los relays modernos exponen en 465. Si tu relay documenta una de las dos, esa documentación prevalece sobre cualquier preferencia que hayas adquirido en un empleador anterior.

Qué te da STARTTLS en 587

La actualización ocurre dentro de la conversación SMTP, así que el relay puede anunciarla y el cliente puede negarse a continuar sin ella. En Python, starttls() lanza una excepción cuando el servidor no ofrece la extensión, lo que convierte un intento de degradación en un fallo visible en lugar de un inicio de sesión silencioso en texto claro. Ese comportamiento es el argumento principal del modo, y vale la pena depender de él a propósito y no por accidente.

La ventana de exposición es pequeña pero real: el intercambio EHLO inicial es legible, y nada antes de la actualización transporta credenciales. En una ruta privada entre una subred de aplicaciones y un relay interno, esa ventana suele ser aceptable. En una ruta que no controlas, es la razón por la que algunos equipos insisten en el otro modo.

Un dispositivo de interceptación que elimina la capacidad STARTTLS produce un envío fallido, no un retroceso. Ese es el resultado que quieres, y también es la forma más rápida de descubrir que algo está reescribiendo SMTP en la ruta.

Qué cambia el TLS implícito en 465

No hay fase de texto claro en absoluto. El cliente realiza un handshake TLS en cuanto se abre el socket, valida el certificado contra su almacén de confianza y solo entonces intercambia EHLO, AUTH y el mensaje. Nada puede degradar la sesión porque no hay un estado de texto claro desde el que negociar, lo que elimina toda una clase de fallos de interceptación.

El precio es la diagnosticabilidad. Un relay que responde en 465 con SMTP en texto claro, o un balanceador de carga que termina TLS con el nombre de certificado equivocado, falla en el handshake y te da un error SSL en lugar de un error de correo. Cuando eso ocurre, el problema está por debajo de la aplicación, y ninguna cantidad de construcción del mensaje lo arreglará.

Una función de envío, dos transportes

El código apenas difiere entre los modos, por lo que la elección debería hacerse desde la política del relay y no desde la comodidad de la biblioteca. Exporta primero la configuración y luego ejecuta un único script que seleccione el transporte según el puerto configurado.

export SMTP_HOST=smtp.relay.internal
export SMTP_PORT=587
export SMTP_USER=svc-notify
export [email protected]
export [email protected]
export SMTP_PASSWORD="$(cat /run/secrets/smtp_password)"
import os, smtplib, ssl
from email.message import EmailMessage

ctx = ssl.create_default_context()
msg = EmailMessage()
msg["From"] = os.environ["MAIL_FROM"]
msg["To"] = os.environ["MAIL_TEST_TO"]
msg["Subject"] = "relay submission test"
msg.set_content("One authorized test message from the app.")

host = os.environ["SMTP_HOST"]
port = int(os.environ.get("SMTP_PORT", "587"))

if port == 465:
    client = smtplib.SMTP_SSL(host, port, context=ctx, timeout=15)
else:
    client = smtplib.SMTP(host, port, timeout=15)
    client.ehlo()
    client.starttls(context=ctx)
    client.ehlo()

with client:
    client.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
    refused = client.send_message(msg)

print("refused:", refused)

create_default_context() verifica la cadena y el nombre de host, así que una CA privada debe instalarse en el almacén de confianza en lugar de omitirse con un contexto no verificado. send_message() devuelve un diccionario de los destinatarios que el relay rechazó; un diccionario vacío significa que los aceptó a todos. El tiempo de espera de quince segundos es un punto de partida, no un valor ajustado para tu red.

Mantén la contraseña fuera de la imagen y fuera de los registros. Un secreto montado en tiempo de ejecución y leído en el entorno está bien; hacer eco del entorno en una línea de registro no lo está, ni tampoco un rastro de depuración del intercambio AUTH.

Elige por política de relay y ruta, no por costumbre

Tres condiciones lo deciden. Primera, qué anuncia realmente el relay: un servicio que solo escucha en 587 con STARTTLS no es alcanzable en 465 por mucho que prefieras el TLS implícito. Segunda, si la ruta entre la aplicación y el relay es lo bastante de confianza como para aceptar un intercambio EHLO legible. Tercera, si algo intermedio reescribe o hace proxy de SMTP, porque eso decide si el riesgo de degradación es teórico o real.

Cuando el relay ofrece ambos, tomo el 465 para un servicio que se ejecuta sin supervisión y el 587 para uno en el que una persona observa el primer despliegue, solo porque los mensajes de fallo apuntan a capas distintas. Esa preferencia es mía y no es una política.

Elijas lo que elijas, registra el puerto, el material de confianza y el dominio del remitente junto con el despliegue. Luego envía un mensaje de prueba autorizado y confirma que el relay lo aceptó, guardando el resultado del envío y cualquier informe de entrega posterior como registros separados. Una entrega correcta al relay es el final de la responsabilidad de la aplicación, no la prueba de que el mensaje llegó a una bandeja de entrada.

Artículos relacionados