Enviar emails desde Python con `smtplib` — Sin liarte con SMTP
Casi cualquier proyecto que automatiza algo acaba necesitando enviar emails: notificaciones, informes, alertas cuando un script falla, confirmaciones de registro, ofertas semanales. Python lo hace fácil con smtplib (en la stdlib, viene de fábrica) y email (también stdlib) para construir el mensaje.
En esta entrada te enseño los patrones que de verdad usas: configurar el envío, mandar texto plano, HTML, adjuntar ficheros, manejar Gmail con app passwords (que es el caso típico para empezar) y los errores que te vuelven loco la primera vez.
Contenido
- 1 La idea en 30 segundos
- 2 Configuración SMTP — los datos que necesitas
- 3 Gmail con App Password (lo que te toca casi seguro)
- 4 El template básico
- 5 Email en HTML
- 6 Adjuntos
- 7 Múltiples destinatarios + CC + BCC
- 8 Caso real típico: alerta cuando un script falla
- 9 Caso real: enviar informe semanal con CSV adjunto
- 10 ¿Y para mailings masivos? Servicios profesionales
- 11 Errores típicos al empezar
- 12 Resumen
- 13 Tu siguiente paso
La idea en 30 segundos
Para enviar un email necesitas:
- Servidor SMTP (Gmail, Outlook, tu propio dominio…) con sus credenciales.
- Un mensaje con
from,to,subject,body. - Conectarte, autenticarte, enviar, desconectar.
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "tu@gmail.com"
msg["To"] = "destinatario@example.com"
msg["Subject"] = "Hola desde Python"
msg.set_content("Este es el cuerpo del email.")
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login("tu@gmail.com", "TU-APP-PASSWORD")
smtp.send_message(msg)
8 líneas. Email enviado.
Configuración SMTP — los datos que necesitas
Cada proveedor tiene los suyos. Los más comunes:
| Proveedor | Servidor | Puerto | Conexión |
|---|---|---|---|
| Gmail | smtp.gmail.com | 465 | SSL |
| Gmail (alternativa) | smtp.gmail.com | 587 | STARTTLS |
| Outlook/Hotmail | smtp-mail.outlook.com | 587 | STARTTLS |
| Yahoo | smtp.mail.yahoo.com | 465 | SSL |
| Tu dominio propio | suele ser mail.tu-dominio.com | 465/587 | depende |
Si tienes dudas, busca “SMTP settings ” — está documentado.
Gmail con App Password (lo que te toca casi seguro)
A día de hoy, Gmail no acepta tu contraseña de Google directamente desde scripts. Necesitas crear un App Password específico para tu app:
- Activa verificación en dos pasos en tu cuenta Google (obligatorio para los App Passwords).
- Ve a myaccount.google.com/apppasswords.
- Crea una contraseña nueva (“Python script”, o lo que quieras).
- Te dan una contraseña de 16 caracteres tipo
abcd efgh ijkl mnop. Esa es la que usas en el script.
smtp.login("tu@gmail.com", "abcd efgh ijkl mnop")
⚠️ Nunca pongas la app password en código que vayas a subir a git. Métela en una variable de entorno o
.env. La revoco si se filtra y vuelvo a generar — pero mejor no llegar ahí.
import os
PASSWORD = os.environ["GMAIL_APP_PASSWORD"]
📥 Llévate el cheatsheet de Python (gratis)
PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.
Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.
El template básico
import smtplib
from email.message import EmailMessage
def enviar_email(destinatario: str, asunto: str, cuerpo: str) -> None:
msg = EmailMessage()
msg["From"] = "tu@gmail.com"
msg["To"] = destinatario
msg["Subject"] = asunto
msg.set_content(cuerpo)
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login("tu@gmail.com", os.environ["GMAIL_APP_PASSWORD"])
smtp.send_message(msg)
enviar_email(
"destinatario@example.com",
"Tu informe semanal",
"Adjunto los datos de esta semana."
)
SMTP_SSL con puerto 465 es la opción más simple. Si tu proveedor solo soporta 587, usa SMTP con starttls():
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
smtp.starttls()
smtp.login(...)
smtp.send_message(msg)
💡 Tip-friki: la diferencia entre 465 (SSL) y 587 (STARTTLS) es cuándo se cifra la conexión. SSL desde el principio. STARTTLS empieza en plano y se actualiza a cifrado. Funcionalmente equivalente.
Email en HTML
Si quieres formatear con negrita, listas, links:
msg = EmailMessage()
msg["From"] = "tu@gmail.com"
msg["To"] = "destinatario@example.com"
msg["Subject"] = "Informe HTML"
# Plain text fallback (para clientes que no soportan HTML)
msg.set_content("Este email se ve mejor en HTML.")
# Versión HTML
html = """
<html>
<body>
<h2 style="color: #3781a9;">Informe semanal</h2>
<p>Hola,</p>
<p>Estos son los <b>datos</b> de esta semana:</p>
<ul>
<li>Ventas: 1.234 €</li>
<li>Visitas: 5.678</li>
</ul>
<p>Un saludo.</p>
</body>
</html>
"""
msg.add_alternative(html, subtype="html")
add_alternative añade la versión HTML como alternativa al texto plano. Los clientes muestran HTML si lo soportan, fallback a texto si no.
Adjuntos
from email.message import EmailMessage
from pathlib import Path
msg = EmailMessage()
msg["From"] = "tu@gmail.com"
msg["To"] = "destinatario@example.com"
msg["Subject"] = "Informe con anexo"
msg.set_content("Adjunto el informe en PDF.")
# Adjuntar un fichero
ruta = Path("informe.pdf")
msg.add_attachment(
ruta.read_bytes(),
maintype="application",
subtype="pdf",
filename=ruta.name,
)
Los maintype/subtype son el MIME type. Algunos comunes:
| Fichero | maintype | subtype |
|---|---|---|
application | pdf | |
| Excel | application | vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| Imagen JPG | image | jpeg |
| Imagen PNG | image | png |
| ZIP | application | zip |
| TXT | text | plain |
💡 ¿Adjuntas Excel o CSV de un script de análisis? Mira Excel con
openpyxly pandas en 15 minutos para generar primero el fichero.
Múltiples destinatarios + CC + BCC
msg["To"] = "a@example.com, b@example.com" # se ven entre sí
msg["Cc"] = "jefe@example.com" # carbon copy, también visible
msg["Bcc"] = "auditoria@example.com" # blind carbon copy, oculto
# send_message envía a To+Cc+Bcc automáticamente
smtp.send_message(msg)
Bcc es ideal para mailings: el destinatario no ve los demás emails. Pero no envíes mailings masivos por SMTP propio — para eso están los servicios profesionales (Mailgun, SendGrid, AWS SES) que gestionan reputación, bounces, unsubscribes, deliverability.
Caso real típico: alerta cuando un script falla
import os
import smtplib
import traceback
from email.message import EmailMessage
def enviar_alerta(asunto: str, mensaje: str) -> None:
msg = EmailMessage()
msg["From"] = os.environ["EMAIL_FROM"]
msg["To"] = os.environ["EMAIL_ALERT_TO"]
msg["Subject"] = f"[ALERTA] {asunto}"
msg.set_content(mensaje)
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login(os.environ["EMAIL_FROM"], os.environ["EMAIL_APP_PASSWORD"])
smtp.send_message(msg)
def main():
try:
procesar_datos()
except Exception:
enviar_alerta(
"El script de procesado falló",
traceback.format_exc(),
)
raise
if __name__ == "__main__":
main()
Patrón muy útil para scripts que corren en cron. Si fallan, te llega un email con el traceback.
💡 ¿Cómo arrancar el script? Patrón estándar:
if __name__ == "__main__"en Python.
Caso real: enviar informe semanal con CSV adjunto
import os
import smtplib
from datetime import date
from email.message import EmailMessage
from pathlib import Path
def enviar_informe_semanal(csv_path: Path, destinatarios: list[str]) -> None:
hoy = date.today().isoformat()
msg = EmailMessage()
msg["From"] = os.environ["EMAIL_FROM"]
msg["To"] = ", ".join(destinatarios)
msg["Subject"] = f"Informe semanal — {hoy}"
msg.set_content(
f"Hola,\n\nAdjunto el informe de la semana ({hoy}).\n\nUn saludo."
)
msg.add_attachment(
csv_path.read_bytes(),
maintype="text",
subtype="csv",
filename=csv_path.name,
)
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login(os.environ["EMAIL_FROM"], os.environ["EMAIL_APP_PASSWORD"])
smtp.send_message(msg)
Con un cron semanal, esto te envía cada lunes el informe a quien pongas. Setup pro en 25 líneas.
💡 ¿Cómo se programa con cron? Mira scripts de Python para automatizar.
¿Y para mailings masivos? Servicios profesionales
smtplib está bien para emails transaccionales puntuales. No para enviar 10.000 emails al día. Razones:
- Reputación de IP: Gmail bloquea cuentas que mandan mucho de golpe.
- Bounces y unsubscribes: gestionar a mano es un infierno.
- Deliverability: SPF, DKIM, DMARC son configuraciones técnicas que tardas en pillar.
- Plantillas, métricas: aperturas, clicks, A/B testing.
Para eso, servicios profesionales:
- SendGrid, Mailgun, Postmark, AWS SES: API REST, te abstraen todo lo de arriba.
- Brevo (antes Sendinblue), Mailchimp: marketing más visual.
# Ejemplo con SendGrid (API)
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
mensaje = Mail(
from_email='tu@dominio.com',
to_emails='destinatario@example.com',
subject='Hola',
html_content='<strong>Hola desde SendGrid</strong>'
)
sg = SendGridAPIClient(os.environ['SENDGRID_API_KEY'])
sg.send(mensaje)
Mucho más sencillo que SMTP a pelo.
Errores típicos al empezar
# 1. Usar la contraseña normal de Gmail
smtp.login("tu@gmail.com", "MiPassNormal")
# ❌ Gmail rechaza con "Username and Password not accepted".
# ✓ Activa 2FA y usa App Password.
# 2. Olvidar starttls en puerto 587
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
smtp.login(...) # ❌ se conecta sin cifrar, falla
# ✓ con starttls():
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
smtp.starttls()
smtp.login(...)
# 3. Hardcodear la contraseña en código
PASSWORD = "abcd-efgh-ijkl-mnop" # ❌ no la subas a git
# ✓ variable de entorno o .env (no commiteado):
import os
PASSWORD = os.environ["GMAIL_APP_PASSWORD"]
# 4. Olvidar el Subject o el From
msg = EmailMessage()
msg.set_content("...")
smtp.send_message(msg) # ❌ sin From/To/Subject probablemente acabe en spam o falle
# 5. Encoding de tildes y emojis
msg.set_content("Hóla 🎉")
# ✓ EmailMessage maneja UTF-8 bien por defecto. Si usas el viejo MIMEText,
# tienes que pasar charset="utf-8" explícito.
# 6. Email a Gmail acaba en spam la primera vez
# Es normal cuando empiezas. Marca como "no spam" desde la cuenta receptora.
# Para producción seria, configura SPF/DKIM/DMARC en tu dominio.
# 7. Leer adjunto incorrectamente
msg.add_attachment(open("archivo.pdf").read(), ...) # ❌ se abre en modo texto
msg.add_attachment(Path("archivo.pdf").read_bytes(), ...) # ✓ binario
Resumen
| Tarea | Sintaxis |
|---|---|
| Crear mensaje | EmailMessage() + From/To/Subject + set_content |
| HTML alternativo | msg.add_alternative(html, subtype="html") |
| Adjuntar | msg.add_attachment(bytes, maintype, subtype, filename) |
| Conectar SSL | smtplib.SMTP_SSL(host, 465) |
| Conectar STARTTLS | smtplib.SMTP(host, 587) + smtp.starttls() |
| Login | smtp.login(usuario, app_password) |
| Enviar | smtp.send_message(msg) |
| Mailing masivo | NO smtplib — usa SendGrid/Mailgun/SES |
| Credenciales | Variables de entorno, NUNCA en código |
¿Te ha valido esto?
Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.
Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.
Tu siguiente paso
Si has llegado hasta aquí, ya tienes lo necesario para que tus scripts manden emails. Aplicación inmediata: alertas cuando algo falla en cron. Aplicación más valiosa: informes automáticos a clientes/equipo con CSV/PDF generado por otro script.
Si quieres aprender Python desde la base hasta proyectos reales con automatización, deploy y notificaciones, en El Pythonista lo enseño paso a paso.
Un abrazo,
Oscar
¿Quieres aprender Python en orden, no a saltos?
Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.
37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía
