Make admin notification recipients configurable via ADMIN_EMAILS
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
This commit is contained in:
@@ -43,6 +43,15 @@ SMTP_USE_TLS=true
|
|||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
REGISTRATION_EMAIL=anmeldung@example.com
|
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
|
# Storage
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ def build_components(config: Config):
|
|||||||
password=config.imap_password,
|
password=config.imap_password,
|
||||||
use_tls=config.smtp_use_tls,
|
use_tls=config.smtp_use_tls,
|
||||||
from_email=config.registration_email,
|
from_email=config.registration_email,
|
||||||
|
admin_emails=config.admin_emails,
|
||||||
)
|
)
|
||||||
|
|
||||||
agent = EmailAgent(model=config.ai_model, kb=kb, store=store, notifier=notifier)
|
agent = EmailAgent(model=config.ai_model, kb=kb, store=store, notifier=notifier)
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ class Config:
|
|||||||
# Registration email address shown to parents
|
# Registration email address shown to parents
|
||||||
registration_email: str = ""
|
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
|
# Storage
|
||||||
data_dir: Path = field(default_factory=lambda: Path("data"))
|
data_dir: Path = field(default_factory=lambda: Path("data"))
|
||||||
knowledge_base_dir: Path = field(
|
knowledge_base_dir: Path = field(
|
||||||
@@ -56,6 +61,11 @@ class Config:
|
|||||||
smtp_port=int(os.getenv("SMTP_PORT", "587")),
|
smtp_port=int(os.getenv("SMTP_PORT", "587")),
|
||||||
smtp_use_tls=os.getenv("SMTP_USE_TLS", "true").lower() == "true",
|
smtp_use_tls=os.getenv("SMTP_USE_TLS", "true").lower() == "true",
|
||||||
registration_email=os.getenv("REGISTRATION_EMAIL", ""),
|
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")),
|
data_dir=Path(os.getenv("DATA_DIR", "data")),
|
||||||
knowledge_base_dir=Path(
|
knowledge_base_dir=Path(
|
||||||
os.getenv(
|
os.getenv(
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ from ..models.registration import RegistrationData
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class AdminNotifier:
|
||||||
"""Sends formatted admin notification emails.
|
"""Sends formatted admin notification emails.
|
||||||
@@ -34,6 +29,7 @@ class AdminNotifier:
|
|||||||
password: str,
|
password: str,
|
||||||
use_tls: bool = True,
|
use_tls: bool = True,
|
||||||
from_email: str = "",
|
from_email: str = "",
|
||||||
|
admin_emails: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._smtp_host = smtp_host
|
self._smtp_host = smtp_host
|
||||||
self._smtp_port = smtp_port
|
self._smtp_port = smtp_port
|
||||||
@@ -41,6 +37,7 @@ class AdminNotifier:
|
|||||||
self._password = password
|
self._password = password
|
||||||
self._use_tls = use_tls
|
self._use_tls = use_tls
|
||||||
self._from_email = from_email or username
|
self._from_email = from_email or username
|
||||||
|
self._admin_emails: list[str] = admin_emails or []
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Public API
|
# Public API
|
||||||
@@ -55,9 +52,11 @@ class AdminNotifier:
|
|||||||
channel: str,
|
channel: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send notification for a newly completed registration (version 1)."""
|
"""Send notification for a newly completed registration (version 1)."""
|
||||||
types = registration.booking.playgroup_types
|
if not self._admin_emails:
|
||||||
to_addresses = self._recipients_for(types)
|
logger.warning("ADMIN_EMAILS not configured — new-registration notification skipped.")
|
||||||
|
return
|
||||||
|
|
||||||
|
types = registration.booking.playgroup_types
|
||||||
subject = (
|
subject = (
|
||||||
f"Neue Anmeldung: {registration.child.full_name} "
|
f"Neue Anmeldung: {registration.child.full_name} "
|
||||||
f"– {self._format_types(types)}"
|
f"– {self._format_types(types)}"
|
||||||
@@ -65,8 +64,8 @@ class AdminNotifier:
|
|||||||
body = self._build_new_body(registration, registration_id, version, channel)
|
body = self._build_new_body(registration, registration_id, version, channel)
|
||||||
|
|
||||||
self._send(
|
self._send(
|
||||||
to=to_addresses,
|
to=[self._admin_emails[0]],
|
||||||
cc=[_ADMIN_CC_EMAIL],
|
cc=self._admin_emails[1:],
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=body,
|
body=body,
|
||||||
reply_to=registration.parent_guardian.email or "",
|
reply_to=registration.parent_guardian.email or "",
|
||||||
@@ -81,15 +80,16 @@ class AdminNotifier:
|
|||||||
conversation_id: str,
|
conversation_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send notification when an existing registration is updated."""
|
"""Send notification when an existing registration is updated."""
|
||||||
types = registration.booking.playgroup_types
|
if not self._admin_emails:
|
||||||
to_addresses = self._recipients_for(types)
|
logger.warning("ADMIN_EMAILS not configured — update notification skipped.")
|
||||||
|
return
|
||||||
|
|
||||||
subject = f"Anmeldung aktualisiert: {registration.child.full_name}"
|
subject = f"Anmeldung aktualisiert: {registration.child.full_name}"
|
||||||
body = self._build_update_body(registration, registration_id, version, change_summary)
|
body = self._build_update_body(registration, registration_id, version, change_summary)
|
||||||
|
|
||||||
self._send(
|
self._send(
|
||||||
to=to_addresses,
|
to=[self._admin_emails[0]],
|
||||||
cc=[_ADMIN_CC_EMAIL],
|
cc=self._admin_emails[1:],
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=body,
|
body=body,
|
||||||
reply_to=registration.parent_guardian.email or "",
|
reply_to=registration.parent_guardian.email or "",
|
||||||
@@ -99,15 +99,6 @@ class AdminNotifier:
|
|||||||
# Formatting helpers
|
# 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
|
@staticmethod
|
||||||
def _format_types(types: list[str]) -> str:
|
def _format_types(types: list[str]) -> str:
|
||||||
has_indoor = "indoor" in types
|
has_indoor = "indoor" in types
|
||||||
|
|||||||
+1
-21
@@ -15,30 +15,10 @@ def notifier():
|
|||||||
password="secret",
|
password="secret",
|
||||||
use_tls=True,
|
use_tls=True,
|
||||||
from_email="agent@example.com",
|
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
|
# _format_types
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user