Send at most one grouped birthday reminder email

Collects birthdays in the next 7 days, groups them into sections
(Today/Tomorrow/In N days), and sends a single email.

Fixes #201.
This commit is contained in:
Edward Betts 2025-08-12 22:33:52 +01:00
parent ebceb4cb51
commit 97c0531a22

View file

@ -21,6 +21,7 @@ import agenda.mail
import agenda.thespacedevs
import agenda.types
import agenda.uk_holiday
from agenda.event import Event
from agenda.types import StrDict
from web_view import app
@ -295,7 +296,11 @@ def update_gandi(config: flask.config.Config) -> None:
def check_birthday_reminders(config: flask.config.Config) -> None:
"""Check for upcoming birthdays and send email reminders."""
"""Send at most one grouped birthday reminder email per day.
Collects birthdays in the next 7 days, groups them into sections
(Today/Tomorrow/In N days), and sends a single email.
"""
today = date.today()
data_dir = config["PERSONAL_DATA"]
entities_file = os.path.join(data_dir, "entities.yaml")
@ -303,55 +308,65 @@ def check_birthday_reminders(config: flask.config.Config) -> None:
if not os.path.exists(entities_file):
return
# Get upcoming birthdays (next 7 days)
birthdays = agenda.birthday.get_birthdays(today, entities_file)
upcoming_birthdays = []
for birthday in birthdays:
days_until = (birthday.date - today).days
# Collect next 7 days into a dict keyed by days-until
by_days: dict[int, list[Event]] = {}
for ev in birthdays:
days_until = (ev.as_date - today).days
if 0 <= days_until <= 7:
upcoming_birthdays.append((birthday, days_until))
by_days.setdefault(days_until, []).append(ev)
if not upcoming_birthdays:
if not by_days:
return
# Group birthdays by days until
birthday_groups = {}
for birthday, days_until in upcoming_birthdays:
if days_until not in birthday_groups:
birthday_groups[days_until] = []
birthday_groups[days_until].append(birthday)
# Build subject
headings: list[str] = []
if 0 in by_days:
headings.append("today")
if 1 in by_days:
headings.append("tomorrow")
others = sum(1 for k in by_days.keys() if k not in (0, 1))
if others:
plural = "s" if others != 1 else ""
headings.append(f"{others} other{plural}")
subject = (
f"🎂 Birthday reminders ({', '.join(headings)})"
if headings
else "🎂 Birthday reminders"
)
# Send reminder emails
for days_until, birthdays in birthday_groups.items():
if days_until == 0:
subject = "🎂 Birthday Today!"
elif days_until == 1:
subject = "🎂 Birthday Tomorrow!"
# Build body (UK style dates)
lines: list[str] = ["Upcoming birthdays (next 7 days):", ""]
for delta in sorted(by_days.keys()):
if delta == 0:
lines.append("Today")
elif delta == 1:
lines.append("Tomorrow")
else:
subject = f"🎂 Birthday in {days_until} days"
lines.append(f"In {delta} days")
body_lines = ["Birthday Reminder:\n"]
for birthday in birthdays:
date_str = birthday.date.strftime("%A, %B %d, %Y")
if days_until == 0:
body_lines.append(f"Today: {birthday.title} - {date_str}")
elif days_until == 1:
body_lines.append(f"Tomorrow: {birthday.title} - {date_str}")
else:
body_lines.append(f"{date_str}: {birthday.title}")
body_lines.append(
f"\nView all birthdays: https://edwardbetts.com/agenda/birthdays"
entries = sorted(
by_days[delta],
key=lambda e: (e.as_date, (e.title or e.name or "")),
)
body = "\n".join(body_lines)
for ev in entries:
d = ev.as_date
# Portable UK-style date: weekday, D Month YYYY
date_str = f"{d:%A}, {d.day} {d:%B %Y}"
label = ev.title or ev.name
lines.append(f"{label}{date_str}")
if sys.stdin.isatty():
print(f"Birthday reminder: {subject}")
print(body)
lines.append("")
agenda.mail.send_mail(config, subject, body)
lines.append("View all birthdays: https://edwardbetts.com/agenda/birthdays")
body = "\n".join(lines)
if sys.stdin.isatty():
print(f"Birthday reminder: {subject}\n{body}")
agenda.mail.send_mail(config, subject, body)
def main() -> None: