mail-address-semantics/docs/spec-change-proposals.md
tegwick 6ee9e4dd54 MAS-WP-0005-T01: SCP-1..7 accepted; T01 done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 252470@bnt-lap001
Assistant-Session: ad628c8f-4be2-4979-94c9-76c7ed6f8241
2026-10-18 18:06:14 +02:00

14 KiB

MAS v0.1 spec change proposals (from MAS-WP-0002/0003)

Status: accepted by the founder (2026-10-18): SCP-1 to SCP-7 as written below, including the decisions recorded under SCP-2 and SCP-6. Implementation is tracked in MAS-WP-0005. These are proposals against mas-v0.1-spec/. The spec bundle stays unchanged until they are accepted, because mas_conformance/ and mas_core/schema/ are vendored copies and the tests check them for drift. Evidence references point to docs/experiment-review.md.

Proposed target: a v0.1.1 errata release for SCP-1 to SCP-3, SCP-5 and the recommendation part of SCP-7. Errata here are changes that clarify or tighten without changing what canonical addresses mean. SCP-6 is a new feature and targets v0.2. SCP-4 is informative only.


SCP-1: quoted local-parts that are valid dot-strings are equivalent

Sections: §4 Canonical ASCII local-part, §8 Address equivalence.

Evidence: Postfix delivered "ticket=PR42"@… as ticket=PR42 in all five probe configurations (RFC 5321 treats unnecessary quoting as equivalent). A receiver behind an MTA cannot tell which form the sender used.

Proposed text (§4, after the second paragraph):

A quoted local-part whose content, after removing the quotes and with no quoted-pairs, is a valid canonical MAS local-part is equivalent to that unquoted form. Receivers MUST treat the two identically. Generators MUST emit the unquoted form.

Impact: clarification only. No parser change, since quoted input still does not parse. A new conformance fixture should record the equivalence.


SCP-2: resource-type names are lower-case

Sections: §6.1 Resource types, grammar resource-type, §8.

Evidence: the ABNF accepts Ticket=PR42, but the statute schema only registers lower-case type names. The address parses but can never match a declared type, even under ascii-insensitive.

Decision (founder, 2026-10-18): option (a). Resource types are lower-case in the grammar.

Options:

  • (a) recommended: restrict resource-type in the grammar to lower-case ALPHA, DIGIT and -. Ticket=PR42 then parses as an opaque target, which is the safe outcome under §6.1 ("no semantics solely because of =").
  • (b) keep the grammar and state in §8 that the statute's equivalence policy also applies to the type name.

(a) keeps the type a protocol token rather than user data, and it matches the schema. (b) makes type matching depend on the statute, which is exactly the hidden inference §8 tries to avoid.

Impact: (a) is a grammar tightening. None of the 31 shared conformance fixtures uses an upper-case type, so a new fixture (Ticket=PR42 → opaque) records the change. No repo test uses one either. Live traffic in MAS-WP-0003 used only lower-case types.


SCP-3: type-intake semantics for TYPE!MODE

Section: §7 Interaction semantics.

Evidence: in ticket!callback, the target ticket is an opaque target with no defined link to the resource type ticket. The canonical §7 example implies one. In the experiment, the statute had to register ticket as a function (registry key) and the service had to know what that means. Live, ticket!callback@mas.coulomb.social worked from Gmail and M365 only because of that registry entry.

Proposed text (§7, new paragraph):

An opaque target that equals a resource type declared in the statute, and that the statute also registers as a function, is that type's intake: mail to it requests creation or selection of a resource of that type. Without such a registration the target has no intake semantics, even if it equals a declared resource type.

Also add a statute-schema field such as "intake_for": "ticket" on registry entries, so the link is declared rather than implied by equal names.

Impact: additive; the schema gains an optional field. The policy resolver reports intake_for when it is present.


SCP-4 (informative): deliverability evidence for ! and =

Section: §12 Deliverability and rewriting.

Evidence: Gmail (web and app) and Microsoft 365 (Outlook mobile and desktop) accepted and delivered all four test forms byte-identical. That includes !, a doubled = and a + facet, both inbound and in a reply to an = From address. Postfix 3.7 never applied the legacy !/% rewrites to MAS addresses carrying an @domain. Pending: SRS and VERP (MAS-WP-0003 T04).

