mail-address-semantics/workplans/MAS-WP-0003-provider-live-test.md
tegwick 1c0fcce69d MAS-WP-0003: finished; MAS-WP-0002 gate decided; residuals MAS-WP-0004..0007
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 14:09:21 +02:00

260 lines
12 KiB
Markdown

---
id: MAS-WP-0003
type: workplan
title: "MAS live provider test on a real subdomain"
domain: communication
repo: mail-address-semantics
status: finished
owner: claude
topic_slug: coulomb-social
created: "2026-10-08"
updated: "2026-10-18"
state_hub_workstream_id: "8e9069c6-5c32-5c56-aded-49c8fdc2b66b"
---
# MAS live provider test on a real subdomain
Follow-up to MAS-WP-0002. The MTA half of the review question *which
characters or address forms were rewritten or rejected?* was answered
against a real Postfix (`experiments/postfix-probe/`,
`docs/experiment-review.md`). This workplan answers the provider half. It
has to be done before the MAS-WP-0002 decision gate.
coulomb.social mail is hosted by IONOS, so the test needs its own
subdomain (for example `mas.coulomb.social`) with its own MX.
## Choose and provision the test subdomain
```task
id: MAS-WP-0003-T01
status: done
priority: high
state_hub_task_id: "48926a68-e5bb-5d7c-910f-12b38560b48f"
```
Needs the operator. Pick the subdomain. Add an MX (and A) record at IONOS
pointing to railiance01 (92.205.62.239). Allow inbound TCP 25 on
railiance01. Record the choice as a decision.
- 2026-10-08: DNS done. `mas.coulomb.social` has MX 10 to itself and A
92.205.62.239. Inbound TCP 25 is still closed (connection timed out from
the workstation; nothing listens on 25 and ufw allows only SSH and 2224).
The port-25 exposure is a live production change and needs the founder's
go.
- 2026-10-18: founder go for A-D (recorded 2026-10-08 in
`deploy/railiance/manifests.yaml`) executed. Port 25 is served by the
k3s `mas-smtp` LoadBalancer (klipper hostPort, so ufw is bypassed the
same way as Traefik 80/443). EHLO answers 250 from the workstation and
the node. coulombcore cannot connect, most likely because its provider
blocks outbound 25.
## Deploy Postfix + gateway + ticket service + statute
```task
id: MAS-WP-0003-T02
status: done
priority: high
state_hub_task_id: "17e4ee15-faac-5103-b62a-65a890d2cd56"
```
Deploy the probe configuration on railiance01, using `local-domain-safe`
settings, TLS and the usual relay restrictions. Serve the statute (with
the namespace set to the subdomain) at `https://<sub>/.well-known/mas`
through Traefik. Verify the deployment with `mas discover <sub>`.
Outbound replies need a relay with SPF/DKIM for the subdomain; until that
exists, keep replies in the outbox only.
- 2026-10-18: deployed to namespace `mas` with image
`docker.io/library/mas-live:wp0003-20261018`, side-loaded into k3s
containerd (`k3s ctr images import`), not pushed to a registry. A local
smoke test found that the committed entrypoint could not run: Postfix
smtpd dies without a reject-class entry in `smtpd_relay_restrictions`.
It also ignores access-map results for `!`/`%` local-parts unless
`allow_untrusted_routing=yes`. Both are fixed. The relay regexp now
`permit`s `@mas.coulomb.social` only, and the list ends in `reject`.
Live checks: the Let's Encrypt cert is issued, `mas discover
mas.coulomb.social` is valid (P1, P2 enabled, type `ticket`, mode
`callback`), and all four T03 addresses plus `x%y@` are delivered to
the mas pipe. Relaying (`x@gmail.com`, `x!y@gmail.com`,
`a@gmail.com@mas...`, `a@mas...@gmail.com`) is refused with 554, and
the queue is empty. The evidence log holds 5 smoke events from
`a@test.example`; exclude them when analysing T03/T04.
Read the evidence with:
`kubectl -n mas exec deploy/mas-mx -- cat /var/lib/mas/events.jsonl`.
## Provider send matrix
```task
id: MAS-WP-0003-T03
status: done
priority: high
state_hub_task_id: "b2af3eb2-0d4b-5cea-b033-99d47c938298"
```
Needs the operator to send from their accounts. Send from Gmail (web and
mobile), Outlook.com and one corporate gateway to:
- `ticket!callback@`
- `ticket=ID@`
- `ticket=ID+comments@`
- `ticket=RE=2026-4711@`
Then reply to a service message sent from `ticket=ID@`. For each
combination, record whether the provider's compose form accepts the
address and what envelope recipient arrives.
- 2026-10-18, Gmail web (subjects "Testmail 1-4"): the
compose form accepted all four addresses unquoted. Each arrived 08:40-08:41Z
in its own SMTP session (209.85.x, STARTTLS, BDAT). The envelope recipient
and header To were byte-identical to what was typed: no quoting, no
rewriting, no stripping of `!`, `=` or the `+comments` facet. The gateway
classified them as expected: `ticket!callback` P2 function/callback → 201
created; `ticket=ID` and `ticket=RE=2026-4711` P1 resource → 202;
`ticket=ID+comments` P1 resource, facet `comments` → 202.
- 2026-10-18, Gmail mobile (subjects "Test 2.1-2.4", 08:44-08:46Z): the
result is identical to Gmail web. All four addresses were accepted
unquoted and arrived byte-identical, with the same classification and
app statuses (201/202/202/202).
- 2026-10-18, Outlook mobile app with a corporate Microsoft 365 account
(sender domain binect.de, subjects "Test 3.1-3.4", 08:47-08:50Z): mail
was delivered by Exchange Online Protection (40.107.149.x, STARTTLS,
BDAT), one session per message. This counts as the corporate gateway.
The result is identical to Gmail. All four addresses were accepted
unquoted and arrived byte-identical, with the same classification and
app statuses (201/202/202/202).
- 2026-10-18, Outlook desktop client with the same Microsoft 365 account
(binect.de, subjects "Test 4.1-4.4", 08:57-08:59Z): mail was delivered by
Exchange Online from another pool (52.101.171.x). The result is
identical: all four addresses arrived byte-identical, with app statuses
201/202/202/202. The compose client makes no difference within M365.
- Consumer Outlook.com: skipped (operator decision 2026-10-18; no account).
Consumer Outlook.com runs on the same Exchange Online infrastructure as
M365, so outbound handling is covered by the two M365 runs. Not covered:
the Outlook-on-the-web compose form (only the native apps were used).
- Reply test: operator chose to set up outbound now (2026-10-18). Port 25
outbound is blocked on railiance01 (to Gmail and Outlook MX it times out;
587 and 465 are open), so replies go through Scaleway Transactional
Email on [smtp.tem.scaleway.com]:587 with SASL. This is enabled only when
Secret `mas-relay` exists; without it, non-local mail fails fast with
5.7.1. Image `mas-live:wp0003-20261018b` also runs Postfix unchrooted:
the chroot lacked resolv.conf, which is why rDNS showed "unknown".
Tested locally against the real TEM endpoint with dummy credentials:
TLS verified, SASL rejected as expected.
Relay login: OpenBao platform/suppliers/scaleway/mas-tem-relay, synced
by ExternalSecret `mas-relay` (ops-mason MASON-WP-0014, CCR-2026-0051).
The ExternalSecret needs explicit target metadata because of
reject-secret-last-applied. The first relay attempt got 535 from TEM
until the operator added the missing TEM IAM policy.
Reply leg 1 (2026-10-18): Gmail → `ticket!callback@` created ticket
GS-85QJ8WC09P. The service reply From
`ticket=GS-85QJ8WC09P@mas.coulomb.social` was accepted by TEM at 11:23:46Z
and arrived at Gmail with SPF pass, DKIM pass (d=mas.coulomb.social) and
DMARC pass. Gmail displays the `=` From address unmodified.
Reply leg 2: the first answer was lost. During TEM setup an MX
`10 blackhole.tem.scaleway.com` had been added at the same priority as
ours, so about half of inbound mail went to Scaleway's discard host
silently. The record was removed at 11:28Z (TTL 3600). Do not add the TEM
blackhole MX to a domain that receives mail. The re-sent answer arrived
11:28:39Z: Gmail replied to `ticket=GS-85QJ8WC09P@mas.coulomb.social`
byte-identical (envelope and To), with In-Reply-To the service
Message-ID and References carrying both. The gateway parsed it as a P1
resource (ticket/GS-85QJ8WC09P); the app returned 200 correlated, and
the ticket thread now holds both messages.
T03 result: no provider/client combination (Gmail web, Gmail mobile, M365
via Outlook mobile and desktop) rewrote, quoted or rejected any test
address, in either direction.
## Forwarding and list paths
```task
id: MAS-WP-0003-T04
status: done
priority: medium
state_hub_task_id: "97c88b4f-14c3-5f6c-8f31-6b9fc3afd236"
```
Send via one forwarding service (SRS) and one mailing list (VERP). Check
whether `=` collides with SRS0/VERP encoding, and whether facets survive
or are stripped.
- 2026-10-18, IONOS mail forward (`mas-fwd@coulomb.social`): the IONOS form
refuses `=` in the destination and forces lower case. The operator used
`ticket--GS-85QJ8WC09P+comments@`, and `ticket--gs-85qj8wc09p+comments@`
arrived at 11:47:06Z from mout.kundenserver.de. The envelope sender was
SRS-rewritten (`SRS0=ppc5=II=gmail.com=bernd.worsch@srs2.kundenserver.de`).
SRS only touches the sender, so there is no collision with `=` in
recipients. The facet survived, and the header To kept the forward
address. The gateway saw an opaque target with a facet, and the app
correctly declined it (404). This led to SCP-6 (statute-declared `--`
separator; the operator endorses the mapping) and SCP-7 (case-robust
identifiers).
Gmail forwarding: skipped (optional; would only add a second forwarder
that accepts `=`).
- 2026-10-18, Google Groups (`coulomb-social@googlegroups.com`): the member
form accepted `ticket=GS-85QJ8WC09P+comments@` unchanged. The invitation
(11:58:20Z) arrived byte-identical and was correlated; no auto-reply was
sent (Auto-Submitted). After acceptance, the list post (12:05:37Z)
arrived for the **lower-cased** `ticket=gs-85qj8wc09p+comments@`. Its
envelope sender was `coulomb-social+bnc…@googlegroups.com`, which does
not collide with our syntax, and the facet survived. Exact comparison
missed the ticket, so the post was quarantined with no reply
(Precedence: list). Correct per v0.1, but lost to the ticket. This is
the second provider to lower-case, so SCP-7 is now the highest priority.
## Cap total outbound mail
```task
id: MAS-WP-0003-T06
status: done
priority: high
state_hub_task_id: "aadd3010-da3f-5e38-b62d-0dcf489b1455"
```
Operator request (2026-10-18): bound the total number of mails sent
through the TEM relay. The initial value is 300. `ticket_service.py
--max-sent N` counts replies actually sent (`outbox.sent=1`, which persists
in the ticket DB on the PVC). Once N is reached, further replies are kept in
the outbox unsent. The entrypoint passes `MAS_MAX_SENT` (manifest: 300) and
routes null-sender mail (Postfix bounces) to `discard:`, so nothing else can
use the relay outside the count. A unit test covers the cap. The container
was verified locally: bounces are discarded and replies go to TEM.
Deployed 2026-10-18 (image `mas-live:wp0003-20261018c`). Live: the ticket
service runs with `--max-sent 300` and null-sender mail goes to `discard:`.
The one reply sent earlier (GS-85QJ8WC09P) is marked `sent=1`, so the count
starts at 1/300. Outbox rows 1-5 predate the relay and were never sent. To
change the cap, edit `MAS_MAX_SENT` in the manifest and restart the pod.
## Close the review
```task
id: MAS-WP-0003-T05
status: done
priority: high
state_hub_task_id: "63e86851-9c7d-5ce8-977f-5d09268d9dcc"
```
Fold the results into `docs/experiment-review.md`. Turn the spec gaps
(quoted equivalence, resource-type case, type-intake semantics for
`ticket!MODE`) into spec change proposals. Then take the MAS-WP-0002
decision gate.
- 2026-10-18: provider results (T02/T03) and operational findings are in
`docs/experiment-review.md`. `docs/spec-change-proposals.md` holds
SCP-1 (quoted equivalence), SCP-2 (lower-case resource types), SCP-3
(type intake), SCP-4 (informative deliverability) and SCP-5 (intake
auto-reply reflection). The review ends with a decision-gate
recommendation; the founder decides. Pending: T04 results, then
revisit SCP-4 and the gate table.
- 2026-10-18: T04 is folded in. SCP-6 (`--` alternate separator) and SCP-7
(case-robust identifiers, now highest priority) were added. MAS-WP-0002
decision gate taken by the founder: build the Postfix profile, the spec
errata and the composer; defer the rest.
## Residuals
Handed off as live work records (origin: residual, origin_ref MAS-WP-0003):
- MAS-WP-0004: Postfix integration profile.
- MAS-WP-0005: spec v0.1.1 errata, v0.2 drafts, case-robust ticket ids
in the reference service.
- MAS-WP-0006: address composer.
- MAS-WP-0007 (backlog): retire the live test environment. Namespace `mas`
stays up for follow-up validation, capped at 300 sends.