Birthday Email Bot
Birthday Email Bot is a Python automation that reads a CSV of names, emails, and birthdays, checks whether today matches any entry, and fires off a personalised email via Gmail SMTP. It comes in two builds: a course-original single-file script and a fully refactored OOP version with separated concerns and centralised config. A GitHub Actions workflow runs it every day at 08:00 UTC so no local machine needs to stay on.
Quick Facts
Overview
Problem
Remembering birthdays is one thing — actually sending a personalised message on the right day is another. Most people rely on calendar reminders that still require manual action: you see the notification, think you'll send something later, and then forget. There is no friction-free way to guarantee a heartfelt birthday email lands in someone's inbox on their actual birthday without opening a client and writing something from scratch. Scaling this across many people makes it worse — each birthday becomes another item to track and act on manually, and the cognitive load quietly compounds.
Solution
I built a Python script that reads a CSV of birthdays, compares each row's month and day against today's date, randomly picks from a pool of letter templates with a personalised name substitution, and sends a plain-text email via Gmail SMTP with STARTTLS. The advanced build separates this into two classes — BirthdayLoader for all CSV and date logic, and EmailSender for template selection and SMTP delivery — wired together by a thin orchestrator in main.py. All constants live in a single config.py so there are no magic numbers scattered through the code. A GitHub Actions workflow runs the advanced build every day using encrypted repository secrets, making the whole thing fully hands-off once deployed.
Challenges
The trickiest part was getting GitHub Actions to pick up credentials without ever touching a .env file in CI. The fix was storing BIRTHDAY_SENDER_EMAIL and BIRTHDAY_APP_PASSWORD as encrypted repository secrets injected as environment variables at runtime, which the existing dotenv pattern picks up transparently. Making the CSV parsing robust against malformed rows was another interesting problem — pandas' to_numeric with errors='coerce' converts bad values to NaN, which are then identified and skipped with a printed warning rather than crashing the run. Getting module imports to resolve correctly both when launched directly and when invoked via menu.py's subprocess.run required sys.path.insert at the top of advanced/main.py combined with an explicit cwd= argument in the subprocess call.
Results / Metrics
This project showed me how much cleaner code gets when you enforce a strict separation between data loading, business logic, and output — swapping the SMTP client or the CSV source would mean touching exactly one class. I got hands-on experience setting up GitHub Actions for scheduled automation with secrets management, which opens up a whole category of set-it-and-forget-it bots. The bot successfully sent a real birthday email to a test address during development, proving the full end-to-end flow works. Next time I would add a log file to keep a record of every send and failure, and explore Jinja2 templates for richer email formatting.
Screenshots
Click to enlarge.
Click to enlarge.