Files
ecw-charge-validation/src/ecw_bot/core/notify.py
T
2026-10-30 10:58:35 +05:30

87 lines
3.2 KiB
Python

"""
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
# `<HHMMSS>_<step>.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("<claim>", 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))