Respuesta rápida: crea un EmailMessage, define remitente, destinatario y asunto, añade alternativas de texto y HTML, abre smtplib.SMTP, llama a starttls() con un contexto SSL predeterminado, autentica y usa send_message(). Host, puerto, usuario y contraseña deben proceder del entorno.
Este flujo es útil para alertas internas, confirmaciones transaccionales e informes destinados a personas autorizadas. No lo uses para mensajes no solicitados. Los proveedores aplican cuotas y pueden exigir contraseña de aplicación, token u otro mecanismo. Consulta la documentación oficial de smtplib, el paquete email de Python y la guía de SSL/TLS.
Configurar credenciales sin exponerlas
Define las variables en la plataforma, un archivo local excluido de Git o un gestor de secretos. No publiques valores reales, no los incorpores a la imagen de un contenedor y no imprimas todo el entorno al depurar.
export SMTP_HOST=smtp.example.com
export SMTP_PORT=587
export [email protected]
export SMTP_PASSWORD=una-contrasena-de-aplicacion
export [email protected]
En PowerShell, usa $env:SMTP_HOST = "smtp.example.com" para la sesión actual. En producción, prefiere el mecanismo de secretos del servicio. Aísla dependencias en un entorno virtual de Python y separa configuración y código.
Enviar texto y HTML juntos
EmailMessage administra cabeceras y estructura MIME. Añade primero texto simple y después HTML como alternativa; el cliente elige el formato compatible. Escapa valores dinámicos antes de introducirlos en HTML. Aquí se utiliza html.escape.
import html
import os
import smtplib
import ssl
from email.message import EmailMessage
def enviar_bienvenida(destinatario: str, nombre: str) -> None:
host = os.environ["SMTP_HOST"]
puerto = int(os.environ.get("SMTP_PORT", "587"))
usuario = os.environ["SMTP_USER"]
contrasena = os.environ["SMTP_PASSWORD"]
remitente = os.environ.get("SMTP_FROM", usuario)
nombre_seguro = html.escape(nombre)
mensaje = EmailMessage()
mensaje["From"] = remitente
mensaje["To"] = destinatario
mensaje["Subject"] = "Tu acceso está listo"
mensaje.set_content(
f"Hola, {nombre}.\n\nTu acceso está listo. "
"Entra desde la dirección oficial de la aplicación.\n"
)
mensaje.add_alternative(
f"""\
<html><body>
<p>Hola, <strong>{nombre_seguro}</strong>.</p>
<p>Tu acceso está listo. Entra desde la dirección oficial.</p>
</body></html>
""",
subtype="html",
)
contexto = ssl.create_default_context()
with smtplib.SMTP(host, puerto, timeout=30) as servidor:
servidor.ehlo()
servidor.starttls(context=contexto)
servidor.ehlo()
servidor.login(usuario, contrasena)
servidor.send_message(mensaje)
create_default_context() activa la validación del certificado y valores seguros. No desactives la verificación para eludir errores: corrige el host, la cadena o el almacén de confianza. El segundo ehlo() actualiza las capacidades después de iniciar TLS.
Añadir adjuntos con límites explícitos
Valida que el archivo exista, esté permitido y no sea demasiado grande. La codificación aumenta el tamaño y muchos servidores rechazan mensajes pesados. Esta función limita cada adjunto a 10 MB y detecta el tipo probable con mimetypes.
import mimetypes
from pathlib import Path
from email.message import EmailMessage
LIMITE_ADJUNTO = 10 * 1024 * 1024
def adjuntar(mensaje: EmailMessage, ruta: Path) -> None:
ruta = ruta.resolve()
if not ruta.is_file():
raise FileNotFoundError(ruta)
if ruta.stat().st_size > LIMITE_ADJUNTO:
raise ValueError(f"El adjunto supera 10 MB: {ruta.name}")
tipo, _ = mimetypes.guess_type(ruta.name)
tipo_principal, subtipo = (tipo or "application/octet-stream").split("/", 1)
mensaje.add_attachment(
ruta.read_bytes(),
maintype=tipo_principal,
subtype=subtipo,
filename=ruta.name,
)
## Después de crear el mensaje:
## adjuntar(mensaje, Path("informe.pdf"))
No aceptes rutas arbitrarias proporcionadas por usuarios. Restringe los archivos a una carpeta conocida y confirma que la ruta resuelta permanece dentro. Nuestra guía de pathlib en Python explica operaciones robustas. Evita ejecutables e inspecciona contenido recibido de otros sistemas.
Proyecto práctico: informe diario
Crea una función que reciba métricas calculadas, genere texto y HTML, adjunte opcionalmente un PDF y envíe solo a una lista autorizada. Separa la obtención de datos del transporte SMTP. Así podrás probar el mensaje sin red e integrarlo en una automatización de tareas con Python.
def crear_informe(destinatario: str, total: int) -> EmailMessage:
if "\n" in destinatario or "\r" in destinatario:
raise ValueError("Destinatario no válido")
msg = EmailMessage()
msg["From"] = os.environ["SMTP_FROM"]
msg["To"] = destinatario
msg["Subject"] = "Resumen diario"
msg.set_content(f"Resumen diario\nElementos procesados: {total}\n")
msg.add_alternative(
f"<p>Resumen diario</p><p>Elementos procesados: <strong>{total}</strong></p>",
subtype="html",
)
return msg
Durante el desarrollo, utiliza un servidor SMTP de prueba que capture mensajes sin entregarlos. Revisa asunto, destinatarios, texto, HTML y adjuntos. En pruebas unitarias, sustituye la conexión por un objeto falso; la guía de pruebas unitarias con Python muestra cómo aislar dependencias.
Tratar errores sin ocultar la causa
Una caída de red puede ser temporal; una autenticación incorrecta o un destinatario rechazado suele requerir corrección. Captura excepciones específicas, registra solo contexto no sensible y decide si reintentar tiene sentido. Usa un máximo bajo, esperas crecientes y un identificador que reduzca duplicados.
import logging
import smtplib
import socket
logger = logging.getLogger(__name__)
try:
enviar_bienvenida("[email protected]", "Ana")
except smtplib.SMTPAuthenticationError:
logger.error("El servidor rechazó la autenticación SMTP")
raise
except smtplib.SMTPRecipientsRefused:
logger.warning("El servidor SMTP rechazó un destinatario")
raise
except (smtplib.SMTPException, TimeoutError, socket.timeout, OSError):
logger.exception("Falló la comunicación SMTP")
raise
No registres contraseñas, cuerpo, adjuntos ni direcciones completas cuando sean datos personales. Un identificador interno suele bastar. Aplica nuestra guía de tratamiento de errores en Python y el tutorial de logging en Python. En una cola, marca éxito tras la aceptación del servidor, sabiendo que aceptar no garantiza la entrega final ni la lectura.
Usar SMTP_SSL para TLS implícito
Si el proveedor exige una conexión cifrada desde el inicio, normalmente en el puerto 465, usa SMTP_SSL en vez de starttls(). Sigue su documentación y no decidas el protocolo únicamente por el número del puerto.
contexto = ssl.create_default_context()
with smtplib.SMTP_SSL(host, 465, context=contexto, timeout=30) as servidor:
servidor.login(usuario, contrasena)
servidor.send_message(mensaje)
Valida direcciones e impide la inyección de cabeceras
Una dirección con formato razonable no es necesariamente entregable, pero una validación básica evita cabeceras mal formadas y errores evidentes. Nunca concatenes un valor no confiable dentro de texto de cabecera sin estructurar. EmailMessage proporciona cabeceras estructuradas, aunque la entrada de la aplicación también debe rechazar retornos de carro y saltos de línea. Analiza direcciones con las utilidades de email.utils, aplica las reglas del producto y confirma la propiedad mediante un mensaje de verificación cuando la identidad sea importante. No intentes demostrar que un buzón existe haciendo consultas SMTP especulativas al servidor del destinatario; son poco fiables y pueden parecer tráfico abusivo.
Mantén explícita la función de To, Cc y Bcc. Quienes aparecen en Bcc reciben el mensaje sin figurar en las cabeceras visibles transmitidas, pero sus direcciones siguen siendo procesadas por la aplicación y el proveedor. Para avisos destinados a personas sin relación entre ellas, envía mensajes individuales en vez de exponer una lista compartida. Limita la cantidad de destinatarios por operación y exige una lista autorizada para informes internos sensibles.
Comprende la entrega después de la aceptación SMTP
El éxito de send_message() significa que el servidor SMTP contactado aceptó la responsabilidad por el mensaje. No demuestra que llegó a la bandeja de entrada, que fue mostrado ni que alguien lo leyó. Un rebote posterior puede informar de un buzón inexistente, un rechazo de política o un fallo temporal. Las integraciones de producción necesitan un mecanismo del proveedor para procesar rebotes y quejas, además de un identificador interno que conecte esos eventos con el aviso original sin almacenar todo el cuerpo en los registros.
Los reintentos requieren atención porque un timeout puede producirse después de que el servidor remoto aceptó el mensaje. Añade una clave estable de entrega, conserva el estado alrededor de la operación de cola y permite detectar repeticiones. La cabecera estándar Message-ID ayuda a identificar mensajes, pero no constituye por sí sola una garantía universal de idempotencia. Reintenta únicamente fallos temporales, usa una demora exponencial limitada y mueve los trabajos agotados a una cola de revisión.
Autenticación y reputación del dominio
Las credenciales SMTP autorizan la conexión, mientras que la autenticación del dominio ayuda a los receptores a evaluar el mensaje. Configura SPF, DKIM y DMARC con el proveedor y el administrador de DNS. Alinea el remitente visible con un dominio que el servicio tenga permiso para usar. Estos controles no se implementan solamente desde un script Python y ninguno garantiza llegar a la bandeja principal, pero omitirlos suele perjudicar la confianza y la entrega.
Para comunicaciones masivas o comerciales, emplea un servicio preparado para registrar consentimiento, mantener listas de supresión, gestionar bajas, limitar el ritmo y recibir quejas. Un bucle directo de smtplib es apropiado para trabajo transaccional controlado, no para evitar esas responsabilidades operativas y legales.
Errores comunes y lista de verificación
- 535 Authentication failed: revisa cuenta, secreto, política de autenticación y necesidad de contraseña de aplicación.
- Error TLS o de certificado: comprueba host, reloj y cadena; nunca desactives la validación.
- Correo en spam: configura SPF, DKIM y DMARC con el proveedor; el código no garantiza reputación.
- Adjunto rechazado: reduce su tamaño y revisa límites del archivo y del mensaje total.
- HTML roto: conserva el texto alternativo, escapa datos y prueba clientes representativos.
Antes de desplegar, confirma: credenciales fuera del código; TLS verificado; timeout definido; remitente autorizado; destinatarios validados; consentimiento y baja cuando correspondan; adjuntos limitados; alternativa de texto; registros mínimos; pruebas sin entrega real; reintentos finitos; fallos monitorizados. Estos controles vuelven predecible una integración sencilla sin confundir aceptación SMTP con entrega garantizada.