mail-address-semantics/mas_modes/mailer.py
tegwick e91cd01a02 MAS-WP-0010 T01-T05: mode contract v0.1, schemas, callback stub, email as master
mas-modes-v0.1-spec/ (contract, message + gateway-event schemas, transition
table, message fixtures, statute) vendored into mas_modes. EmailMode
implements the master role over the reference ticket service (MasterStore):
handoff, negotiation by email, statuses in the thread, timeouts, resume by
email, principle 10 shared via mas_modes.mailer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
2026-10-19 10:57:20 +02:00

82 lines
3.2 KiB
Python

"""Outbound mail under principle 10 (CONTRACT.md section 6).
Every send is a reply to a recorded inbound message: the recipient is that
message's sender, and the message's own sender authentication decides
whether email may answer at all.
"""
from __future__ import annotations
import smtplib
import sqlite3
from email.message import EmailMessage
from typing import Optional
OUTBOX = """
CREATE TABLE IF NOT EXISTS mode_outbox (
seq INTEGER PRIMARY KEY AUTOINCREMENT, case_ref TEXT, mail_from TEXT, mail_to TEXT,
subject TEXT, body TEXT, in_reply_to TEXT, sent INTEGER DEFAULT 0
);
"""
def refusal(msg: dict, require_dmarc_pass: bool = True) -> Optional[str]:
"""Why email must not answer `msg`, or None if it may."""
auto = (msg.get("auto_submitted") or "no").lower() != "no"
bulk = (msg.get("precedence") or "").lower() in ("bulk", "list", "junk")
if not msg.get("from") or auto or bulk:
return "automated-sender"
if require_dmarc_pass and (msg.get("authentication") or {}).get("dmarc") != "pass":
return "unauthenticated-sender"
return None
class Mailer:
"""Gated replies, recorded in an outbox; sent over SMTP when configured,
up to a lifetime cap counted in the outbox."""
def __init__(self, db: sqlite3.Connection, smtp: Optional[tuple[str, int]] = None,
max_sent: Optional[int] = None, require_dmarc_pass: bool = True):
self.db = db
self.db.executescript(OUTBOX)
self.smtp = smtp
self.max_sent = max_sent
self.require_dmarc_pass = require_dmarc_pass
def reply(self, case_ref: str, mail_from: str, msg: dict, subject: str,
body: str) -> Optional[str]:
"""Answer `msg` from `mail_from`. Returns None when accepted, else the reason."""
why = refusal(msg, self.require_dmarc_pass)
if why:
return why
if self.cap_reached():
return "send-cap-reached"
if not subject.lower().startswith("re:"):
subject = "Re: " + subject
cur = self.db.execute(
"INSERT INTO mode_outbox (case_ref, mail_from, mail_to, subject, body, in_reply_to)"
" VALUES (?,?,?,?,?,?)", (case_ref, mail_from, msg["from"], subject, body,
msg.get("message_id")))
if self.smtp:
self._send(mail_from, msg["from"], subject, body, msg.get("message_id"))
self.db.execute("UPDATE mode_outbox SET sent=1 WHERE seq=?", (cur.lastrowid,))
return None
def cap_reached(self) -> bool:
if self.max_sent is None:
return False
n = self.db.execute("SELECT COUNT(*) FROM mode_outbox WHERE sent=1").fetchone()[0]
return n >= self.max_sent
def outbox(self) -> list[dict]:
return [dict(r) for r in self.db.execute("SELECT * FROM mode_outbox ORDER BY seq")]
def _send(self, mail_from, to, subject, body, in_reply_to):
m = EmailMessage()
m["From"], m["To"], m["Subject"] = mail_from, to, subject
m["Auto-Submitted"] = "auto-replied"
if in_reply_to:
m["In-Reply-To"] = m["References"] = in_reply_to
m.set_content(body)
with smtplib.SMTP(*self.smtp) as s:
s.send_message(m)