|
|
|
@@ -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.
|