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
260 lines
12 KiB
Markdown
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.
|