"""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)