- experiments/postfix-probe: Postfix 3.7 -> pipe -> mas-gateway -> ticket service in Docker; 14 address forms x 5 configurations. 13/14 arrive byte-identical everywhere; quoted "ticket=PR42" is unquoted in transit. Legacy bang/percent rewrites never fire for MAS addresses. - docs/experiment-review.md: Email infrastructure section with results - MAS-WP-0003: provider-side live test on a real subdomain (needs DNS/MX) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 623586@bnt-lap001 Assistant-Session: 10b30928-5b3b-4c11-9934-5ecca07367d8
7.6 KiB
MAS v0.1 experiment review (first pass, 2026-10-08)
Status of MAS-WP-0002, Milestone 9. The answers below come from building
mas_core, the mas CLI, mas-gateway and the reference ticket service,
and from running the test suite (135 tests, all offline except the
loopback fixture servers).
Scope: the MTA half of the email path was tested against a real
Postfix (3.7.11, Debian bookworm, in Docker). The setup was SMTP → Postfix
→ pipe transport → mas-gateway → reference ticket service
(experiments/postfix-probe/). Mailbox providers (Gmail, Outlook,
corporate gateways) and public DNS/MX were not involved. That half moves
to MAS-WP-0003.
Answers to the review questions
Syntax: did the parser stay simple?
Yes. The parser is about 100 lines with no I/O. It always uses the maximal
(P2) grammar and records required_profile, so ticket!callback parses
without knowing anything about the namespace. All 31 spec fixtures and 27
added property cases pass.
Gaps found in the spec:
- Resource-type case. The ABNF allows
Ticket=PR42, but the statute schema only registers lower-case type names. Such an address parses, but it can never match a declared type, even when the statute saysascii-insensitive. The spec should either make resource types lower-case in the grammar, or state that the equivalence policy also applies to the type name. - Interaction-mode names. The value grammar allows
call.backanda=b. The schema's mode-name pattern does not. Those modes parse but can never be enabled. This is harmless, but it should be documented. - Registry keys. The schema pattern already blocks
+ ! $ ^, but it still allows=and leading or trailing dots. Semantic lint is needed to reject a key liketicket=GS-1(registry-key-resource).
Discovery: was exact-host metadata sufficient?
For this experiment, yes. Implementation choices the spec does not settle:
application/json(or+json) is required as the content type (InvalidContentType). The spec only asks for 2xx plus the schema. It should say whether the content type is checked.- Redirects are followed only when scheme, host and port all match. That
is stricter than "same host". A port change counts as
CrossHostRedirectand an https→http change asSchemeDowngrade. mas_versionis checked before the schema runs, so a future version is reported asUnsupportedVersionand not as a schema error.- Operational finding: a service must not depend on discovering its
own statute. If the statute endpoint is down, the ticket service sees
namespace_confirmed = falseand correctly refuses to act. The mail is then declined. Operators should pin their own statute in local configuration (mas-gateway --statute). Discovery is for third-party namespaces.
Policy: could all inference stay explicit?
Yes. Policy is a pure function. Without a statute, kind is always
opaque, = alone gives no resource semantics, and ! stays
"unconfirmed". mas explain lists each of these as an unknown.
Folding is applied only when a statute declares it, and dot folding never
touches resource identifiers.
Facets have no statute declaration at all, so they are always reported as "application-defined". A later version could let statutes list the facets they recognise.
Email infrastructure: which forms were rewritten or rejected?
Postfix (tested): 14 address forms were sent through 5 Postfix
configurations (experiments/postfix-probe/results/):
- relay domain with defaults;
- local rewrite context, with and without
swap_bangpath/allow_percent_hack; - domain in
mydestination(local delivery class), with and without those two settings.
The results:
- 13 of 14 forms arrived byte-identical in every configuration. That
includes
ticket!callback,ticket=ID+comments,ticket==X1,ticket=PR%2B42,ticket=ID+comments!callbackandticket=PR42$4.99.recipient_delimiter = +does not change the envelope recipient thatpipepasses on as${recipient}. Upper case is preserved. - The quoted local-part
"ticket=PR42"arrived unquoted asticket=PR42. Postfix drops quotes that are not needed (RFC 5321 treats the two forms as equivalent), so a form the spec calls non-canonical became canonical in transit. Spec gap: §4 should say that a quoted local-part whose content is a valid dot-string is equivalent to the unquoted form, because an MAS receiver after an MTA cannot tell them apart. - The
!and%legacy rewrites did not trigger for any MAS address, even with Postfix defaults and the domain inmydestination. In 3.7 the bang-path swap fired only for a baresite!userwith no@domain(checked via localsendmail: it was rerouted tosite). MAS addresses always carry an@domain. Settingswap_bangpath = noandallow_percent_hack = nois still a sensible hardening default for MAS hosts. $passed through SMTP untouched. Rejecting it is purely a MAS parser decision. The gateway forwarded it asvalid: false, and the app ignored it.
Still untested (MAS-WP-0003):
- whether senders' providers and web forms accept
!and=in recipient addresses; - overlap with SRS (
SRS0=...) and VERP when forwarding; - facet stripping by providers.
Resource addressing: did it improve correlation?
Yes. The service replies from ticket=GS-…@great-service.example, so an
ordinary reply lands on the resource address. Correlation uses the address
only. The tests send an unrelated subject and a facet-stripped address,
and both correlate.
Subject lines: could identifiers disappear?
Yes. Replies use the customer's own subject, and no ticket id is ever put in it (asserted in the tests).
Lifecycle: did closed resources stay understandable?
Yes. A closed ticket stays addressable. Mail to it is archived separately
and the ticket is not changed. A single notice goes only to the original
requester, and never to automated senders (Auto-Submitted,
Precedence). Mail to an unknown resource is quarantined with no reply.
For an MTA pipe, the gateway exits 0 when the app declines and 75 when the
app is unreachable, so it never generates a bounce (no backscatter).
Interaction: was !callback useful?
Partly. It expresses intent cleanly, but the service still needs a phone
number and a time. The address cannot carry those, so the first reply has
to ask by email. !callback therefore starts a negotiation; it does not
replace one.
The bare ticket in ticket!callback is an opaque target. It has no
defined link to the resource type ticket. The spec's canonical example
(§7) implies such a link. The statute has to register ticket as an
intake function, and the service has to know what that means. The spec
should either say this explicitly, or define "type intake" semantics for
an opaque target that names a declared resource type.
Security: did any address start acting like a capability?
Not for reading or control. Knowing a ticket address grants no read access and no admin action. It does let anyone append correspondence to an open ticket, which is the intended function of a communication endpoint. That is also an injection and spam vector. Ticket ids are non-guessable (10 Crockford base32 characters), but sender policy belongs to the application, not to MAS.
Next step (MAS-WP-0003, before the decision gate)
Run the provider half on a real subdomain:
- an MX pointing to a Postfix configured like the probe;
- the statute served at
https://<sub>/.well-known/mas; - test mail from Gmail, Outlook and one corporate gateway to
ticket!callback@,ticket=ID@andticket=ID+comments@, plus replies to the service'sticket=ID@From address.