Restore per-leader routing with configurable email addresses

Previously routing was hardcoded (Andrea for indoor, Barbara for outdoor).
Then it was replaced with a flat ADMIN_EMAILS list which lost the routing.
This commit restores routing via three separate env vars:

  ADMIN_EMAIL_INDOOR  — indoor leader, To when indoor days are booked
  ADMIN_EMAIL_OUTDOOR — outdoor leader, To when outdoor days are booked
  ADMIN_EMAIL_CC      — always Cc'd (comma-separated for multiple)

For testing, set all three to your own address so no real leader gets mail.

Changes:
- Config: replaced admin_emails with admin_email_indoor/outdoor/cc fields
- AdminNotifier: replaced admin_emails param with indoor_email/outdoor_email/
  cc_emails; _recipients_for() restored as an instance method using these
- main.py: wires the three new config fields into AdminNotifier
- .env.example: documents the three new variables with production defaults
- Tests: fixture updated to use new params

https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
This commit is contained in:
Claude
2026-02-21 21:31:49 +00:00
parent 7fb1d1fa0f
commit db97a357c9
5 changed files with 59 additions and 28 deletions
+7 -6
View File
@@ -44,13 +44,14 @@ SMTP_USE_TLS=true
REGISTRATION_EMAIL=anmeldung@example.com
# ---------------------------------------------------------------
# Admin notification recipients (comma-separated)
# First address → To; remaining addresses → Cc.
# For testing, set this to just your own email address.
# Production example (indoor leader, outdoor leader, admin):
# ADMIN_EMAILS=andrea.sigrist@gmx.net,baba.laeubli@gmail.com,spielgruppen@familien-verein.ch
# Admin notification routing
# Each leader receives mail only when a day in their group is booked.
# ADMIN_EMAIL_CC is always included as Cc (comma-separated for multiple).
# For testing, point all three to your own email address.
# ---------------------------------------------------------------
ADMIN_EMAILS=you@example.com
ADMIN_EMAIL_INDOOR=andrea.sigrist@gmx.net
ADMIN_EMAIL_OUTDOOR=baba.laeubli@gmail.com
ADMIN_EMAIL_CC=spielgruppen@familien-verein.ch
# ---------------------------------------------------------------
# Storage
+3 -1
View File
@@ -58,7 +58,9 @@ def build_components(config: Config):
password=config.imap_password,
use_tls=config.smtp_use_tls,
from_email=config.registration_email,
admin_emails=config.admin_emails,
indoor_email=config.admin_email_indoor,
outdoor_email=config.admin_email_outdoor,
cc_emails=[e.strip() for e in config.admin_email_cc.split(",") if e.strip()],
)
agent = EmailAgent(model=config.ai_model, kb=kb, store=store, notifier=notifier)
+9 -9
View File
@@ -32,10 +32,12 @@ class Config:
# Registration email address shown to parents
registration_email: str = ""
# Admin notification recipients (comma-separated).
# First address → To; remaining addresses → Cc.
# Set to a single address (e.g. your own) during testing.
admin_emails: list = field(default_factory=list)
# Admin notification routing.
# Each leader receives mail only when a day in their group is booked.
# For testing, point all three to your own address.
admin_email_indoor: str = "" # Indoor leader (Andrea Sigrist) — To when indoor booked
admin_email_outdoor: str = "" # Outdoor leader (Barbara Gross) — To when outdoor booked
admin_email_cc: str = "" # Always Cc'd (Markus Graf / admin); comma-separated if multiple
# Storage
data_dir: Path = field(default_factory=lambda: Path("data"))
@@ -61,11 +63,9 @@ class Config:
smtp_port=int(os.getenv("SMTP_PORT", "587")),
smtp_use_tls=os.getenv("SMTP_USE_TLS", "true").lower() == "true",
registration_email=os.getenv("REGISTRATION_EMAIL", ""),
admin_emails=[
e.strip()
for e in os.getenv("ADMIN_EMAILS", "").split(",")
if e.strip()
],
admin_email_indoor=os.getenv("ADMIN_EMAIL_INDOOR", ""),
admin_email_outdoor=os.getenv("ADMIN_EMAIL_OUTDOOR", ""),
admin_email_cc=os.getenv("ADMIN_EMAIL_CC", ""),
data_dir=Path(os.getenv("DATA_DIR", "data")),
knowledge_base_dir=Path(
os.getenv(
+37 -11
View File
@@ -29,7 +29,9 @@ class AdminNotifier:
password: str,
use_tls: bool = True,
from_email: str = "",
admin_emails: list[str] | None = None,
indoor_email: str = "",
outdoor_email: str = "",
cc_emails: list[str] | None = None,
) -> None:
self._smtp_host = smtp_host
self._smtp_port = smtp_port
@@ -37,7 +39,9 @@ class AdminNotifier:
self._password = password
self._use_tls = use_tls
self._from_email = from_email or username
self._admin_emails: list[str] = admin_emails or []
self._indoor_email = indoor_email
self._outdoor_email = outdoor_email
self._cc_emails: list[str] = cc_emails or []
# ------------------------------------------------------------------
# Public API
@@ -52,11 +56,15 @@ class AdminNotifier:
channel: str,
) -> None:
"""Send notification for a newly completed registration (version 1)."""
if not self._admin_emails:
logger.warning("ADMIN_EMAILS not configured — new-registration notification skipped.")
types = registration.booking.playgroup_types
to_addresses = self._recipients_for(types)
if not to_addresses:
logger.warning(
"No leader email configured for types %s — new-registration notification skipped.",
types,
)
return
types = registration.booking.playgroup_types
subject = (
f"Neue Anmeldung: {registration.child.full_name} "
f" {self._format_types(types)}"
@@ -64,8 +72,8 @@ class AdminNotifier:
body = self._build_new_body(registration, registration_id, version, channel)
self._send(
to=[self._admin_emails[0]],
cc=self._admin_emails[1:],
to=to_addresses,
cc=self._cc_emails,
subject=subject,
body=body,
reply_to=registration.parent_guardian.email or "",
@@ -80,21 +88,39 @@ class AdminNotifier:
conversation_id: str,
) -> None:
"""Send notification when an existing registration is updated."""
if not self._admin_emails:
logger.warning("ADMIN_EMAILS not configured — update notification skipped.")
types = registration.booking.playgroup_types
to_addresses = self._recipients_for(types)
if not to_addresses:
logger.warning(
"No leader email configured for types %s — update notification skipped.",
types,
)
return
subject = f"Anmeldung aktualisiert: {registration.child.full_name}"
body = self._build_update_body(registration, registration_id, version, change_summary)
self._send(
to=[self._admin_emails[0]],
cc=self._admin_emails[1:],
to=to_addresses,
cc=self._cc_emails,
subject=subject,
body=body,
reply_to=registration.parent_guardian.email or "",
)
# ------------------------------------------------------------------
# Routing helpers
# ------------------------------------------------------------------
def _recipients_for(self, types: list[str]) -> list[str]:
"""Return To addresses based on which playgroup types are booked."""
recipients = []
if "indoor" in types and self._indoor_email:
recipients.append(self._indoor_email)
if "outdoor" in types and self._outdoor_email:
recipients.append(self._outdoor_email)
return recipients
# ------------------------------------------------------------------
# Formatting helpers
# ------------------------------------------------------------------
+3 -1
View File
@@ -15,7 +15,9 @@ def notifier():
password="secret",
use_tls=True,
from_email="agent@example.com",
admin_emails=["to@example.com", "cc1@example.com", "cc2@example.com"],
indoor_email="andrea@example.com",
outdoor_email="barbara@example.com",
cc_emails=["markus@example.com"],
)