Counter-evidence (T04): the IONOS forwarding form refused = and forced lower case. Google Groups accepted = but lower-cased the member address (SCP-6, SCP-7). SRS and list bounce addresses never collided with = or +. Mail clients preserved MAS syntax; provider configuration forms did not.

Proposal: keep the normative text. Add an informative note recording the tested paths, including the IONOS refusal, and an implementation note that a default Postfix relay configuration rejects every local-part containing ! (see SCP-5 and the review's operational findings). The "cooperating environments" wording for ! stays until T04 has covered forwarding.


SCP-5: intake auto-replies are a reflection vector

Section: §14 Security (items 4 and 5).

Evidence: an intake address that answers every request (as ticket!callback@ does with "your request is now ticket …") sends mail to whatever From: the request carried. A forged From: makes the service send mail to a third party, which is backscatter in all but name. §14.5 only says "avoid backscatter" and is usually read as covering MTA bounces. The live deployment had to add an application-level cap (T06) and discard MTA bounces to keep the relay bounded.

Proposed text (§14, new item):

  1. A receiver that automatically replies to mail sent to an intake or interaction address SHOULD reply only when the sender is authenticated (for example DMARC alignment pass), and MUST bound the volume of such replies (rate limits or caps). Replies MUST NOT be sent to automated mail (Auto-Submitted, Precedence).

Impact: normative for receivers. The reference ticket service already skips automated mail and has a send cap. A DMARC check would need the gateway to pass Authentication-Results in the event, which is a small event-schema addition.


SCP-6: statute-enabled stand-ins and destination transforms (-- for =, --- for !)

Sections: §6 Resource semantics, §11 Statute document, §12.

Evidence: the IONOS forwarding form refuses = in a destination address. The operator compensated with ticket--GS-…, and it was delivered, but under v0.1 it is only an opaque target. The service correctly could not attach it to the ticket.

Decisions (founder, 2026-10-18):

  • = stays canonical. -- is only a replacement for endpoints that refuse =.

  • It is enabled by statute opt-in (option A), not as a spec-wide default.

  • The spec stays normative for the mechanism and explains how specific integrations are done through the statute when needed. Concrete integrations are not part of the normative text.

  • Similar endpoint behaviours are covered in the same mechanism: a stand-in for !, plus case, facet, interaction, quoting, dot and length handling (founder confirmed, 2026-10-18).

  • Stand-ins use dashes only: -- for = and --- for !. No other stand-in characters are added. + is considered functional by default and has no stand-in (founder, 2026-10-18).

Proposal, receiving side (normative): the spec defines two fixed stand-ins. A statute MAY enable each one:

"alternate_syntax": { "resource": true, "interaction": true }

The stand-ins are fixed by the spec, not configurable, so every generator and receiver uses the same forms.

  • Resource stand-in -- for =. In a target TYPE--ID, where TYPE is a resource type declared in the same statute, the form is equivalent to TYPE=ID. The policy resolver reports target_syntax: "resource-alternate" and the canonical equivalence key TYPE=ID.

  • Interaction stand-in --- for !. A local-part ending in ---MODE, where MODE is an interaction mode the statute enables, is equivalent to !MODE: ticket---callback ≡ ticket!callback.

  • Matching order (normative):

    1. Interaction first. If the local-part ends in ---MODE with an enabled MODE, that suffix (at the last such ---) is the interaction.
    2. Resource next. In the remainder, TYPE--ID with a declared type is a resource. The split is at the first --.

    Examples:

    • ticket---callback ≡ ticket!callback
    • ticket--GS1---callback ≡ ticket=GS1!callback
    • ticket--A---B ≡ ticket=A---B if B is not an enabled mode
  • Constraints:

    • Resource-type names (lower-case after SCP-2) never contain --.
    • Interaction mode names do not start with -.
    • A namespace that enables the interaction stand-in MUST NOT issue resource IDs containing ---.
    • A namespace with existing opaque names matching either pattern must not enable that stand-in.
  • No stand-in for +. + is functional by default: every endpoint tested accepted it, and facets degrade safely when stripped (§12).

  • Without the statute switch, or for an undeclared type or mode, -- and --- have no meaning (§6.1 still applies). Receivers store, correlate and reply using the canonical form only.

Proposal, sending side (normative mechanism, informative integrations): generators produce addresses for a destination, such as a forwarding form, a mailing list or a reply header. The spec defines a destination transform, the set of changes a generator may apply so that an address fits a destination's constraints. The set is closed and covers the endpoint behaviours seen or expected:

Transform Effect Allowed only if
resource-alternate TYPE=ID → TYPE--ID statute enables alternate_syntax.resource
interaction-alternate !MODE → ---MODE statute enables alternate_syntax.interaction and the mode MODE
lowercase fold the whole local-part to lower case statute declares case-insensitive identifiers (SCP-7); types are lower-case (SCP-2)
drop-facet remove +FACET (loses information) always; only when explicitly requested
drop-interaction remove !MODE (loses the requested mode; the mail still reaches intake) always; only when explicitly requested, with a warning

Endpoint behaviours that need no transform:

  • Quoting: generators never quote, and receivers treat quoted and unquoted forms as equal (SCP-1).
  • Dots: generators never insert dots. Dot handling is statute dot folding (§8).
  • Length: after all transforms, the 64-octet limit is checked again (-- adds one octet and --- adds two). A generator MUST refuse rather than truncate.
  • $ and ^: remain reserved; P3 is out of scope.

A generator MUST emit the canonical form when the destination allows it. It MAY apply the transforms the destination needs, and only those the namespace's statute permits. If the result would not be accepted by the namespace, the generator MUST report that the address cannot be expressed for that destination, instead of emitting a form the receiver would misread. Outbound service mail (From/Reply-To) stays canonical unless a destination transform is known to be needed.

An informative section, "Integrating with constrained endpoints", explains the procedure:

  1. observe what the endpoint refuses or rewrites;
  2. declare the matching acceptance in the statute;
  3. describe the endpoint's constraints as a destination profile in the generator's configuration.

It uses IONOS forwarding as the worked example: = is refused and case is folded, so the profile is resource-alternate + lowercase, and the result is ticket--gs-85qj8wc09p+comments@. Concrete profiles (e.g. ionos-forward) live in implementation configuration or a catalog, not in the spec.

Impact: additive: an optional schema object (alternate_syntax, two booleans), parser and policy support for both stand-ins, a generator destination parameter with the five transforms, and new fixtures. Targets v0.2. The composer (MAS-WP-0006) is the first consumer of destination profiles.


SCP-7: resource identifiers must survive lower-casing

Sections: §6.2 Resource identifiers, §8 Address equivalence, §13.

Evidence: the IONOS forwarding form forces destination addresses to lower case: GS-85QJ8WC09P became gs-85qj8wc09p. Google Groups did the same after a member accepted the invitation: the invitation was delivered mixed-case, but list mail went to the lower-cased address and was quarantined as an unknown resource. With the spec default (exact comparison) and a statute declaring case: exact, the identifier no longer matches. Many systems treat local-parts as case-insensitive in practice, despite RFC 5321.

Priority: highest of this set. Two of two forwarding/list providers lower-cased the recipient.

Proposal:

  • §6.2: "Generators SHOULD use resource identifiers whose meaning does not depend on letter case (for example lower-case only, or a case-insensitive alphabet such as Crockford base32), because forwarders and web forms may change the case of local-parts."
  • Statute: allow equivalence.resource_id_case: "insensitive" independently of the local-part case setting. Receivers then fold the identifier for lookup but must not fold anything else.
  • Together with SCP-2, the resource type is already lower-case.

Impact: recommendation plus an optional statute field. The reference service could switch to lower-case IDs today. Crockford base32 decoding is case-insensitive by design.


Lower-priority gaps (no proposal yet)

Recorded in the review, not urgent:

  • interaction-mode names allowed by the value grammar but not by the schema (call.back, a=b);
  • registry keys containing = or leading/trailing dots, which need semantic lint;
  • discovery: whether the content type is checked, and the exact redirect same-origin rule;
  • statute-declared facets.