27. Envío Automático de Emails
SMTP, attachments, HTML emails y seguridad
Generar reportes es solo la mitad del trabajo. En esta lección aprenderás a enviarlos automáticamente por email con adjuntos, HTML profesional y listas de distribución dinámicas.
¿Por qué automatizar el envío de emails?
El problema manual
Sin automatización, después de generar reportes debes:
- Abrir cliente de email
- Adjuntar archivos uno por uno
- Copiar lista de destinatarios
- Escribir asunto y mensaje
- Verificar que se envió correctamente
- Repetir para cada stakeholder o departamento
Tiempo: 10-15 minutos por reporte x 20 reportes mensuales = 4 horas/mes
La solución automatizada
Un script Python ejecuta todo automáticamente:
- Envía a múltiples destinatarios simultáneamente
- Adjunta archivos PDF, Excel, imágenes
- Formatea emails con HTML profesional
- Incluye contenido dinámico (métricas en el cuerpo)
- Maneja errores y reintentos
- Registra envíos exitosos en log
Tiempo: 0 minutos de intervención humana
Fundamentos de email con Python
Protocolo SMTP (Simple Mail Transfer Protocol)
Python usa smtplib para conectarse a servidores SMTP:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
# Servidores SMTP comunes
SMTP_SERVERS = {
'Gmail': ('smtp.gmail.com', 587),
'Outlook': ('smtp-mail.outlook.com', 587),
'Yahoo': ('smtp.mail.yahoo.com', 587),
'Office365': ('smtp.office365.com', 587),
'Custom': ('smtp.tuempresa.com', 587)
}
Tipos de contenido MIME
# Texto plano
MIMEText('Contenido texto plano', 'plain')
# HTML
MIMEText('<h1>Contenido HTML</h1>', 'html')
# Adjunto
MIMEBase('application', 'octet-stream')
# Multipart (combina texto + adjuntos)
MIMEMultipart()
Implementación completa
Paso 1: Configuración segura de credenciales
# config.py
"""
Configuración de email - NO SUBIR A GIT
"""
import os
from dotenv import load_dotenv
# Cargar variables de entorno
load_dotenv()
EMAIL_CONFIG = {
# Credenciales (usar variables de entorno)
'smtp_server': 'smtp.gmail.com',
'smtp_port': 587,
'email_usuario': os.getenv('EMAIL_USUARIO', 'analytics@tuempresa.com'),
'email_password': os.getenv('EMAIL_PASSWORD'), # App Password para Gmail
# Configuración de envío
'remitente_nombre': 'Analytics Team',
'usar_tls': True,
'timeout': 30,
# Listas de distribución
'destinatarios': {
'ejecutivos': [
'ceo@tuempresa.com',
'cfo@tuempresa.com',
'coo@tuempresa.com'
],
'marketing': [
'marketing-director@tuempresa.com',
'marketing-analyst@tuempresa.com'
],
'ventas': [
'sales-manager@tuempresa.com',
'sales-ops@tuempresa.com'
],
'todos': [
'analytics@tuempresa.com',
'team@tuempresa.com'
]
}
}
# Para Gmail: Generar App Password en https://myaccount.google.com/apppasswords
# Para Outlook/Office365: Habilitar SMTP en configuración de cuenta
Paso 2: Clase EmailSender robusta
# email_sender.py
"""
Sistema de envío automático de emails con reportes
"""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email import encoders
from datetime import datetime
import os
import logging
from config import EMAIL_CONFIG
# Configurar logging
logging.basicConfig(
filename='logs/email_sender.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class EmailSender:
"""Envía emails automáticamente con adjuntos y HTML"""
def __init__(self):
"""Inicializa conexión SMTP"""
self.config = EMAIL_CONFIG
self.smtp_server = None
def conectar_smtp(self):
"""Establece conexión con servidor SMTP"""
try:
self.smtp_server = smtplib.SMTP(
self.config['smtp_server'],
self.config['smtp_port'],
timeout=self.config['timeout']
)
self.smtp_server.ehlo()
if self.config['usar_tls']:
self.smtp_server.starttls()
self.smtp_server.ehlo()
self.smtp_server.login(
self.config['email_usuario'],
self.config['email_password']
)
logging.info("Conexión SMTP establecida exitosamente")
return True
except smtplib.SMTPAuthenticationError:
logging.error("Error de autenticación SMTP - Verificar credenciales")
return False
except smtplib.SMTPException as e:
logging.error(f"Error SMTP: {e}")
return False
except Exception as e:
logging.error(f"Error conectando a SMTP: {e}")
return False
def crear_mensaje_html(self, titulo, metricas=None, contenido_adicional=""):
"""Crea email HTML profesional con métricas"""
# Extraer métricas si existen
metricas_html = ""
if metricas:
metricas_html = f"""
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin: 30px 0;">
<div style="background: linear-gradient(135deg, #ae4291 0%, #35286b 100%);
color: white; padding: 20px; border-radius: 10px; text-align: center;">
<div style="font-size: 14px; opacity: 0.9;">Ventas Totales</div>
<div style="font-size: 32px; font-weight: bold; margin: 10px 0;">
${metricas.get('total_ventas', 0):,.0f}
</div>
<div style="font-size: 14px;">
{metricas.get('var_ventas', 0):+.1f}% vs mes anterior
</div>
</div>
<div style="background: linear-gradient(135deg, #ae4291 0%, #35286b 100%);
color: white; padding: 20px; border-radius: 10px; text-align: center;">
<div style="font-size: 14px; opacity: 0.9;">Transacciones</div>
<div style="font-size: 32px; font-weight: bold; margin: 10px 0;">
{metricas.get('num_transacciones', 0):,}
</div>
<div style="font-size: 14px;">
{metricas.get('var_transacciones', 0):+.1f}% vs mes anterior
</div>
</div>
</div>
"""
html_template = f"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0; padding: 0; background-color: #f5f5f5;">
<div style="max-width: 600px; margin: 20px auto; background-color: white;
border-radius: 10px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<!-- Header -->
<div style="background: linear-gradient(135deg, #35286b 0%, #ae4291 100%);
padding: 30px; text-align: center; color: white;">
<h1 style="margin: 0; font-size: 28px;">{titulo}</h1>
<p style="margin: 10px 0 0 0; opacity: 0.9;">
{datetime.now().strftime('%d de %B, %Y')}
</p>
</div>
<!-- Contenido -->
<div style="padding: 30px;">
<p style="font-size: 16px; line-height: 1.6; color: #333;">
Hola,
</p>
<p style="font-size: 16px; line-height: 1.6; color: #333;">
Adjunto encontrarás el reporte automatizado de análisis.
A continuación un resumen de las métricas clave:
</p>
{metricas_html}
{contenido_adicional}
<p style="font-size: 16px; line-height: 1.6; color: #333;">
Para más detalles, por favor revisa los archivos adjuntos.
</p>
<div style="margin: 30px 0; padding: 20px; background-color: #f8f9fa;
border-left: 4px solid #ae4291; border-radius: 5px;">
<p style="margin: 0; font-size: 14px; color: #666;">
<strong>Nota:</strong> Este email fue generado automáticamente.
Si tienes preguntas, contacta al equipo de Analytics.
</p>
</div>
</div>
<!-- Footer -->
<div style="background-color: #f8f9fa; padding: 20px; text-align: center;
border-top: 1px solid #dee2e6;">
<p style="margin: 0; font-size: 14px; color: #666;">
Analytics Team | Powered by Python
</p>
</div>
</div>
</body>
</html>
"""
return html_template
def adjuntar_archivo(self, mensaje, ruta_archivo):
"""Adjunta archivo al email"""
try:
# Verificar que archivo existe
if not os.path.exists(ruta_archivo):
logging.warning(f"Archivo no encontrado: {ruta_archivo}")
return False
# Determinar tipo MIME basado en extensión
nombre_archivo = os.path.basename(ruta_archivo)
extension = nombre_archivo.split('.')[-1].lower()
mime_types = {
'pdf': ('application', 'pdf'),
'xlsx': ('application', 'vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
'csv': ('text', 'csv'),
'png': ('image', 'png'),
'jpg': ('image', 'jpeg'),
'jpeg': ('image', 'jpeg')
}
maintype, subtype = mime_types.get(extension, ('application', 'octet-stream'))
# Leer y adjuntar archivo
with open(ruta_archivo, 'rb') as archivo:
if maintype == 'image':
adjunto = MIMEImage(archivo.read(), _subtype=subtype)
else:
adjunto = MIMEBase(maintype, subtype)
adjunto.set_payload(archivo.read())
encoders.encode_base64(adjunto)
adjunto.add_header(
'Content-Disposition',
f'attachment; filename={nombre_archivo}'
)
mensaje.attach(adjunto)
logging.info(f"Archivo adjuntado: {nombre_archivo}")
return True
except Exception as e:
logging.error(f"Error adjuntando archivo {ruta_archivo}: {e}")
return False
def enviar_email(self, destinatarios, asunto, cuerpo_html, archivos_adjuntos=None, metricas=None):
"""
Envía email con adjuntos
Args:
destinatarios: Lista de emails o string con grupo ('ejecutivos', 'marketing')
asunto: Asunto del email
cuerpo_html: HTML del email o None para usar plantilla
archivos_adjuntos: Lista de rutas de archivos a adjuntar
metricas: Dict con métricas para incluir en template
"""
try:
# Resolver destinatarios si es un grupo
if isinstance(destinatarios, str):
destinatarios = self.config['destinatarios'].get(
destinatarios,
[destinatarios]
)
# Crear mensaje
mensaje = MIMEMultipart()
mensaje['From'] = f"{self.config['remitente_nombre']} <{self.config['email_usuario']}>"
mensaje['To'] = ', '.join(destinatarios)
mensaje['Subject'] = asunto
mensaje['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
# Usar plantilla HTML si no se proporciona cuerpo
if cuerpo_html is None:
cuerpo_html = self.crear_mensaje_html(asunto, metricas)
# Adjuntar cuerpo HTML
mensaje.attach(MIMEText(cuerpo_html, 'html', 'utf-8'))
# Adjuntar archivos
if archivos_adjuntos:
for archivo in archivos_adjuntos:
self.adjuntar_archivo(mensaje, archivo)
# Conectar y enviar
if not self.smtp_server or not self.smtp_server.sock:
if not self.conectar_smtp():
return False
self.smtp_server.send_message(mensaje)
logging.info(f"Email enviado exitosamente a {len(destinatarios)} destinatarios")
print(f"✅ Email enviado a: {', '.join(destinatarios)}")
return True
except smtplib.SMTPException as e:
logging.error(f"Error SMTP enviando email: {e}")
print(f"❌ Error SMTP: {e}")
return False
except Exception as e:
logging.error(f"Error enviando email: {e}")
print(f"❌ Error: {e}")
return False
def enviar_reporte_completo(self, grupo_destinatarios, pdf_path, excel_path, html_path, metricas):
"""Envía reporte completo con todos los adjuntos"""
fecha = datetime.now().strftime('%Y-%m-%d')
asunto = f"Reporte de Ventas - {fecha}"
archivos = [pdf_path, excel_path, html_path]
archivos_existentes = [f for f in archivos if os.path.exists(f)]
if not archivos_existentes:
logging.error("No se encontraron archivos para adjuntar")
return False
return self.enviar_email(
destinatarios=grupo_destinatarios,
asunto=asunto,
cuerpo_html=None, # Usa template
archivos_adjuntos=archivos_existentes,
metricas=metricas
)
def cerrar_conexion(self):
"""Cierra conexión SMTP"""
if self.smtp_server:
try:
self.smtp_server.quit()
logging.info("Conexión SMTP cerrada")
except:
pass
Paso 3: Script de integración completa
# send_report.py
"""
Genera y envía reporte automáticamente
"""
from report_generator import ReportGenerator
from email_sender import EmailSender
from datetime import datetime
import os
def main():
"""Genera reportes y los envía por email"""
print("="*60)
print("📧 GENERACIÓN Y ENVÍO AUTOMÁTICO DE REPORTES")
print("="*60)
print(f"Inicio: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
# Crear directorios necesarios
os.makedirs('logs', exist_ok=True)
try:
# 1. Generar reportes
print("📊 PASO 1: Generando reportes...")
generator = ReportGenerator(data_path='data/ventas.csv')
pdf_file = generator.generar_reporte_pdf()
excel_file = generator.generar_reporte_excel()
html_file = generator.generar_reporte_html()
# Obtener métricas para incluir en email
metricas = generator.calcular_metricas()
# 2. Enviar por email
print("\n📧 PASO 2: Enviando reportes por email...")
sender = EmailSender()
# Enviar a ejecutivos (solo PDF)
print("\n→ Enviando a ejecutivos...")
sender.enviar_email(
destinatarios='ejecutivos',
asunto=f"Reporte Ejecutivo de Ventas - {datetime.now().strftime('%Y-%m-%d')}",
cuerpo_html=None,
archivos_adjuntos=[pdf_file],
metricas=metricas
)
# Enviar a marketing (PDF + Excel)
print("\n→ Enviando a marketing...")
sender.enviar_email(
destinatarios='marketing',
asunto=f"Reporte de Marketing - {datetime.now().strftime('%Y-%m-%d')}",
cuerpo_html=None,
archivos_adjuntos=[pdf_file, excel_file],
metricas=metricas
)
# Enviar reporte completo a analytics
print("\n→ Enviando reporte completo a analytics...")
sender.enviar_reporte_completo(
grupo_destinatarios='todos',
pdf_path=pdf_file,
excel_path=excel_file,
html_path=html_file,
metricas=metricas
)
# Cerrar conexión
sender.cerrar_conexion()
print("\n" + "="*60)
print("✅ PROCESO COMPLETADO EXITOSAMENTE")
print("="*60)
print(f"Fin: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
except Exception as e:
print(f"\n❌ Error en el proceso: {e}")
logging.error(f"Error en proceso principal: {e}")
if __name__ == "__main__":
main()
Seguridad y mejores prácticas
1. No hardcodear contraseñas
# ❌ MAL - Contraseña en código
password = "mi_password_secreto"
# ✅ BIEN - Variables de entorno
import os
password = os.getenv('EMAIL_PASSWORD')
# ✅ MEJOR - Archivo .env (NO SUBIR A GIT)
from dotenv import load_dotenv
load_dotenv()
password = os.getenv('EMAIL_PASSWORD')
2. Usar App Passwords (Gmail)
Gmail requiere "App Passwords" en vez de contraseña real:
- Ir a https://myaccount.google.com/apppasswords
- Seleccionar "Mail" y "Other (Custom name)"
- Copiar password de 16 caracteres
- Usar ese password en tu script
3. Rate limiting y buenas prácticas
import time
# No enviar más de 100 emails/minuto
for destinatario in destinatarios:
enviar_email(destinatario, asunto, cuerpo)
time.sleep(0.6) # Esperar 0.6 segundos entre envíos
4. Manejo de errores y reintentos
def enviar_con_reintentos(email_func, max_intentos=3):
"""Reintenta envío si falla"""
for intento in range(max_intentos):
try:
resultado = email_func()
if resultado:
return True
except Exception as e:
logging.warning(f"Intento {intento + 1} falló: {e}")
if intento < max_intentos - 1:
time.sleep(5) # Esperar 5 segundos antes de reintentar
return False
Casos de uso avanzados
1. Emails personalizados por destinatario
for destinatario in ['ceo@empresa.com', 'cfo@empresa.com']:
# Filtrar métricas específicas para cada rol
metricas_personalizadas = filtrar_por_rol(destinatario)
enviar_email(
destinatarios=[destinatario],
asunto=f"Tu Reporte Personalizado - {datetime.now()}",
metricas=metricas_personalizadas
)
2. Alertas condicionales
# Enviar solo si hay cambios significativos
if abs(metricas['var_ventas']) > 10:
enviar_email(
destinatarios='ejecutivos',
asunto="🚨 ALERTA: Cambio significativo en ventas",
metricas=metricas
)
3. Resúmenes semanales vs diarios
from datetime import datetime
dia = datetime.now().weekday()
if dia == 0: # Lunes - reporte semanal
enviar_reporte_semanal()
else: # Otros días - reporte diario
enviar_reporte_diario()
Resumen de la lección
✅ Implementaste sistema completo de envío de emails con Python ✅ Creaste templates HTML profesionales para emails ✅ Configuraste adjuntos múltiples (PDF, Excel, imágenes) ✅ Aprendiste seguridad y best practices (App Passwords, variables de entorno) ✅ Desarrollaste listas de distribución y envíos personalizados
🎯 Próxima lección: 28. Scheduling con Cron - Programa estos scripts para ejecución automática
En la siguiente lección aprenderás a programar estos scripts para que se ejecuten automáticamente cada día, semana o mes sin ninguna intervención manual. ¡La automatización total está a una lección de distancia!
¿Completaste esta lección?
Marca esta lección como completada. Tu progreso se guardará en tu navegador.