From 7fb1d1fa0f2d2ece68602beaf051758fcac24a97 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 21:23:51 +0000 Subject: [PATCH] Make admin notification recipients configurable via ADMIN_EMAILS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the To/Cc addresses were hardcoded in notifier.py (Andrea, Barbara, Markus). This caused accidental emails to production contacts during testing. Changes: - New ADMIN_EMAILS env var: comma-separated list of addresses. First address → To; remaining addresses → Cc. - AdminNotifier now accepts admin_emails list; warns and skips if empty. - Removed hardcoded _INDOOR_EMAIL / _OUTDOOR_EMAIL / _ADMIN_CC_EMAIL constants and the _recipients_for() routing method. - Config.from_env() parses ADMIN_EMAILS into a list. - main.py passes config.admin_emails to AdminNotifier. - .env.example documents the new variable with production example. - Tests: fixture updated; TestRecipientsFor removed (routing gone). For testing: ADMIN_EMAILS=you@example.com For production: ADMIN_EMAILS=andrea.sigrist@gmx.net,baba.laeubli@gmail.com,spielgruppen@familien-verein.ch https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw --- .env.example | 9 +++++++++ main.py | 1 + src/config.py | 10 ++++++++++ src/notifications/notifier.py | 35 +++++++++++++---------------------- tests/test_notifier.py | 22 +--------------------- 5 files changed, 34 insertions(+), 43 deletions(-) diff --git a/.env.example b/.env.example index 8e554cb..5765957 100644 --- a/.env.example +++ b/.env.example @@ -43,6 +43,15 @@ 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_EMAILS=you@example.com + # --------------------------------------------------------------- # Storage # --------------------------------------------------------------- diff --git a/main.py b/main.py index 718eeff..627c818 100644 --- a/main.py +++ b/main.py @@ -58,6 +58,7 @@ 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, ) agent = EmailAgent(model=config.ai_model, kb=kb, store=store, notifier=notifier) diff --git a/src/config.py b/src/config.py index a956728..a1c81aa 100644 --- a/src/config.py +++ b/src/config.py @@ -32,6 +32,11 @@ 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) + # Storage data_dir: Path = field(default_factory=lambda: Path("data")) knowledge_base_dir: Path = field( @@ -56,6 +61,11 @@ 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() + ], data_dir=Path(os.getenv("DATA_DIR", "data")), knowledge_base_dir=Path( os.getenv( diff --git a/src/notifications/notifier.py b/src/notifications/notifier.py index ddccabb..f8e9025 100644 --- a/src/notifications/notifier.py +++ b/src/notifications/notifier.py @@ -10,11 +10,6 @@ from ..models.registration import RegistrationData logger = logging.getLogger(__name__) -# Notification routing per spec -_INDOOR_EMAIL = "andrea.sigrist@gmx.net" -_OUTDOOR_EMAIL = "baba.laeubli@gmail.com" -_ADMIN_CC_EMAIL = "spielgruppen@familien-verein.ch" - class AdminNotifier: """Sends formatted admin notification emails. @@ -34,6 +29,7 @@ class AdminNotifier: password: str, use_tls: bool = True, from_email: str = "", + admin_emails: list[str] | None = None, ) -> None: self._smtp_host = smtp_host self._smtp_port = smtp_port @@ -41,6 +37,7 @@ 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 [] # ------------------------------------------------------------------ # Public API @@ -55,9 +52,11 @@ class AdminNotifier: channel: str, ) -> None: """Send notification for a newly completed registration (version 1).""" - types = registration.booking.playgroup_types - to_addresses = self._recipients_for(types) + if not self._admin_emails: + logger.warning("ADMIN_EMAILS not configured — new-registration notification skipped.") + return + types = registration.booking.playgroup_types subject = ( f"Neue Anmeldung: {registration.child.full_name} " f"– {self._format_types(types)}" @@ -65,8 +64,8 @@ class AdminNotifier: body = self._build_new_body(registration, registration_id, version, channel) self._send( - to=to_addresses, - cc=[_ADMIN_CC_EMAIL], + to=[self._admin_emails[0]], + cc=self._admin_emails[1:], subject=subject, body=body, reply_to=registration.parent_guardian.email or "", @@ -81,15 +80,16 @@ class AdminNotifier: conversation_id: str, ) -> None: """Send notification when an existing registration is updated.""" - types = registration.booking.playgroup_types - to_addresses = self._recipients_for(types) + if not self._admin_emails: + logger.warning("ADMIN_EMAILS not configured — update notification skipped.") + return subject = f"Anmeldung aktualisiert: {registration.child.full_name}" body = self._build_update_body(registration, registration_id, version, change_summary) self._send( - to=to_addresses, - cc=[_ADMIN_CC_EMAIL], + to=[self._admin_emails[0]], + cc=self._admin_emails[1:], subject=subject, body=body, reply_to=registration.parent_guardian.email or "", @@ -99,15 +99,6 @@ class AdminNotifier: # Formatting helpers # ------------------------------------------------------------------ - @staticmethod - def _recipients_for(types: list[str]) -> list[str]: - recipients = [] - if "indoor" in types: - recipients.append(_INDOOR_EMAIL) - if "outdoor" in types: - recipients.append(_OUTDOOR_EMAIL) - return recipients - @staticmethod def _format_types(types: list[str]) -> str: has_indoor = "indoor" in types diff --git a/tests/test_notifier.py b/tests/test_notifier.py index 15750f6..e94ea7b 100644 --- a/tests/test_notifier.py +++ b/tests/test_notifier.py @@ -15,30 +15,10 @@ def notifier(): password="secret", use_tls=True, from_email="agent@example.com", + admin_emails=["to@example.com", "cc1@example.com", "cc2@example.com"], ) -# --------------------------------------------------------------------------- -# _recipients_for -# --------------------------------------------------------------------------- - - -class TestRecipientsFor: - def test_indoor_only_recipients(self, notifier): - recipients = notifier._recipients_for(["indoor"]) - assert any("andrea" in r.lower() or "sigrist" in r.lower() for r in recipients) - - def test_outdoor_only_recipients(self, notifier): - recipients = notifier._recipients_for(["outdoor"]) - assert any("baba.laeubli" in r.lower() for r in recipients) - - def test_both_includes_both_leaders(self, notifier): - recipients = notifier._recipients_for(["indoor", "outdoor"]) - joined = " ".join(recipients).lower() - assert "andrea.sigrist" in joined - assert "baba.laeubli" in joined - - # --------------------------------------------------------------------------- # _format_types # ---------------------------------------------------------------------------