Map a MAS gateway event into the email-connect evidence ceiling.

mas-email-evidence 0.1 reports MX acceptance, a bounce, or a reply, and
refuses the claim that a person read the message. Ordinary product mail
stays in email-connect. A product speaks the mode contract.

Assistant: grok
Assistant-Session: 01a198fa-fd27-7e41-a0a2-57355c3e4efd
This commit is contained in:
tegwick 2026-11-01 23:13:04 +01:00
parent f18eb0b8df
commit aef4c5dd71
5 changed files with 271 additions and 2 deletions

View file

@ -62,7 +62,9 @@ delivery stays external.
`mas.coulomb.social`, with replies through Scaleway TEM capped at 300.
`power.sh` turns the two Deployments on and off. Off keeps the
namespace, evidence volume, credentials, DNS, and LoadBalancer.
MAS-WP-0007.
MAS-WP-0007. Ordinary product mail stays in email-connect. A product
speaks the mode contract. Gateway events map into that service's
evidence ceiling through `docs/mas-email-evidence-v0.1.json`.
## Out of Scope
@ -102,7 +104,7 @@ delivery stays external.
2026-10-19). Founder decision 2026-11-01: keep it and power it on or
off (MAS-WP-0007). It was left on. The 300-reply cap, the side-loaded
image, and snakeoil SMTP TLS stay until a later decision.
- Tests: `.venv/bin/pytest` runs 280 tests, offline apart from loopback
- Tests: `.venv/bin/pytest` runs 285 tests, offline apart from loopback
fixtures, the Docker probe and Node for `impl/js/`. `mas test` runs
four corpora: parser 64, statute 16, v0.2 statute-aware 33, mode
contract 195. `cd impl/js && node --test` runs 85 more.

View file

@ -0,0 +1,69 @@
{
"contract": "mas-email-evidence",
"version": "0.1",
"source": "mas-gateway",
"owner": "mail-address-semantics",
"consumer": "email-connect",
"statement": "A MAS gateway event is a channel fact from the MAS MX. This mapping reports MX acceptance, a bounce, or a reply. It does not claim that a person read the message, opened a payload, or satisfied a business result. Tickets, statutes, and templates stay outside this contract.",
"evidence_ceiling": {
"max_positive_event": "interaction.reply_received",
"max_positive_strength": "medium",
"can_prove_human_awareness": false,
"can_prove_payload_access": false,
"can_prove_payload_delivery": false,
"can_prove_identity": false,
"can_prove_authority": false,
"can_prove_non_repudiation": false
},
"outcomes": {
"parse_failed": {
"event_type": "diagnostic.message.parse_failed",
"assessment_category": "undef",
"assessment_subclass": "undef.parse_failed",
"evidence_strength": "none",
"normalized_meaning": "The value is not a MAS gateway event v0.1. No acceptance, bounce, or reply is claimed."
},
"bounced_permanent": {
"event_type": "notification.endpoint.rejected_permanent",
"assessment_category": "fail",
"assessment_subclass": "fail.hard_bounce",
"evidence_strength": "negative",
"normalized_meaning": "The message accepted by the MAS MX is a permanent delivery failure for an earlier message. That is not evidence about a person."
},
"bounced_temporary": {
"event_type": "notification.endpoint.rejected_temporary",
"assessment_category": "undef",
"assessment_subclass": "undef.deferred",
"evidence_strength": "ambiguous",
"normalized_meaning": "The message accepted by the MAS MX is a temporary delivery failure. Later evidence can change this."
},
"bounce_unclassified": {
"event_type": "notification.endpoint.unknown",
"assessment_category": "undef",
"assessment_subclass": "undef.conflicting_evidence",
"evidence_strength": "ambiguous",
"normalized_meaning": "The message looks like a delivery report, and the permanent or temporary result is not reliable."
},
"out_of_office": {
"event_type": "interaction.out_of_office_received",
"assessment_category": "undef",
"assessment_subclass": "undef.out_of_office",
"evidence_strength": "weak",
"normalized_meaning": "An automatic reply was accepted by the MAS MX. It is not a human reply and it does not show that a person read the earlier message."
},
"reply_received": {
"event_type": "interaction.reply_received",
"assessment_category": "success",
"assessment_subclass": "success.reply_received",
"evidence_strength": "medium",
"normalized_meaning": "A non-automatic reply was accepted by the MAS MX. A reply does not prove who read the earlier message or that a request was accepted."
},
"mx_accepted": {
"event_type": "notification.endpoint.accepted",
"assessment_category": "undef",
"assessment_subclass": "undef.endpoint_accepted_only",
"evidence_strength": "weak",
"normalized_meaning": "The MAS MX accepted this message for the recipient. Acceptance is not inbox placement and not human reading."
}
}
}

