"""Developer alert notifier. Single source of truth for immediate infrastructure/system error email alerts sent to the developer team when no Mail ID is available to attribute the error to. Processing-error alerts (Mail ID available) flow through processing_error_handler → daily_brief instead. Callers MUST await send_developer_alert() before continuing so delivery is confirmed before the loop or job resumes. """ from __future__ import annotations import logging from app.config import Settings from app.lib.graph.auth import GraphAuthProvider from app.lib.graph.client import GraphClient logger = logging.getLogger(__name__) def _build_sendmail_payload( recipients: list[str], subject: str, body_html: str, mailbox_user: str, ) -> tuple[str, dict]: """Build a (path, payload) pair ready for GraphClient.post(). Shared by send_developer_alert and scheduler._send_daily_brief to avoid duplicating the sendMail envelope shape. """ path = f"/users/{mailbox_user}/sendMail" payload = { "message": { "subject": subject, "body": {"contentType": "HTML", "content": body_html}, "toRecipients": [ {"emailAddress": {"address": r}} for r in recipients ], }, "saveToSentItems": False, } return path, payload async def send_developer_alert( settings: Settings, subject: str, body: str, ) -> None: """Send an immediate alert email to DEV_ALERT_RECIPIENTS. The body is expected to contain both the exception message and the full stack trace (formatted via traceback.format_exc()) so the developer has complete context in the email. Creates its own Graph client on demand — safe to call from the processor (which has no shared client) and from failure paths where the existing client may be broken. If sending fails the exception is logged and swallowed — this function must never recurse or propagate, as all callers are already in an error path. """ recipients = settings.dev_alert_recipient_list() if not recipients: logger.warning( "send_developer_alert: DEV_ALERT_RECIPIENTS not configured; " "alert not sent. Subject: %s", subject, ) return client: GraphClient | None = None try: auth = GraphAuthProvider(settings) client = GraphClient(auth) path, payload = _build_sendmail_payload( recipients=recipients, subject=subject, body_html=f"
{body}
", mailbox_user=settings.mailbox_user, ) await client.post(path, payload) logger.info( "Developer alert sent to %d recipient(s). Subject: %s", len(recipients), subject, ) except Exception: logger.exception( "send_developer_alert: failed to send alert. Subject: %s", subject ) finally: if client is not None: try: await client.aclose() except Exception: pass