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:
		
							parent
							
								
									ebceb4cb51
								
							
						
					
					
						commit
						97c0531a22
					
				
							
								
								
									
										91
									
								
								update.py
									
									
									
									
									
								
							
							
						
						
									
										91
									
								
								update.py
									
									
									
									
									
								
							| 
						 | 
					@ -21,6 +21,7 @@ import agenda.mail
 | 
				
			||||||
import agenda.thespacedevs
 | 
					import agenda.thespacedevs
 | 
				
			||||||
import agenda.types
 | 
					import agenda.types
 | 
				
			||||||
import agenda.uk_holiday
 | 
					import agenda.uk_holiday
 | 
				
			||||||
 | 
					from agenda.event import Event
 | 
				
			||||||
from agenda.types import StrDict
 | 
					from agenda.types import StrDict
 | 
				
			||||||
from web_view import app
 | 
					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:
 | 
					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()
 | 
					    today = date.today()
 | 
				
			||||||
    data_dir = config["PERSONAL_DATA"]
 | 
					    data_dir = config["PERSONAL_DATA"]
 | 
				
			||||||
    entities_file = os.path.join(data_dir, "entities.yaml")
 | 
					    entities_file = os.path.join(data_dir, "entities.yaml")
 | 
				
			||||||
| 
						 | 
					@ -303,53 +308,63 @@ def check_birthday_reminders(config: flask.config.Config) -> None:
 | 
				
			||||||
    if not os.path.exists(entities_file):
 | 
					    if not os.path.exists(entities_file):
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Get upcoming birthdays (next 7 days)
 | 
					 | 
				
			||||||
    birthdays = agenda.birthday.get_birthdays(today, entities_file)
 | 
					    birthdays = agenda.birthday.get_birthdays(today, entities_file)
 | 
				
			||||||
    upcoming_birthdays = []
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for birthday in birthdays:
 | 
					    # Collect next 7 days into a dict keyed by days-until
 | 
				
			||||||
        days_until = (birthday.date - today).days
 | 
					    by_days: dict[int, list[Event]] = {}
 | 
				
			||||||
 | 
					    for ev in birthdays:
 | 
				
			||||||
 | 
					        days_until = (ev.as_date - today).days
 | 
				
			||||||
        if 0 <= days_until <= 7:
 | 
					        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
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Group birthdays by days until
 | 
					    # Build subject
 | 
				
			||||||
    birthday_groups = {}
 | 
					    headings: list[str] = []
 | 
				
			||||||
    for birthday, days_until in upcoming_birthdays:
 | 
					    if 0 in by_days:
 | 
				
			||||||
        if days_until not in birthday_groups:
 | 
					        headings.append("today")
 | 
				
			||||||
            birthday_groups[days_until] = []
 | 
					    if 1 in by_days:
 | 
				
			||||||
        birthday_groups[days_until].append(birthday)
 | 
					        headings.append("tomorrow")
 | 
				
			||||||
 | 
					    others = sum(1 for k in by_days.keys() if k not in (0, 1))
 | 
				
			||||||
    # Send reminder emails
 | 
					    if others:
 | 
				
			||||||
    for days_until, birthdays in birthday_groups.items():
 | 
					        plural = "s" if others != 1 else ""
 | 
				
			||||||
        if days_until == 0:
 | 
					        headings.append(f"{others} other{plural}")
 | 
				
			||||||
            subject = "🎂 Birthday Today!"
 | 
					    subject = (
 | 
				
			||||||
        elif days_until == 1:
 | 
					        f"🎂 Birthday reminders ({', '.join(headings)})"
 | 
				
			||||||
            subject = "🎂 Birthday Tomorrow!"
 | 
					        if headings
 | 
				
			||||||
        else:
 | 
					        else "🎂 Birthday reminders"
 | 
				
			||||||
            subject = f"🎂 Birthday in {days_until} 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"
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        body = "\n".join(body_lines)
 | 
					    # 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:
 | 
				
			||||||
 | 
					            lines.append(f"In {delta} days")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        entries = sorted(
 | 
				
			||||||
 | 
					            by_days[delta],
 | 
				
			||||||
 | 
					            key=lambda e: (e.as_date, (e.title or e.name or "")),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        lines.append("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lines.append("View all birthdays: https://edwardbetts.com/agenda/birthdays")
 | 
				
			||||||
 | 
					    body = "\n".join(lines)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if sys.stdin.isatty():
 | 
					    if sys.stdin.isatty():
 | 
				
			||||||
            print(f"Birthday reminder: {subject}")
 | 
					        print(f"Birthday reminder: {subject}\n{body}")
 | 
				
			||||||
            print(body)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    agenda.mail.send_mail(config, subject, body)
 | 
					    agenda.mail.send_mail(config, subject, body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue