From 929c7b5b69d7ebb9b1d4f6168e8f94ae2fa7c50e Mon Sep 17 00:00:00 2001 From: minichorus-pm Date: Tue, 23 Jun 2026 16:21:49 -0400 Subject: [PATCH] docs: mailing-lists explanation + manage-mailing-lists polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second of the two docs offers from the dev-list thread on Final Prep for v2. Adds the conceptual explainer that was missing and fills the gaps in the existing how-to. ## docs/explanations/mailing-lists.md (new) The conceptual model. Coverage: - What a list is: a swarm-scoped, addressable fan-out target with no inbox. - The address shape: list:@@, with the list: prefix as the distinguishing marker against the agent / user / admin shapes. - Why lists exist: three concrete problems (broadcast in one send, stable address for changing audience, policy separated from membership). - Anatomy: name, swarm, host, owner, members, policy, metadata. Canonical address immutable; policy and members mutable. - The policy shape: three independent enumerations (visibility, join_policy, send_policy), each with v1's honored variant flagged and the deferred variants explained as forward-looking wire-format reservations. - How messages flow: local delivery picks up list:, looks up list by address, fans out per-member with metadata.list_address, nested-list members get skipped, webhook firing is per-member. - Admin vs user-agent permission split: admin owns create / patch / member add-remove / delete and gets unconditional reads; user-agents get policy-gated read / subscribe / unsubscribe / send. - Addressability examples (table of the four address shapes). - Things lists are NOT: not a queue or buffer, not a history store, not a privacy boundary in v1. Sources verified against src/mail/protocol/src/mail_protocol/core/lists.py — the policy enum and 'forward-looking' framing are paraphrased from the inline docstring. ## docs/howtos/manage-mailing-lists.md (polished) The existing how-to covered list / list-get / create / subscribe / unsubscribe / member-post / member-delete / send. Added: - An updated Starting Point that points at the new explanation and names the address-shape convention explicitly. - Step 6 (NEW): update list policy via list-patch with the v1 honored variants and a pointer to the deferred-variant discussion. - Step 7 (NEW): delete a list via list-delete with the in-flight-messages-already-expanded note. - Step 8 (was step 6): send to a list, plus a forward reference to the webhook delivery doc for how the list_address metadata surfaces on the wire. - See-also section with cross-links to mailing-lists, webhook-delivery, addressing-model, and http-api. ## docs/explanations/README.md Index row added for the new mailing-lists explainer. --- docs/explanations/README.md | 1 + docs/explanations/mailing-lists.md | 235 ++++++++++++++++++++++++++++ docs/howtos/manage-mailing-lists.md | 84 +++++++++- 3 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 docs/explanations/mailing-lists.md diff --git a/docs/explanations/README.md b/docs/explanations/README.md index 91a73b9..36abf6c 100644 --- a/docs/explanations/README.md +++ b/docs/explanations/README.md @@ -12,6 +12,7 @@ They are for understanding, not for step-by-step tasks or exhaustive lookup. | [Addressing Model](addressing-model.md) | Why does MAIL use host-scoped and swarm-scoped addresses? | | [Delivery Model](delivery-model.md) | Why are daemons responsible for message delivery? | | [Security Model](security-model.md) | What are the main trust boundaries and risks? | +| [Mailing Lists](mailing-lists.md) | What is a list? How does it expand, what does its policy mean, and how do admin and user-agent permissions split? | | [MAIL v1 Legacy Runtime](mail-v1-legacy.md) | How should readers interpret the archived v1 runtime and docs? | | [Documentation System](documentation-system.md) | How should maintainers decide where a new page belongs? | diff --git a/docs/explanations/mailing-lists.md b/docs/explanations/mailing-lists.md new file mode 100644 index 0000000..cc46b8f --- /dev/null +++ b/docs/explanations/mailing-lists.md @@ -0,0 +1,235 @@ +# Mailing Lists + +Status: draft + +## Scope + +The conceptual model behind MAIL's mailing lists: what they are, how +they're addressed, how messages flow through them, how the policy +shape works, and how admin and user-agent permissions split. The +matching how-to for *operating* lists via the CLI is [Manage +Mailing Lists](../howtos/manage-mailing-lists.md); the formal route +reference is in [HTTP API](../references/http-api.md). + +## What a list is + +A MAIL list is a **swarm-scoped, addressable fan-out target**. It is +not a user-agent and it does not own an inbox. When a sender +addresses a message to a list, MAIL's local delivery path expands +the list and delivers one copy of the message to each member +(see [Local versus remote delivery in the Delivery +Model](delivery-model.md#local-versus-remote-delivery) for the +broader pipeline). + +The address shape is: + +``` +list:@@ +``` + +The `list:` prefix is what distinguishes a list address from the +other three address shapes (`agent`, `user`, `admin`); see +[Addressing Model](addressing-model.md) for the full address +taxonomy. Subscribers (members) are themselves addressable +user-agents on the same host — typically the same swarm, though +cross-swarm membership is possible. + +## Why lists exist + +Three concrete problems lists solve cleanly: + +- **One sender, many recipients with a single send.** Without + lists, a sender broadcasting to N recipients has to issue N + send requests (or send to one address and have the receiver + re-broadcast). Lists let the fan-out happen server-side in + one atomic dispatch. +- **Stable address for a changing audience.** Members can be + added or removed without the sender needing to know. A + `list:announcements@chorus@chrn.ai` address persists; the + set of recipients behind it can change daily. +- **Policy control on send / join / visibility separated from + membership.** Who can join, who can post, and who can see + the list are three different questions; the list's `policy` + object addresses each independently. + +## Anatomy of a list + +A MAIL list has the following fields (see +[`MAILList`](../references/data-models.md) for the formal +schema): + +| Field | Meaning | +| --- | --- | +| `name` | The swarm-scoped identifier (e.g., `announcements`). | +| `swarm` | The swarm this list belongs to. | +| `host` | The MAIL host the list lives on. | +| `owner` | The MAIL address of the user-agent that created or owns the list. | +| `members` | The current list of subscribed user-agent addresses. | +| `policy` | The visibility / join / send policy (see below). | +| `metadata` | Free-form key/value pairs for downstream consumers. | + +Once created, the canonical address (`name`, `swarm`, `host`) is +**immutable for the life of the list**. The `policy` and `members` +fields can change; admin-side patches in v1 are limited to policy +edits. + +## The policy shape + +The `policy` object has three fields, each an enumeration with the +v1 variant the server actually honors flagged below: + +```python +class MAILListPolicy: + visibility: "public" | "private" # v1 honors: public + join_policy: "open" | "approval" | "admin-only" # v1 honors: open + send_policy: "open" | "members-only" | "admin-only" # v1 honors: open +``` + +The wire format reserves all enumerations now so future +contributions can extend the server without changing the protocol +shape. Other variants pass protocol-layer validation but are +rejected at the endpoint layer in v1 with `501 Not Implemented`. + +What "open" means in each field: + +- `visibility: public` — the list address appears in `GET /lists` + and `GET /lists/{addr}` for any authenticated user-agent. +- `join_policy: open` — any user-agent can `POST + /lists/{addr}/subscribe` to add themselves as a member without + admin intervention. +- `send_policy: open` — any user-agent can address a message to + the list and have it expanded. + +A v1 list is therefore effectively a **public open-open** list: +anyone can see it, anyone can join, anyone can post. + +The deferred variants (`approval`, `admin-only`, `members-only`, +`private`) define the structure that v1.1+ can fill in. Designing +a list with `join_policy: admin-only` today means the policy is +recorded faithfully but the server returns `501` on any +self-subscribe attempt; readers can use that signal to know "this +list will become admin-managed when the server honors it." + +## How messages flow through a list + +When a sender addresses a message to `list:@@`, +the following happens on the receiving MAIL server: + +1. **Local delivery picks up the list address.** The recipient + prefix `list:` triggers the list-expansion path rather than + the normal user-agent delivery. +2. **The list is looked up by address.** If the list does not + exist, the message is dropped with a log line (no error to + the sender; lists are an opportunistic fan-out, not a + reliable RPC). +3. **For each member of the list,** MAIL's local delivery is + invoked again with the member's address. Each member receives + the message in their inbox as if the sender had addressed + them directly, except that the message's `metadata` carries + a `list_address` field pointing back at the originating list. +4. **Nested list members are rejected.** A list address inside + another list's member set logs a warning and is skipped; + v1 does not support recursive expansion. +5. **Webhook firing happens per-member, not per-list.** Each + recipient's webhook fires individually; the originating list + surfaces via the per-event `metadata.list_address`. + +The `metadata.list_address` field is what lets downstream +consumers (a webhook receiver, an inbox UI) distinguish "I was +sent this directly" from "I was sent this because I'm on a list." +See the [Webhook Delivery](webhook-delivery.md) explainer for +how the field appears on the wire. + +## Admin and user-agent permission split + +Lists have a clean two-layer permission model: + +### Admin-only operations + +- **Create a list** (`POST /admin/lists`). The list address must + be unique on the server. +- **Patch policy** (`PATCH /admin/lists/{addr}`). Only `policy` + is mutable; the address is fixed for the life of the list. +- **Add or remove members** (`POST /admin/lists/{addr}/members`, + `DELETE /admin/lists/{addr}/members/{member}`). Forcible + membership change without the member's consent. +- **Delete a list** (`DELETE /admin/lists/{addr}`). +- **Read everything** (`GET /admin/lists`, `GET + /admin/lists/{addr}`). Admin reads are not gated by + `visibility`. + +### User-agent operations + +- **Read public lists** (`GET /lists`, `GET /lists/{addr}`). Lists + with `visibility: public` appear; private lists do not. +- **Self-subscribe** (`POST /lists/{addr}/subscribe`). Honored + when `join_policy: open`; returns `501` for deferred variants. + Membership is permission-blind at storage — the router gates + on policy, not the storage layer. +- **Self-unsubscribe** (`POST /lists/{addr}/unsubscribe`). + Symmetric: members can always leave. +- **Send to a list** (compose + send with a list address as + recipient). Honored when `send_policy: open`; deferred variants + similarly return `501`. + +### Why this split + +The split reflects the broader MAIL trust model (see [Security +Model](security-model.md)). Admins have the authority to shape +the list as an object: who exists, who's on it, what its policy +is. User-agents have the authority to participate within the +policy bounds the admin has set. + +This means a deployment can have a `join_policy: open` list that +any user-agent can join, but the *list itself* — its existence, +its members at create time, its policy — is an admin's +responsibility. Conversely, a `join_policy: admin-only` list (when +v1.1+ honors it) means even discoverable lists can't be joined +without going through the admin. + +## Addressability examples + +A few address-shape examples to make the model concrete: + +| Address | Means | +| --- | --- | +| `bob@chorus@example.com` | The user-agent `bob` in the `chorus` swarm. | +| `list:announcements@chorus@example.com` | The list `announcements` in the `chorus` swarm. | +| `admin:ops@example.com` | The admin `ops` on the host (not swarm-scoped). | +| `user:alice@example.com` | The end-user `alice` on the host. | + +Sending to a list looks identical to sending to a user-agent +from the sender's side — the difference is what the receiving +server does with the address. + +## Things lists are not + +A few clarifying negatives: + +- **Lists are not a queue or buffer.** Messages are expanded + synchronously into per-member deliveries; there is no + list-level inbox or pending state. +- **Lists do not retain a "sent through the list" history.** The + history lives in the senders' outboxes and the recipients' + inboxes. The list as an object has no message log. +- **Lists are not a privacy boundary.** Members of a list whose + `visibility: public` is honored can be enumerated by any + authenticated user-agent via `GET /lists/{addr}`. Treat the + member list as discoverable in v1. + +## See also + +- [Manage Mailing Lists](../howtos/manage-mailing-lists.md) — the + task-oriented CLI walkthrough for operating lists. +- [Addressing Model](addressing-model.md) — the broader address + taxonomy lists sit within. +- [Delivery Model](delivery-model.md) — the local-vs-remote + delivery pipeline that handles list expansion. +- [Webhook Delivery](webhook-delivery.md) — how list deliveries + carry the `metadata.list_address` field for downstream + consumers. +- [Data Models](../references/data-models.md) — formal + field-by-field schema for `MAILList`, `MAILListInBackend`, and + `MAILListPolicy`. +- [HTTP API](../references/http-api.md) — the formal route + reference, including admin and user-agent list endpoints. diff --git a/docs/howtos/manage-mailing-lists.md b/docs/howtos/manage-mailing-lists.md index 4710e54..260be13 100644 --- a/docs/howtos/manage-mailing-lists.md +++ b/docs/howtos/manage-mailing-lists.md @@ -4,11 +4,24 @@ Status: draft ## Goal -How to create mailing lists, inspect them, and manage subscriptions or members. +How to create mailing lists, inspect them, manage subscriptions or +members, edit policy, and delete them. The conceptual model — the +address shape, the policy structure, the admin/user permission +split — is in [Mailing Lists](../explanations/mailing-lists.md); +this how-to assumes you've at least skimmed it. ## Starting Point -The reader has credentials with permissions appropriate for the list action. +The reader has credentials with permissions appropriate for the list +action. Admin credentials are required for create / patch / member +add-remove / delete; user-agent credentials are sufficient for +read / subscribe / unsubscribe / send. + +The list address shape is `list:@@` — see +[Addressing Model](../explanations/addressing-model.md). Anywhere +this document writes `{list_address}`, it expects the full +`list:`-prefixed form (e.g., +`list:announcements@chorus@example.com`). ## Steps @@ -99,9 +112,51 @@ uv run mail-admin list-member-delete {list_address} {member_address} If successful, details on the mailing list will be printed to the console. -### 6. Send a message to a list address +### 6. Update list policy as an admin + +Admins can update the policy on an existing list with the +`mail-admin` command `list-patch`. The list's canonical address +(`name`, `swarm`, `host`) is immutable; only the policy fields can +change. + +```bash +MAIL_SERVER={server_url} +MAIL_TOKEN={admin_jwt} +uv run mail-admin list-patch {list_address} \ + --visibility public \ + --join-policy open \ + --send-policy open +``` + +For v1, only `public` / `open` / `open` are honored; the other +variants are reserved in the wire format and rejected at the +endpoint layer with `501`. See [Mailing +Lists](../explanations/mailing-lists.md#the-policy-shape) for +context on the deferred variants. + +### 7. Delete a list as an admin -If they are authorized to do so, user-agents can send a message to a list address by specifying the list address in the `mail` command `send`: +To remove an existing list entirely, use `list-delete`: + +```bash +MAIL_SERVER={server_url} +MAIL_TOKEN={admin_jwt} +uv run mail-admin list-delete {list_address} +``` + +The list is removed from the server and the canonical address +becomes available for re-creation. In-flight messages already +expanded into per-member deliveries before the delete are +unaffected (they live in the recipients' inboxes); messages +addressed to the list after the delete are dropped per the +unknown-list path described in [Mailing Lists → How messages +flow through a list](../explanations/mailing-lists.md#how-messages-flow-through-a-list). + +### 8. Send a message to a list address + +If they are authorized to do so, user-agents can send a message to +a list address by specifying the list address in the `mail` command +`send`: ```bash MAIL_SERVER={server_url} @@ -109,6 +164,27 @@ MAIL_TOKEN={ua_jwt} uv run mail send {draft_id} {list_address} ``` +The receiving server expands the list and delivers one copy to +each member's inbox. Each member's webhook (if any) fires with a +`metadata.list_address` field naming the originating list so +downstream consumers can distinguish list deliveries from direct +ones — see [Webhook +Delivery](../explanations/webhook-delivery.md#payload-shape) for +the field placement on the wire. + +## See also + +- [Mailing Lists](../explanations/mailing-lists.md) — the + conceptual model: address shape, policy fields, expansion + semantics, permission split. +- [Webhook Delivery](../explanations/webhook-delivery.md) — how + list deliveries surface to webhook receivers via + `metadata.list_address`. +- [Addressing Model](../explanations/addressing-model.md) — the + full address taxonomy lists sit within. +- [HTTP API](../references/http-api.md) — the formal route + reference for admin and user-agent list endpoints. + ## Source Material - `src/mail/client/src/mail_client/commands/lists.py`