100 Días de Código en Python — Día 32: Enviar correos (smtplib) y manejar fechas (datetime)
Nivel: Grado décimo
Área: Tecnología e Informática
Modalidad: Tutorial práctico
Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 32 (nivel intermedio+).
28 min de lectura
14 secciones
Este tutorial adapta pedagógicamente el Día 32. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso. Los retos 🟢🟡🟠🔴, el proyecto extra y el proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.
Nota sobre las verificaciones: los ejemplos se ejecutaron con Python 3.14 y pandas 3.0. El envío de correos se probó con un servidor de correo local (aiosmtpd 1.4.6, ver sección 1), no con cuentas reales. Los archivos iniciales del curso (quotes.txt, birthdays.csv y las plantillas de cartas) no están en la carpeta; el tutorial explica cómo crearlos.
🔐 Seguridad y ética (adaptación para el aula): en este día se trabaja con contraseñas y datos personales (nombres, correos, fechas de nacimiento). Reglas para la clase:
Practica con el servidor local de la sección 1. Si el docente autoriza el envío real, usa solo una cuenta de prueba creada para eso, nunca tu correo personal ni el de tu familia.
Nunca subas una contraseña a un repositorio, un documento compartido o un chat.
Usa datos inventados. En Colombia, los datos personales están protegidos por la Ley 1581 de 2012 (habeas data): para guardar y usar los datos de otra persona se necesita su autorización.
(Fuente: 243 - Day 32 Goals- what we will make by the end of the day.md)
Hoy aprenderemos dos módulos que ya vienen con Python (no hay que instalarlos):
smtplib: enviar correos electrónicos desde un programa.
datetime: saber la fecha y hora actuales, y crear y comparar fechas.
Con ellos construiremos un felicitador automático de cumpleaños: una hoja de cálculo con los cumpleaños de amigos y familiares, varias cartas de felicitación y un programa que, cada día, revisa si alguien cumple años y le envía una carta elegida al azar.
La profesora del curso cuenta que depende de Facebook para recordar cumpleaños. El programa resuelve ese problema… aunque también plantea una pregunta ética que ella misma deja abierta: ¿es sincero un saludo automático? Discútanlo en clase (sección 4).
Prerequisito general: archivos y with open (Día 24); CSV, pandas e iterrows (Días 25 y 26); comprensión de diccionarios (Día 26); tuplas como claves de diccionario (Día 26); random (Día 4); replace de cadenas; excepciones (Día 30).
Cuando angela@gmail.com escribe a timmy@yahoo.com:
Texto
12
Programa de Angela ──► Servidor de Gmail ──► Servidor de Yahoo ──► Computador de Timmy
SMTP SMTP (Timmy descarga el correo)
SMTP (Simple Mail Transfer Protocol, protocolo simple de transferencia de correo) es el conjunto de reglas con las que los servidores reciben y reenvían correos. La analogía del curso: los servidores son oficinas de correos, el computador de Timmy es su buzón y SMTP es el cartero.
La solución del curso (con with, que cierra la conexión solo):
Python
12345678910111213
import smtplib
my_email ="cuenta_de_prueba@gmail.com"
password ="contraseña_de_aplicación"with smtplib.SMTP("smtp.gmail.com", port=587)as connection:
connection.starttls()
connection.login(user=my_email, password=password)
connection.sendmail(
from_addr=my_email,
to_addrs="destinatario_de_prueba@yahoo.com",
msg="Subject:Hello\n\nThis is the body of my email.")
El asunto y el cuerpo: un correo sin asunto tiene muchas probabilidades de ir a spam. En sendmail, el mensaje empieza con Subject: y el asunto; después van dos saltos de línea (\n\n) y luego el cuerpo.
📖 La configuración según el proveedor (lección 244)#
Dirección SMTP: cada proveedor tiene la suya. Gmail: smtp.gmail.com; Yahoo: smtp.mail.yahoo.com. Para otros, busca "[proveedor] SMTP settings" en la ayuda oficial del proveedor.
Contraseña de aplicación (Gmail): activar la verificación en 2 pasos en la cuenta de Google, buscar App Passwords, crear una con un nombre (por ejemplo, "Python Mail") y copiarla: solo se muestra una vez. Son 16 caracteres. En el código se usa esa contraseña, no la normal. Yahoo tiene un proceso parecido en su página de seguridad.
Puerto: por defecto smtplib.SMTP usa el puerto 25, que hoy casi todos los servidores bloquean por abuso. Para conexiones desde un programa se usa normalmente el 587: smtplib.SMTP("smtp.gmail.com", port=587).
🛠️ Nota técnica (información desactualizada): la lección 244 indica smtp.live.com para Hotmail y outlook.office365.com para Outlook. Esa segunda dirección corresponde a la lectura del correo (IMAP/POP), no al envío, y Microsoft ha cambiado varias veces la forma de autenticar cuentas personales, por lo que un programa sencillo con usuario y contraseña puede no funcionar. Además, el video 245 habla de hacer las cuentas "menos seguras"; Google ya no ofrece esa opción, y el camino actual es la contraseña de aplicación. Verifica siempre en la ayuda oficial del proveedor.
🏫 Alternativa para el aula: un servidor de correo local (adaptación)#
Para aprender cómo funciona smtplibno hace falta enviar correos reales. Existe una biblioteca, aiosmtpd, que crea en tu computador un servidor de correo de práctica: recibe los mensajes y los muestra en la consola, sin enviarlos a nadie.
Instálala: pip install aiosmtpd.
En una terminal, inicia el servidor y déjalo abierto:
Texto
1
python -m aiosmtpd -n -l localhost:8025
En tu programa, conéctate a él. Este servidor no usa cifrado ni contraseñas, así que se omitenstarttls() y login():
Python
12345678
import smtplib
with smtplib.SMTP("localhost", port=8025)as connection:
connection.sendmail(
from_addr="yo@colegio.test",
to_addrs="tu@colegio.test",
msg="Subject:Prueba\n\nHola desde Python")
Mira la terminal del servidor: aparece el correo completo entre MESSAGE FOLLOWS y END MESSAGE (verificado).
Cuando tu programa funcione con el servidor local, pasar a una cuenta real solo requiere cambiar la dirección, el puerto y volver a poner starttls() y login().
Dirección SMTP equivocada, puerto equivocado o cuenta mal configurada
Revisa los tres puntos de la lección 244
Un error de escritura en el correo, la contraseña o la dirección SMTP
Fallos difíciles de ver
Revisa cada cadena letra por letra, como insiste el curso
El correo no llega
Probablemente está en spam
Revisa spam; agrega un asunto
UnicodeEncodeError: 'ascii' codec can't encode character '\xf1'
El mensaje tiene ñ, tildes o signos como ¡ (verificado)
Ver la nota técnica siguiente
SMTPNotSupportedError: STARTTLS extension not supported
Llamaste starttls() en el servidor local (verificado)
Con el servidor local, omite starttls() y login()
🛠️ Nota técnica (importante en español):sendmail convierte el mensaje a ASCII, que no incluye ñ, tildes ni ¡¿. Por eso msg="Subject:Feliz cumpleaños\n\n¡Hola, Mamá!" produce UnicodeEncodeError (verificado). El curso no lo menciona porque sus mensajes están en inglés. La solución moderna es construir el mensaje con EmailMessage y enviarlo con send_message (verificado con el servidor local):
Python
12345678910
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"]="Feliz cumpleaños"
msg["From"]="yo@colegio.test"
msg["To"]="tu@colegio.test"
msg.set_content("¡Hola, Mamá! Que tengas un día increíble.")with smtplib.SMTP("localhost", port=8025)as connection:
connection.send_message(msg)
Además de aceptar cualquier carácter, así no hay que armar a mano el Subject: con \n\n.
🟢 Reto 1: con el servidor local, envía un correo con asunto y un cuerpo de tres líneas. Copia lo que muestra el servidor e identifica el asunto, el remitente y el cuerpo.
🟡 Reto 2: envía el mismo correo a tres destinatarios en una sola llamada. (Pista: to_addrs acepta una lista.)
🟠 Reto 3: reescribe el Reto 1 con EmailMessage y un asunto con tildes. Compara, en la consola del servidor, cómo se ve el asunto (=?utf-8?q?...). ¿Por qué crees que se codifica así?
🔴 Reto 4: escribe una función enviar_correo(destino, asunto, cuerpo, servidor, puerto) que capture SMTPAuthenticationError, SMTPServerDisconnected y ConnectionRefusedError (servidor local apagado) y muestre un mensaje claro para cada uno.
El módulo datetime tiene una clase que también se llama datetime, lo que confunde. El curso usa un alias:
Python
12345
import datetime as dt
now = dt.datetime.now()print(now)# por ejemplo: 2026-09-26 10:15:42.123456print(type(now))# <class 'datetime.datetime'>
now es un objeto, no un texto. Por eso podemos pedirle sus partes como atributos, que son números enteros:
Python
123456
year = now.year # int
month = now.month
day = now.day
hour = now.hour
if year ==2026:print("Estamos en 2026")
Día de la semana:now.weekday() es un método (lleva paréntesis) que devuelve un número. Como los computadores cuentan desde cero: 0 = lunes, 1 = martes, …, 6 = domingo (verificado: el 28 de septiembre de 2026 es lunes y weekday() devuelve 0).
Crear una fecha propia: año, mes y día son obligatorios; hora, minuto y segundo tienen valores por defecto (cero):
Alternativa de importación (la usa el curso en el proyecto): from datetime import datetime y luego datetime.now(). Las dos formas son válidas; elige una y úsala de forma consistente.
🟢 Reto 1: imprime la fecha de hoy con el formato 26/09/2026 usando los atributos.
🟡 Reto 2: imprime el nombre del día de la semana en español usando weekday() y una lista.
🟠 Reto 3: pide tu fecha de nacimiento y calcula cuántos años cumples este año. ¿Ya los cumpliste?
🔴 Reto 4: investiga timedelta en la documentación oficial (se abre en otra pestaña) y calcula cuántos días faltan para tu próximo cumpleaños. Prueba con alguien que haya nacido un 29 de febrero.
Enunciado: si hoy es lunes, enviarte un correo con una cita motivacional al azar tomada de quotes.txt (una cita por línea, con su autor).
Truco del curso para probar: para no esperar al lunes, compara con el día de hoy mientras pruebas (por ejemplo, weekday == 1 si es martes). Cuando todo funcione, cámbialo a 0.
Si no tienes quotes.txt: créalo con al menos 5 frases, una por línea, con el formato "Frase" - Autor. Pueden ser frases de personajes colombianos o frases escritas por el curso.
Olvidar volver a poner 0 después de las pruebas: recibirás citas los martes.
Las citas copiadas de internet suelen tener comillas tipográficas (“ ”) o rayas (—). Con sendmail, eso produce UnicodeEncodeError (nota técnica de la sección 1). Usa EmailMessage o comillas rectas.
open("quotes.txt") sin encoding="utf-8" puede leer mal las tildes en Windows. Agrégalo si tus citas están en español.
🟢 Reto 1: prueba el programa con el servidor local y muestra 5 ejecuciones seguidas. ¿Se repite alguna cita?
🟡 Reto 2: envía una cita diferente según el día: lunes motivación, miércoles humor, viernes reflexión (tres archivos).
🟠 Reto 3: guarda en un archivo las citas ya enviadas y evita repetirlas hasta haberlas usado todas.
🔴 Reto 4: separa la lógica (¿toca enviar hoy?, ¿qué cita?) del envío en funciones distintas, de modo que puedas probar la lógica sin conexión, pasándole una fecha cualquiera: debe_enviar(fecha).
06
4. 🚀 Proyecto del día: el Felicitador Automático de Cumpleaños#
(Fuentes: 248 - Automated Birthday Wisher Project Challenge.md, 249 - Solution & Walkthrough for the Automated Birthday Wisher.md)
Enviar automáticamente un correo de "¡Feliz cumpleaños!" a las personas de una lista, el día de su cumpleaños, con una carta elegida al azar y personalizada con su nombre.
La profesora cuenta que su mamá espera la felicitación a las 8:00 a. m. en punto. El programa revisa un CSV de cumpleaños, elige una de tres cartas (para que no llegue la misma todos los años), cambia el marcador del nombre y envía el correo.
Cartas con un marcador de posición del nombre (por ejemplo, [NAME])
Tres niveles de dificultad (lección 248): el curso ofrece tres proyectos iniciales. Normal, con muchas pistas; difícil, con pocas pistas; y muy difícil, que solo dice qué debe hacer el programa. Elige el tuyo y anótalo.
Mensaje del curso sobre la solución (249): "No existe una verdadera respuesta correcta". Si tu programa funciona con otro método, está bien.
Si la fecha de hoy (mes y día) coincide con un cumpleaños, elegir una carta al azar.
Reemplazar el marcador por el nombre de la persona.
Enviar la carta al correo de esa persona con el asunto "Happy Birthday!".
Construcción paso a paso (solución del curso, nivel normal)#
Paso 1 — Datos de prueba. Reemplaza los datos de birthdays.csv por datos inventados y pon en una fila el mes y día de hoy, con un correo al que tengas acceso (o del servidor local). El año no importa.
Paso 2 — ¿Hoy cumple alguien? Es el paso más difícil.
Dos cumpleaños el mismo día. Pon a dos personas con la misma fecha. El diccionario solo guarda una por clave: la última reemplaza a la primera (verificado: con Ana y Luis el 14 de julio, solo queda Luis). ¡Ana no recibe felicitación! Corrige el programa para que funcione con varias personas. (Pista: recorre las filas con iterrows y compara cada una, o guarda como valor una lista de filas.)
Tildes y ñ. Un nombre como "Mamá" en el mensaje produce UnicodeEncodeError con sendmail (verificado). Cámbialo a EmailMessage y send_message.
29 de febrero. En un año no bisiesto, (2, 29) nunca coincide con hoy. Decide si esa persona debe recibir el correo el 28 de febrero o el 1 de marzo, e impleméntalo.
Doble envío. Si el programa se ejecuta dos veces el mismo día, la persona recibe dos correos. ¿Cómo lo evitarías? (Pista: guardar la fecha del último envío.)
La profesora dice: "La ética de todo este proyecto realmente depende de ti". Ella misma no lo usa con su mamá. Escribe un párrafo: ¿cuándo un mensaje automático es útil y cuándo es falso? ¿Le avisarías a la persona que el saludo es automático?
El felicitador solo funciona si alguien lo ejecuta cada día. Abrir el editor y pulsar Ejecutar a diario es más trabajo que felicitar a mano. La solución es un servicio que ejecute el programa en la nube a una hora fija.
📝 Contenido original del curso: GitHub Actions (lección 250)#
El curso propone GitHub Actions, gratuito para este uso:
Crear una cuenta en GitHub.
Hacer un fork (copia propia) del repositorio plantilla appbrewery/scheduled-tasks.
Habilitar los workflows en la pestaña Actions. El curso advierte: revisa siempre los archivos de un workflow antes de habilitarlo; nunca ejecutes código de internet sin mirarlo.
Copiar el código en main.py y los archivos que necesite (.csv, .txt).
Reemplazar las contraseñas escritas en el código por variables de entorno:
Python
123456789
import os
# ANTES (¡no lo hagas!: cualquiera que vea el código ve la contraseña)
MY_EMAIL ="myemail@gmail.com"
MY_PASSWORD ="mysecretpassword"# DESPUÉS (los secretos se guardan en GitHub)
MY_EMAIL = os.environ.get("MY_EMAIL")
MY_PASSWORD = os.environ.get("MY_PASSWORD")
Listar en requirements.txt las bibliotecas que no vienen con Python (por ejemplo, pandas).
Guardar los secretos en Settings → Secrets and variables → Actions → New repository secret.
Programar la hora en .github/workflows/scheduled.yml con una expresión cron.
Probar con el workflow "Test Setup" y luego ejecutar "Daily Script" manualmente.
cron: '0 9 * * *'
│ │
│ └─ hora (0-23) en UTC
└─── minuto (0-59)
Los servidores usan la hora UTC (tiempo universal). La tabla del curso no incluye Colombia, así que la calculamos (adaptación): Colombia está en UTC−5 todo el año (no hay horario de verano). Para saber la hora UTC, suma 5 a la hora de Colombia.
Quieres que se ejecute en Colombia a las…
Hora UTC
Expresión cron
6:00 a. m.
11
'0 11 * * *'
8:00 a. m.
13
'0 13 * * *'
9:00 a. m.
14
'0 14 * * *'
🛠️ Nota técnica (análisis de esta adaptación): en el servidor de GitHub, datetime.now() también devuelve la hora UTC. Si programas la ejecución a las 3:00 UTC, en Colombia son las 10:00 p. m. del día anterior… pero el programa creerá que ya es el día siguiente y felicitará a la persona un día antes. Con horas entre las 5:00 y las 23:59 UTC (de las 00:00 a las 18:59 en Colombia) la fecha coincide.
Problemas frecuentes que señala el curso:
La ejecución puede retrasarse de 5 a 15 minutos: es normal.
Si el repositorio no tiene actividad durante 60 días, GitHub pausa la programación. Un cambio pequeño la reactiva.
No module named 'pandas': falta en requirements.txt.
Los nombres de los secretos distinguen mayúsculas: MY_EMAIL no es my_email.
Gmail exige la contraseña de aplicación y, normalmente, el puerto 587.
Consejo del curso: provoca a propósito un error para ver cómo se muestra, porque no hay consola.
El curso mostraba antes PythonAnywhere: subir los archivos, probar con python3 main.py en una consola y crear una tarea programada (también en UTC). La lección misma aclara que la programación de tareas ya no es gratuita. El video incluye además un paso para "desbloquear" el acceso de Gmail que ya no existe; hoy se usa la contraseña de aplicación.
🏫 Para el aula: crear cuentas en GitHub tiene una edad mínima según sus condiciones de uso, y estos pasos implican guardar contraseñas reales. Recomendación: el docente hace la demostración con una cuenta institucional de prueba, y los estudiantes practican cron y variables de entorno en su computador.
🟢 Reto 1: escribe las expresiones cron para: todos los días a las 7:30 a. m. de Colombia, y todos los lunes a las 6:00 a. m. de Colombia (investiga el quinto campo).
🟡 Reto 2: en tu computador, crea una variable de entorno de prueba y léela con os.environ.get. Haz que el programa muestre un mensaje claro si no existe.
🟠 Reto 3: haz que el felicitador calcule la fecha en la hora de Colombia aunque el servidor esté en UTC. (Investiga zoneinfo y ZoneInfo("America/Bogota").)
🔴 Reto 4: en tu sistema operativo existe una forma de programar tareas sin la nube: el Programador de tareas de Windows o cron en Linux y Mac. Investiga cuál tiene tu equipo, compara sus ventajas y desventajas con GitHub Actions y preséntalas en una tabla.
08
6. 🌟 Proyecto extra: el Boletín Diario de Tareas del Curso#
En el grupo de décimo, cada semana se olvidan entregas. El representante del curso propone un programa que, cada mañana, envíe un boletín con las tareas que se entregan hoy y mañana.
7. 🏆 Proyecto integrador de los Días 24 a 32: el Asistente del Director de Grupo#
Proyecto integrador creado para esta adaptación. Reúne archivos y rutas (Día 24), CSV y pandas (Día 25), comprensión de listas y diccionarios (Día 26), excepciones y JSON (Día 30), y correo y fechas (Día 32).
El director de grupo de un curso de décimo dedica mucho tiempo a tareas repetitivas: recordar cumpleaños, enviar recordatorios de entregas y preparar un resumen semanal de asistencia para coordinación.
Construir un programa de consola, Asistente del Director de Grupo, que cada día decide qué correos enviar y los envía (al servidor local durante el desarrollo).
Solo datos inventados. Incluye en el documento un párrafo sobre la Ley 1581 de 2012: ¿qué autorizaciones necesitaría el colegio para usar datos reales?
Escribe dias_para_cumpleanos(fecha_nacimiento, hoy) que devuelva cuántos días faltan para el próximo cumpleaños. Pruébala con al menos 4 fechas, incluido un cumpleaños que ya pasó este año.
Actividad 1: verificar información técnica que cambia con el tiempo. Este día tiene mucha información que se desactualiza: direcciones SMTP, opciones de seguridad de Gmail, servicios gratuitos. Pregunta a una IA: "¿Cuál es el servidor SMTP de Outlook.com y cómo me autentico desde Python?".
Compara su respuesta con la lección 244 del curso.
Compárala con la página de ayuda oficial de Microsoft.
¿Coinciden las tres fuentes? ¿Cuál es la más reciente? ¿La IA dijo en algún momento que su información podía estar desactualizada?
Actividad 2: nunca compartas secretos. Si le pides ayuda a una IA con tu código, borra primero correos reales y contraseñas. Revisa el código que pegues y reemplázalos por "cuenta_de_prueba@..." y "CONTRASEÑA". Explica por qué es importante, aunque la IA "no se lo diga a nadie".
Actividad 3: ¿la IA prevé los casos difíciles? Pide a una IA que escriba el felicitador de cumpleaños. Comprueba si su solución funciona con: dos cumpleaños el mismo día, un nombre con tilde y el 29 de febrero. Registra qué casos falló y corrígelos tú.
12
10. 🛠️ Correcciones técnicas al material original#
Captura de la consola del servidor local mostrando un correo recibido con tildes.
El programa de citas de los lunes con tu propio quotes.txt.
El felicitador funcionando con datos inventados, con las cuatro correcciones de la sección 🧪.
Tu tabla de expresiones cron para la hora de Colombia.
La reflexión ética sobre los saludos automáticos y el párrafo sobre la Ley 1581 de 2012.
El Boletín Diario de Tareas (proyecto extra) o el Asistente del Director de Grupo (proyecto integrador).
Cuaderno de errores:SMTPAuthenticationError, SMTPServerDisconnected, UnicodeEncodeError con ñ, starttls en el servidor local, weekday sin paréntesis, replace sin asignar, cumpleaños duplicados, 29 de febrero, variable de entorno en None y confusión de UTC.