Snapshot of D:\Ecw Process a11c9ff (2026-10-30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.2 KiB
Python
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))
|