""" The agent's email to the operator (operator, 2026-10-30): when something technical goes wrong — a login that fails, a screen that sticks, a run that stops — say what happened, what the agent did about it and whether that worked, instead of leaving it for someone to find in the morning. Sent from the bot's own mailbox (the 2FA mailbox's account, over SMTP). What it may carry is fixed here, not by the caller: * screenshots of login and error pages only (`_EMAILABLE`), PNG only — a claim screen or a page's HTML carries PHI and never leaves the PC; * no claim numbers: any run of 6+ digits in the subject or body is masked. Never fatal: a mail that cannot be sent is a log line, and the run goes on. """ import re import smtplib import ssl from email.message import EmailMessage from pathlib import Path from .. import settings as cfg # `_.png` as page_utils.shot() names them. Login and error # pages only: not "logged_in" or "landing" (eCW's home can list patients). _EMAILABLE = re.compile( r"^\d{6}_(security_image_page|login_unconfirmed|username_error|" r"username_step_disabled|no_login_form|captcha|restricted|" r"password_expired|password_expiry_no_cancel|2fa_[a-z_]+|" r"approval_(page|done|no_button))\.png$") _DIGITS = re.compile(r"\d{6,}") def _mask(text: str) -> str: return _DIGITS.sub("", text or "") def emailable(path) -> bool: return bool(_EMAILABLE.match(Path(path).name)) def _transport(msg: EmailMessage) -> None: with smtplib.SMTP_SSL(cfg.SMTP_HOST, cfg.SMTP_PORT, timeout=30, context=ssl.create_default_context()) as smtp: smtp.login(cfg.SMTP_USERNAME, cfg.SMTP_PASSWORD) smtp.send_message(msg) def _send(subject: str, body: str, shots=()) -> bool: if not cfg.NOTIFY_EMAIL_TO: return False msg = EmailMessage() msg["From"] = cfg.SMTP_USERNAME msg["To"] = cfg.NOTIFY_EMAIL_TO msg["Subject"] = f"[eCW Agent] {_mask(subject)}" msg.set_content(_mask(body)) for p in shots or (): p = Path(p) if emailable(p) and p.exists(): msg.add_attachment(p.read_bytes(), maintype="image", subtype="png", filename=p.name) try: _transport(msg) print(f"[NOTIFY] Mailed: {msg['Subject']}") return True except Exception as exc: print(f"[NOTIFY] Could not mail '{msg['Subject']}': " f"{str(exc).splitlines()[0] if str(exc) else type(exc).__name__}") return False def incident(title: str, what_happened: str, what_agent_did: str, recovered: bool, shots=()) -> bool: """One technical problem: what, what the agent did, and the outcome.""" outcome = "Recovered - the run goes on." if recovered else \ "Not recovered - the agent stopped or is waiting to retry." body = (f"What happened:\n {what_happened}\n\n" f"What the agent did:\n {what_agent_did}\n\n" f"Outcome:\n {outcome}\n\n" f"Run log and screenshots are on the bot PC and in the Portal.") return _send(title, body, shots) def summary(title: str, lines) -> bool: """The end of a run: counts and anything left for a person.""" return _send(title, "\n".join(lines))