Merge pull request 'Design the mail web client: mesh-mail-web on the private network behind Keycloak (design 59, ADR 0440)' (#437) from design/59-the-mail-web-client into main

This commit was merged in pull request #437.
This commit is contained in:
2026-10-19 18:44:29 +00:00
3 changed files with 154 additions and 0 deletions
+1
View File
@@ -5,6 +5,7 @@ code: [mesh-controller internal/catalogue/state.go, mesh-controller internal/bro
updated: 2026-10-04
decisions:
- "ADR 0201"
- "ADR 0439"
---
# A module's state, as it runs
@@ -0,0 +1,152 @@
---
layer: to-be
status: in-progress
code: [mesh-catalog]
updated: 2026-10-19
decisions:
- "ADR 0440"
- "ADR 0378"
- "ADR 0379"
- "ADR 0359"
- "ADR 0361"
- "ADR 0138"
- "ADR 0167"
- "ADR 0439"
- "ADR 0201"
- "ADR 0436"
---
# 59 — The mail web client
**The operator's mail, every account of it, in a browser on the mesh's own machines. A module of its own,
`mesh-mail-web`, holds the seat `mail-webclient`; it renders pages on the server in Go and reaches mail only through
`mail-store`'s verbs, under a caller the operator grants account by account. Its route reaches the private network
only, under its internal name, and the operator signs in with Keycloak. A mail written there goes out only on the
operator's Approve, as every mail the mesh sends does. Once it works on the laptop and the workstation, Betterbird is removed.**
(ADR 0440, building on ADR 0378 §5 and ADR 0379 §2.)
**Acceptance.** In a browser on the laptop and on the workstation, the operator signs in, browses, searches and reads the mail of
every account `mesh-mail` holds, opens an attachment, and writes a mail that goes out on their Approve on Telegram;
the same page is not reachable from a machine outside the private network.
## 1. The parts
```
browser (laptop, workstation) ── https://mail.<node>.internal ──▶ route proxy (internal reach only, the mesh's own certificate)
│
▼
mesh-mail-web (seat mail-webclient, once for the mesh)
│ sign-in: oidc-client ──▶ Keycloak
│ pages rendered on the server, a mail shown as text
▼ over the bus, as caller <machine>.mesh-mail-web
mail-store (mesh-mail): mail_accounts · mail_search · mail_read
mail_send ──▶ the operator's Approve (Telegram)
mail_send_status
mesh-mail's account bucket (ADR 0439 §2): folders and their counts
```
- **`mesh-mail-web`** is built from `mesh-mail`'s source, as `mesh-mail-notifier` is, and is assigned to the machine
that runs `mesh-mail` (the control-node today). It keeps no mail: no database, nothing on disk (ADR 0440 §1, §4).
- **`mesh-mail`** gains its account bucket (ADR 0439 §2, issue 1255) and a caller check on `mail_send_status`
(ADR 0440 §7, issue 1261), both released with `mesh-mail-web`. It
answers `mesh-mail-web` only for the accounts its trusted setting `grants` gives `mesh-mail-web` (ADR 0359 §4,
ADR 0378 §13). `mail_send_status` becomes part of `mail-store`'s protocol (ADR 0440 §7).
- **The browser never reaches the bus.** The bus has no WebSocket listener or sign-in for a person yet (research 036,
gap G1), so every page is HTML from `mesh-mail-web` (ADR 0440 §5).
## 2. Reachable from the private network only (ADR 0440 §2)
The operator's rule of 2026-10-15: a web UI for mail is reached only over the mesh's private network, never from
outside. Two things hold it:
1. **Its route's reach is `internal`** (ADR 0138 §7). Reach is a setting of the machine a module runs on, keyed by
port, and with none the route proxy composes a public name as well as the internal one. So `mesh-mail-web`'s
`reach` is set to `internal` on its machine before it is assigned there. The route proxy then composes only
`mail.<node>.internal`, serves it only to the mesh's own machines (ADR 0167 §6), and certifies it with the mesh's
own authority.
2. **The module answers only its internal name.** A request whose host is not `mail.<node>.internal` is answered
404, so a public name composed by mistake shows nothing.
Measured on 2026-10-19: the laptop and the workstation hold the mesh's authority (`mesh-internal-ca.crt`) as a trust anchor, and
Firefox on both reads the system's trust through p11-kit, so the page opens there without a warning. Away from the
private network (the laptop travelling without its tunnel) the page is unreachable; Betterbird stays the offline client
until it is removed (§6).
## 3. Signing in (ADR 0440 §3)
`mesh-mail-web` requires `oidc-client` from Keycloak, as grafana does, and uses the authorisation-code flow. It answers
no page, attachment or send to a browser that has not signed in, and lets in only the identities its trusted setting
`operators` names (nobody by default). The session cookie is `Secure`, `HttpOnly` and `SameSite=Strict` and ends
after 12 hours. Every form that changes something (today only the send) carries a token bound to the session.
The operator chose this on 2026-10-19: without it, any process on any mesh machine, agents included, could read every
account through the page and get around `grants`.
## 4. The pages
| page | what it shows | how |
|---|---|---|
| accounts | every granted account, its state, its folders and their counts | `mail_accounts`; the folders from `mesh-mail`'s account bucket, only of an account `mail_accounts` answered (the bucket's read is wider than the grant, ADR 0440 §4) |
| folder | one folder's mails, newest first, in pages of 50 | `mail_search` with the folder; the next page asks for mails before the last one shown |
| search | mails across every granted account by words, sender, recipient, date, attachment | `mail_search`'s filters (issues 1228-1232) |
| mail | one mail: its headers, its body as escaped text, its attachments | `mail_read` |
| attachment | one attachment, as a download | `mail_read` with the attachment's key |
| write | from (a granted account), to, subject, body; after sending, the Approve's state | `mail_send`, then `mail_send_status` |
- **A mail is shown as text** (ADR 0440 §5): `mail_read` turns HTML into text, the page escapes it, and nothing
remote is loaded. An attachment answers `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`.
Every page carries a content security policy that allows no script or style from elsewhere.
- **A send is an ask** (ADR 0361 §2): the page says the ask is raised, then follows `mail_send_status` (ADR 0436
§12's states, now part of `mail-store`'s protocol, ADR 0440 §7) until a final one: sent, failed, refused, not
proven, declined, expired or unknown. The operator's Approve comes on Telegram, as for every mail the mesh sends.
- **The seat's verbs** (ADR 0378 §5): `open` answers the page's address, and with an account and a mail that mail's
page; `status` answers whether it serves and which accounts it is granted.
## 5. What the first version leaves out
Each is an issue with an owner (ADR 0440 §8):
- a reply, cc, bcc and attachments on a sent mail: issue 1257 (it needs `mail_send` to take them first);
- a page that shows new mail without a reload, from ADR 0439's buckets: issue 1258, after issue 1255;
- the agent that searches, triages and drafts (ADR 0378 §5): issue 1259, which needs a decision first;
- the work account: a later decision of its own (ADR 0359 §7).
## 6. The build's acceptance tests
1. A request without a session, for a page, an attachment or a send, is redirected to sign-in; an identity not in
`operators` is answered 403; a send form without its token is answered 403; the session cookie is `Secure`,
`HttpOnly` and `SameSite=Strict` and ends 12 hours after sign-in.
2. A request whose host is not the internal name is answered 404.
3. Against a stand-in `mail-store` and a filled account bucket on the real bus, the pages call only
`mail_accounts`, `mail_search`, `mail_read`, `mail_send` and `mail_send_status`, list folders from the bucket,
page by date, and a send shows its state; the manifest declares no database and no writable directory.
4. A mail whose body holds `<script>` and a remote image is shown as text; an attachment is a download with
`nosniff`; every page carries a content security policy allowing only its own origin.
5. After a mail is indexed, `mesh-mail`'s account bucket changes only the keys that describe it, within ADR 0439's
bounds (issue 1255's own tests).
6. The declaration for its machine composes `mesh-mail-web`'s route with its internal name only (`mesh-controller.plan`).
7. `mail_send_status` answers each state of ADR 0436 §12, and `unknown` for an id it does not know and for another
caller's send (a unit test of `mesh-mail`, issue 1261).
8. The seat's verbs: `open` answers the page's address, and with an account and a mail that mail's page; `status`
answers whether it serves and the accounts it is granted (a unit test of `mesh-mail-web`, issue 1262).
9. The acceptance sentence at the top, live: in a browser on the laptop and on the workstation, for every account.
Tests 1 to 4 are one integration test that starts the module whole against the real bus (playbook 07 step 3 item 4);
tests 5 and 7 are `mesh-mail`'s own and test 8 `mesh-mail-web`'s, unit tests; test 6 is read before the module is
assigned; test 9 is proven live after the delivery.
## 7. Milestones
| # | Outcome, in one sentence | Proves | Repositories and their parts | Depends on |
|---|---|---|---|---|
| 1 | In a browser on the laptop and the workstation, the operator signs in and browses, searches and reads every account's mail with its attachments, and writes a mail that goes out on their Approve | tests 1 to 9 | mesh-catalog: `mesh-mail`'s account bucket (issue 1255, ADR 0439 §2); the module `mesh-mail-web` (manifest, sign-in, the pages, the send, the seat's verbs, the integration test) | — |
| 2 | Betterbird is gone from the laptop and the workstation, its profile and mail kept in the home | ADR 0379 §2 | mesh-catalog: Betterbird unassigned on the operator's Approve, then the `betterbird` module removed from the catalogue (issue 806) | 1 |
**Confirmed by the operator on 2026-10-19.** Milestone 1's issues, one repository part each, in their order:
1255 (`mesh-mail`'s account bucket), 1261 (`mail_send_status` answers only its caller), 1262 (the module), 1263
(sign-in), 1264 (accounts, folders and mails), 1265 (search), 1266 (a mail and its attachments), 1267 (writing a
mail), 1268 (the integration test). Milestone 2 is issue 806.
After milestone 1's delivery and before its live proof, four values are set, each on the operator's Approve:
`mesh-mail-web`'s `reach` on its machine (`internal`), then its assignment there, its `operators`, and its caller in
`mesh-mail`'s `grants` for every account.
+1
View File
@@ -58,6 +58,7 @@ document is written and this one's status becomes `implemented`.
| [`56-the-mail-client-set-up-by-the-mesh.md`](56-the-mail-client-set-up-by-the-mesh.md) | **Designed.** On every node with a display server, the `betterbird` module installs Betterbird from a pinned upstream release mirrored into the artifact store, ships the Owl add-on pinned and installed by Betterbird's enterprise policy, and declares the operator's mail accounts in a trusted setting without credentials; each account signs in once at a person's first use, and one the profile already holds is adopted by its address | ADR 0316, ADR 0317 |
| [`57-building-config-and-provisioning.md`](57-building-config-and-provisioning.md) | **Designed.** The work of ADR 0348 and ADR 0349 as 37 packages, each an issue with its proof: foundations, template files, config in its own module, the provisioner and its client, secrets in the vault, bindings from providers, collections, a module's runtime readers, and removing the old forms; at most five worked at once | ADR 0348, ADR 0349 |
| [`58-agent-work-as-the-meshs-own.md`](58-agent-work-as-the-meshs-own.md) | **Designed.** Agent work as the mesh's own: a thread seat keeps every thread whole on the bus; a thread handler runs the harness headless in a sandbox per session, the workspace on the thread's own forge branch; a workspace toolkit and named tools, no general command runner; one identity seat for persons, agents and modules, roles of grants, every tool an effect, signed bus logins, and the switch to them one identity at a time | ADR 0383, ADR 0384, ADR 0385, ADR 0386, ADR 0387, ADR 0388, ADR 0389, ADR 0390 |
| [`59-the-mail-web-client.md`](59-the-mail-web-client.md) | **In progress.** The operator's mail, every account, in a browser on the mesh's own machines: a module of its own, `mesh-mail-web`, holds `mail-webclient`, renders pages on the server and reads and sends only through `mail-store`'s verbs under a caller granted per account; its route reaches the private network only, the operator signs in with Keycloak, and a mail goes out only on the operator's Approve; then Betterbird is removed | ADR 0440, ADR 0378, ADR 0379 |
## Not yet written