Automatizar correos con Python permite enviar informes, alertas, facturas, recordatorios y mensajes personalizados sin repetir tareas manuales. La biblioteca estándar incluye smtplib para comunicarse con servidores SMTP y el paquete email para construir mensajes con texto, HTML y archivos adjuntos.
Para complementar este tutorial, consulta las guías sobre entornos virtuales, archivos, diccionarios, logging y automatización de tareas. Como referencias externas, revisa la documentación oficial del paquete email y la documentación oficial de smtplib.
Cómo funciona el envío
Tu script no entrega directamente el correo al destinatario. Se conecta a un servidor SMTP, se autentica y le entrega el mensaje. El proveedor se encarga de la transmisión posterior. Necesitas conocer el host, el puerto y el método de seguridad de tu servicio.
No guardar contraseñas en el código
Las credenciales deben permanecer fuera del archivo:
import os
USUARIO = os.environ["SMTP_USUARIO"]
CONTRASENA = os.environ["SMTP_CONTRASENA"]
Configura variables de entorno o utiliza un gestor de secretos. Añadir una contraseña directamente al repositorio puede comprometer la cuenta incluso si después borras la línea.
Crear un mensaje simple
from email.message import EmailMessage
mensaje = EmailMessage()
mensaje["Subject"] = "Informe semanal"
mensaje["From"] = "[email protected]"
mensaje["To"] = "[email protected]"
mensaje.set_content("El informe semanal está listo.")
EmailMessage administra cabeceras, cuerpo y adjuntos de forma más segura que construir el texto SMTP manualmente.
Enviar mediante SSL
import smtplib
import ssl
contexto = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.example.com", 465, context=contexto) as servidor:
servidor.login(USUARIO, CONTRASENA)
servidor.send_message(mensaje)
Los valores dependen del proveedor. No desactives la verificación TLS para resolver errores de certificados.
Enviar mediante STARTTLS
with smtplib.SMTP("smtp.example.com", 587) as servidor:
servidor.ehlo()
servidor.starttls(context=contexto)
servidor.ehlo()
servidor.login(USUARIO, CONTRASENA)
servidor.send_message(mensaje)
STARTTLS inicia una conexión normal y después la protege. Utiliza el método indicado por el proveedor.
Contenido HTML con alternativa de texto
mensaje.set_content("Tu navegador de correo no muestra HTML. Total: 125,00 €")
mensaje.add_alternative("""
<h1>Informe</h1>
<p>El total procesado fue <strong>125,00 €</strong>.</p>
""", subtype="html")
Incluye siempre una versión de texto. Mejora compatibilidad, accesibilidad y entrega.
Añadir un archivo adjunto
from pathlib import Path
ruta = Path("informe.pdf")
contenido = ruta.read_bytes()
mensaje.add_attachment(
contenido,
maintype="application",
subtype="pdf",
filename=ruta.name,
)
Comprueba que el archivo exista, tenga un tamaño razonable y pertenezca al envío correcto.
Detectar el tipo MIME
import mimetypes
mime, _ = mimetypes.guess_type(ruta.name)
maintype, subtype = (mime or "application/octet-stream").split("/", 1)
mensaje.add_attachment(
ruta.read_bytes(),
maintype=maintype,
subtype=subtype,
filename=ruta.name,
)
El tipo genérico funciona cuando no se reconoce la extensión.
Varios destinatarios
destinatarios = ["[email protected]", "[email protected]"]
mensaje["To"] = ", ".join(destinatarios)
Para listas privadas, no expongas las direcciones en To. Envía mensajes individuales o utiliza BCC con cuidado. Los mensajes individuales permiten personalización y mejor control de errores.
CC y BCC
mensaje["Cc"] = "[email protected]"
bcc = ["[email protected]"]
todos = destinatarios + [mensaje["Cc"]] + bcc
servidor.send_message(mensaje, to_addrs=todos)
BCC no debe aparecer como cabecera visible. La lista se entrega mediante to_addrs.
Personalizar mensajes
PLANTILLA = "Hola {nombre}, tu pedido {pedido} está listo."
for cliente in clientes:
cuerpo = PLANTILLA.format(
nombre=cliente["nombre"],
pedido=cliente["pedido"],
)
Valida que los campos existan y escapa valores antes de insertarlos en HTML. No permitas que datos externos modifiquen cabeceras.
Crear una función reutilizable
def crear_mensaje(remitente, destinatario, asunto, texto, html=None):
mensaje = EmailMessage()
mensaje["From"] = remitente
mensaje["To"] = destinatario
mensaje["Subject"] = asunto
mensaje.set_content(texto)
if html:
mensaje.add_alternative(html, subtype="html")
return mensaje
Separar creación y envío facilita las pruebas. Puedes comprobar cabeceras y cuerpo sin conectarte a un servidor.
Manejo de errores
import logging
import smtplib
try:
with smtplib.SMTP_SSL(HOST, PUERTO, context=contexto) as servidor:
servidor.login(USUARIO, CONTRASENA)
servidor.send_message(mensaje)
except smtplib.SMTPAuthenticationError:
logging.exception("Credenciales SMTP rechazadas")
except smtplib.SMTPRecipientsRefused:
logging.exception("Destinatario rechazado")
except (smtplib.SMTPException, OSError):
logging.exception("No fue posible enviar el correo")
Captura excepciones específicas y registra contexto suficiente, pero nunca credenciales ni el contenido completo de mensajes sensibles.
Reintentos responsables
Reintenta solo errores temporales, con espera creciente y límite:
import time
def enviar_con_reintentos(enviar, intentos=3):
for numero in range(1, intentos + 1):
try:
return enviar()
except (smtplib.SMTPServerDisconnected, TimeoutError, OSError):
if numero == intentos:
raise
time.sleep(numero * 3)
No reintentes indefinidamente una autenticación incorrecta o una dirección rechazada.
Evitar envíos duplicados
Guarda un identificador por tarea, destinatario y periodo. Antes de enviar, comprueba si ya existe un registro exitoso. Después marca el envío únicamente cuando el servidor lo haya aceptado. En sistemas críticos, utiliza una cola con estados como pendiente, procesando, enviado y fallido.
Programar el script
En Windows puedes usar el Programador de tareas. En Linux o macOS, cron es una opción habitual. También puedes ejecutar el proceso desde un servidor, contenedor o sistema de colas. La programación debe llamar al script; no es necesario mantener un while True activo.
Límites del proveedor
Los servicios aplican límites de destinatarios y frecuencia. Un script que envía miles de mensajes rápidamente puede bloquear la cuenta. Para campañas, utiliza una plataforma de correo transaccional o marketing con gestión de bajas, reputación y devoluciones.
Consentimiento y privacidad
Envía mensajes únicamente a destinatarios que los esperan. Incluye mecanismos apropiados de cancelación cuando corresponda y almacena solo los datos necesarios. No uses este tipo de automatización para correo masivo no solicitado.
Probar con un servidor local
Durante el desarrollo, utiliza un servidor SMTP de pruebas o una herramienta que capture mensajes. Esto evita enviar correos reales por accidente. También puedes abstraer el transportador y sustituirlo por una función falsa en los tests.
Pruebas unitarias
def test_crear_mensaje():
mensaje = crear_mensaje(
"[email protected]",
"[email protected]",
"Prueba",
"Contenido",
)
assert mensaje["Subject"] == "Prueba"
assert "Contenido" in mensaje.get_body(preferencelist=("plain",)).get_content()
Prueba también adjuntos, caracteres Unicode, varios destinatarios y datos incompletos.
Errores frecuentes
Los problemas habituales son guardar contraseñas en el código, usar el puerto equivocado, olvidar STARTTLS, construir mensajes manualmente, cargar adjuntos enormes, revelar destinatarios y ejecutar reintentos sin límite. También es común usar la contraseña normal cuando el proveedor exige una contraseña de aplicación u OAuth.
Conclusión
La combinación de EmailMessage y smtplib permite automatizar correos fiables con la biblioteca estándar. Protege credenciales, utiliza TLS, incluye texto alternativo, valida adjuntos, controla reintentos y registra cada resultado. Para grandes volúmenes, delega la entrega en un proveedor especializado y mantén el consentimiento como requisito central.