View file

@ -45,6 +45,17 @@ that handoff (it carries a per-handoff token), with an increasing `seq`.
Never send `timed-out`.
`mas_modes/stubs/callback.py` is a complete example.
## Ordinary product mail stays in email-connect
A product integrates with this facility by speaking the mode contract. It
does not send MAS syntax through the email-connect transactional receiver.
`docs/mas-email-evidence-v0.1.json` maps one gateway event into
email-connect's evidence ceiling: the MX accepted the message, the message
is a bounce, or the message is a non-automatic reply. The ceiling refuses
the claim that a person read it. `mas_gateway.email_evidence` is the
reference function. Tickets and statutes stay here. Templates and the
product ledger stay in email-connect.
## Examples only, not products
- `examples/ticket_service/`: the reference ticket service, used as the

View file

@ -0,0 +1,127 @@
"""Map a MAS gateway event into the email-connect evidence ceiling.
The normative outcome table is docs/mas-email-evidence-v0.1.json. The same
file is vendored in email-connect. A gateway event means the MAS MX took the
message. Bounce and reply are properties of that message. Nothing here claims
that a person read it.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
CONTRACT_PATH = Path(__file__).resolve().parents[1] / "docs" / "mas-email-evidence-v0.1.json"
_CONTRACT = json.loads(CONTRACT_PATH.read_text())
_ENHANCED = re.compile(r"\b([45]\.\d{1,3}\.\d{1,3})\b")
_SMTP = re.compile(r"\b([45]\d{2})\b")
_DSN_MARKERS = (
"mailer-daemon",
"delivery status notification",
"undelivered mail",
"mail delivery failed",
"final-recipient",
"diagnostic-code",
)
_PERMANENT_TEXT = ("user unknown", "mailbox not found", "unknown user", "undeliverable")
_TEMPORARY_TEXT = ("mailbox full", "try again later", "temporary failure")
def from_gateway_event(event: object) -> dict:
"""Return one evidence record for a gateway event."""
if not _is_event(event):
return _record("parse_failed", None)
message = event["message"]
blob = _blob(message)
if _is_bounce(message, blob):
if _permanent(blob):
outcome = "bounced_permanent"
elif _temporary(blob):
outcome = "bounced_temporary"
else:
outcome = "bounce_unclassified"
elif _is_out_of_office(message):
outcome = "out_of_office"
elif _is_reply(message):
outcome = "reply_received"
else:
outcome = "mx_accepted"
return _record(outcome, event)
def _is_event(event: object) -> bool:
return (
isinstance(event, dict)
and event.get("mas_event_version") == "0.1"
and isinstance(event.get("message"), dict)
and isinstance(event.get("recipient"), str)
)
def _blob(message: dict) -> str:
parts = [
message.get("from") or "",
message.get("subject") or "",
message.get("auto_submitted") or "",
message.get("text") or "",
]
return "\n".join(str(part) for part in parts).lower()
def _is_bounce(message: dict, blob: str) -> bool:
sender = str(message.get("from") or "").lower()
if "mailer-daemon" in sender or "postmaster@" in sender:
return True
return any(marker in blob for marker in _DSN_MARKERS if marker != "mailer-daemon")
def _permanent(blob: str) -> bool:
enhanced = _ENHANCED.search(blob)
smtp = _SMTP.search(blob)
if enhanced and enhanced.group(1).startswith("5."):
return True
if smtp and smtp.group(1).startswith("5"):
return True
return any(marker in blob for marker in _PERMANENT_TEXT)
def _temporary(blob: str) -> bool:
enhanced = _ENHANCED.search(blob)
smtp = _SMTP.search(blob)
if enhanced and enhanced.group(1).startswith("4."):
return True
if smtp and smtp.group(1).startswith("4"):
return True
return any(marker in blob for marker in _TEMPORARY_TEXT)
def _is_out_of_office(message: dict) -> bool:
auto = str(message.get("auto_submitted") or "").lower()
return auto.startswith("auto-replied")
def _is_reply(message: dict) -> bool:
reply = str(message.get("in_reply_to") or "").strip()
auto = str(message.get("auto_submitted") or "").lower()
return bool(reply) and auto in ("", "no")
def _record(outcome: str, event: dict | None) -> dict:
spec = _CONTRACT["outcomes"][outcome]
message = (event or {}).get("message") or {}
return {
"contract": _CONTRACT["contract"],
"contract_version": _CONTRACT["version"],
"source": _CONTRACT["source"],
"outcome": outcome,
"event_type": spec["event_type"],
"assessment_category": spec["assessment_category"],
"assessment_subclass": spec["assessment_subclass"],
"evidence_strength": spec["evidence_strength"],
"normalized_meaning": spec["normalized_meaning"],
"evidence_ceiling": _CONTRACT["evidence_ceiling"],
"recipient": (event or {}).get("recipient"),
"message_id": message.get("message_id"),
"in_reply_to": message.get("in_reply_to"),
}

View file

@ -0,0 +1,60 @@
"""MAS gateway event to email-connect evidence ceiling (mas-email-evidence 0.1)."""
from mas_gateway.email_evidence import from_gateway_event
def event(**message):
base = {"from": "a@example.org", "subject": "hello", "text": "body"}
base.update(message)
return {
"mas_event_version": "0.1",
"recipient": "ticket=GS-1@mas.coulomb.social",
"mas": {"valid": True},
"message": base,
}
def test_ordinary_mail_is_mx_acceptance_only():
got = from_gateway_event(event())
assert got["outcome"] == "mx_accepted"
assert got["event_type"] == "notification.endpoint.accepted"
assert got["assessment_subclass"] == "undef.endpoint_accepted_only"
assert got["evidence_ceiling"]["can_prove_human_awareness"] is False
assert got["evidence_ceiling"]["max_positive_strength"] == "medium"
def test_reply_is_medium_and_does_not_prove_a_person_read_it():
got = from_gateway_event(event(in_reply_to="<m@x>", text="I saw this"))
assert got["outcome"] == "reply_received"
assert got["evidence_strength"] == "medium"
assert "does not prove" in got["normalized_meaning"]
assert got["evidence_ceiling"]["can_prove_human_awareness"] is False
def test_automatic_reply_is_not_a_human_reply():
got = from_gateway_event(event(in_reply_to="<m@x>", auto_submitted="auto-replied", text="away"))
assert got["outcome"] == "out_of_office"
assert got["event_type"] == "interaction.out_of_office_received"
def test_permanent_and_temporary_bounces():
hard = from_gateway_event(event(**{
"from": "mailer-daemon@mx.example",
"subject": "Undelivered Mail Returned to Sender",
"text": "Final-Recipient: rfc822; b@example.org\nStatus: 5.1.1\nuser unknown",
}))
soft = from_gateway_event(event(**{
"from": "mailer-daemon@mx.example",
"subject": "Delivery Status Notification",
"text": "Final-Recipient: rfc822; b@example.org\nStatus: 4.2.2\nmailbox full",
}))
assert hard["outcome"] == "bounced_permanent"
assert soft["outcome"] == "bounced_temporary"
assert hard["evidence_ceiling"] == soft["evidence_ceiling"]
def test_a_non_event_makes_no_channel_claim():
got = from_gateway_event({"hello": "no"})
assert got["outcome"] == "parse_failed"
assert got["evidence_strength"] == "none"
assert got["recipient"] is None