|
|
|
@@ -0,0 +1,177 @@
|
|
|
|
|
---
|
|
|
|
|
layer: as-is
|
|
|
|
|
status: implemented
|
|
|
|
|
code: [mesh-catalog modules/mesh-mail, mesh-catalog modules/mesh-mail-web, mesh-catalog modules/mesh-mail-notifier]
|
|
|
|
|
updated: 2026-10-20
|
|
|
|
|
decisions:
|
|
|
|
|
- "ADR 0359"
|
|
|
|
|
- "ADR 0361"
|
|
|
|
|
- "ADR 0378"
|
|
|
|
|
- "ADR 0379"
|
|
|
|
|
- "ADR 0436"
|
|
|
|
|
- "ADR 0439"
|
|
|
|
|
- "ADR 0440"
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# The mail modules, as they run
|
|
|
|
|
|
|
|
|
|
**One module, `mesh-mail`, holds every mail account and is the mesh's one `mail-store`. A second module,
|
|
|
|
|
`mesh-mail-web`, is the mesh's one mail web client. A third, `mesh-mail-notifier`, shows a
|
|
|
|
|
desktop notice of new mail on the machines it is assigned to. A caller may read an account's mail, or ask to
|
|
|
|
|
send from it, only when `mesh-mail`'s trusted setting `grants` names it for that account — one list per
|
|
|
|
|
account, giving both at once, to the same callers. A send is never performed by a tool: it is queued for a
|
|
|
|
|
process of its own, which sends only once the operator approves it on Telegram.** Read from `mesh-catalog` on
|
|
|
|
|
`origin/main` at `a67f0196` (2026-10-20) and from the live mesh through the mesh MCP server on 2026-10-20.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
mesh-mail (mail-store, once for the mesh — the control-node)
|
|
|
|
|
accounts search read send send-status ── MAIL stream (headers only, 7 days) ──▶ a granted caller
|
|
|
|
|
own database (index) · own object-store bucket (attachments) · three key-value buckets (sends, account state, newest mails)
|
|
|
|
|
process keep (IDLE + index) · process watch (mail_health) · process send (mesh-mail-send account; holds the passwords)
|
|
|
|
|
▲ grants: one list per account, read AND send alike
|
|
|
|
|
│
|
|
|
|
|
┌────┴─────────────────────────────┬───────────────────────────────┐
|
|
|
|
|
mesh-mail-notifier (mail-notifier, mesh-mail-web (mail-webclient,
|
|
|
|
|
once per machine — laptop, work- once for the mesh — the control-node)
|
|
|
|
|
station) sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve (Telegram)
|
|
|
|
|
node-notifier.send on new inbox mail
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 1. The modules and their seats
|
|
|
|
|
|
|
|
|
|
- **`mesh-mail`** claims `mail-store`, held once for the mesh (`accounts`, `search`, `read`, `send`,
|
|
|
|
|
`send-status`). It is assigned today to one machine, the control-node (`module.json`; confirmed live,
|
|
|
|
|
2026-10-20). ADR 0378 §4 decided to move it to the home server as a step of its own, on the operator's
|
|
|
|
|
Approve (issue 803); as built, that move has not happened: the live mesh still runs it, and `mesh-mail-web`
|
|
|
|
|
beside it, on the control-node.
|
|
|
|
|
- **`mesh-mail-web`** is a module of its own, with its own Go module and source under `modules/mesh-mail-web`;
|
|
|
|
|
it claims `mail-webclient`, held once for the mesh. It is assigned to the same machine as `mesh-mail` and runs as its
|
|
|
|
|
own account, `mesh-mail-web`. Its manifest declares no database and no store of its own: nothing beyond its
|
|
|
|
|
route, its sign-in binding and its own settings on disk.
|
|
|
|
|
- **`mesh-mail-notifier`** is a module of its own as well (`modules/mesh-mail-notifier`), and keeps a
|
|
|
|
|
byte-identical copy of `mesh-mail`'s `mailevent.go`, held to it by its `copies_test.go`, because each module
|
|
|
|
|
builds alone. ADR 0378 §9 and ADR 0440 §1 say both are "built from `mesh-mail`'s source"; as built, they are
|
|
|
|
|
separate modules that share none of `mesh-mail`'s own code but that copied file. It claims `mail-notifier`, held once per machine; a
|
|
|
|
|
module claiming it needs `node-notifier` held on the same machine (ADR 0378 §7). It is assigned today to two
|
|
|
|
|
machines, the laptop and the workstation (live read of each machine's modules, 2026-10-20); the control-node
|
|
|
|
|
and the home server hold no instance of it.
|
|
|
|
|
- None of the three seats is named `mesh-*`. A caller reaches them by seat (`mail-store.<verb>`,
|
|
|
|
|
`mail-webclient.<verb>`, `mail-notifier.<verb>`); `mesh-mail` also answers its own tool names (`mail_accounts`
|
|
|
|
|
and the rest) through the same handlers.
|
|
|
|
|
|
|
|
|
|
## 2. Accounts and the MAIL stream
|
|
|
|
|
|
|
|
|
|
- **Three accounts**, all of the kind `imap`, each a password-based sign-in given at the desk as the module's
|
|
|
|
|
own secret `mail-credential-<account>` — one against the mesh's own mail server, two against outside
|
|
|
|
|
providers (ADR 0359 §5, ADR 0361 §1; `mesh-controller.settings` for `mesh-mail`, read live on 2026-10-20). No
|
|
|
|
|
account's name or address is repeated in this document.
|
|
|
|
|
- **It watches each account and says each mail on the bus.** `mesh-mail` keeps IMAP IDLE on the inbox and polls
|
|
|
|
|
the other folders; for each new, changed or removed message it publishes an event on its own stream `MAIL` —
|
|
|
|
|
account, folder, message id, date, from, to, subject, size, flags and attachment references, never a body or
|
|
|
|
|
an attachment's bytes (ADR 0359 §2). The stream keeps 7 days (`module.json`).
|
|
|
|
|
- **A send's own record** lives in its state bucket `sends`, which only the `send` process and the tools write
|
|
|
|
|
(`module.json`; ADR 0436 §8).
|
|
|
|
|
- **The index and the attachment store are its own.** A row per message and folder lives in its own Postgres
|
|
|
|
|
database; attachments live in an object-store bucket, each kept once by its SHA-256 digest (ADR 0359 §3, ADR
|
|
|
|
|
0439 §5-§6).
|
|
|
|
|
- **Two more buckets answer without the database.** An account bucket holds each account's state and, per
|
|
|
|
|
folder, its name and its mail and unread counts; a mail bucket holds the newest mails of each account (up to
|
|
|
|
|
200, fewer once there are more than 81 accounts), for a view that need not ask the database (ADR 0439 §2-§4).
|
|
|
|
|
`mesh-mail-web`'s accounts page reads the folders from this bucket — a read wider than any one caller's grant
|
|
|
|
|
— but only for an account its own grant already let it ask about (ADR 0440 §4).
|
|
|
|
|
|
|
|
|
|
## 3. `grants` — one list per account, giving both at once
|
|
|
|
|
|
|
|
|
|
- **The setting.** `mesh-mail`'s trusted setting `grants` names, per account, the callers that may reach it: a
|
|
|
|
|
module's name alone (granted wherever it runs), `<machine>.<module>`, or a person. Nobody is granted an
|
|
|
|
|
account by default (ADR 0359 §4).
|
|
|
|
|
- **One check, every door.** The code that decides whether a caller may read an account's mail (`mail_search`,
|
|
|
|
|
`mail_read`, the account's share of `MAIL`) and the code that decides whether it may ask to send from it
|
|
|
|
|
(`mail_send`) is the same function, reading the one list (`modules/mesh-mail/cmd/mesh-mail/config.go`'s
|
|
|
|
|
`Grants.Allows`, called from `tools.go` for every tool, `mail_send` included, confirmed on `origin/main` at `a67f0196`). There is no
|
|
|
|
|
second list and no narrower grant for sending alone: a caller named for an account may do both, and a caller
|
|
|
|
|
left out of it may do neither.
|
|
|
|
|
- **Who is granted, today, for every account alike** (`mesh-controller.settings` for `mesh-mail`, read live on
|
|
|
|
|
2026-10-20): `mesh-mail-notifier` (named alone, so wherever it is assigned it may read); each of the four
|
|
|
|
|
machines' own `node-tools` — the control-node's, the laptop's, the workstation's and the home server's, which
|
|
|
|
|
is how an agent session on any of them reaches a mail account at all; and the control-node's `mesh-mail-web`.
|
|
|
|
|
So the notifier, every machine's agent sessions, and the web client can each both read an account's mail and
|
|
|
|
|
ask to send from it, from the one list.
|
|
|
|
|
|
|
|
|
|
## 4. The send path
|
|
|
|
|
|
|
|
|
|
- **A send is an ask, never a write by the tool.** `mail_send` (account, to, subject, body) is open only to a
|
|
|
|
|
caller the account's `grants` names. It performs nothing itself: it raises an ask on the operator channel
|
|
|
|
|
naming the account, the recipients, the subject and the first lines, and returns the ask's id at once (ADR
|
|
|
|
|
0361 §2).
|
|
|
|
|
- **The operator's Approve queues it once.** On an Approve recorded on a channel that proves who answered,
|
|
|
|
|
`mesh-mail`'s tools record the send `approved` and publish it, once, on the work queue `mail-sends`, carrying
|
|
|
|
|
only the send's id, its account and its ask id — never the recipients, subject or body, which stay in the
|
|
|
|
|
send's own record (ADR 0436 §8).
|
|
|
|
|
- **A process of its own performs it.** `mesh-mail`'s process `send` runs as its own account, `mesh-mail-send`,
|
|
|
|
|
never the tool runner's account: it alone reads the accounts' passwords. It takes work from the queue,
|
|
|
|
|
verifies the operator's warrant again against the router's own record rather than trusting the queued message,
|
|
|
|
|
claims the send once, hands it to the account's outgoing server, and records the outcome — retrying a send the
|
|
|
|
|
server provably never took, up to five times, each after a longer wait, before giving up (ADR 0436 §2, §7,
|
|
|
|
|
§9-§10).
|
|
|
|
|
- **`mail_send_status` answers one of a fixed set of states**: waiting for the operator, approved and waiting
|
|
|
|
|
for the sender, being sent, waiting for its next attempt, sent, failed (with its reason), refused, not proven
|
|
|
|
|
(the handover may or may not have reached the server), declined, expired, or unknown for an id it does not
|
|
|
|
|
know or that belongs to another caller's send (`send.go`, `sender.go`; ADR 0436 §12).
|
|
|
|
|
|
|
|
|
|
## 5. `mesh-mail-web`
|
|
|
|
|
|
|
|
|
|
- **Its pages**, all behind sign-in: the accounts page (`/`), a folder (`/folder`), search (`/search`), one
|
|
|
|
|
mail (`/mail`), an attachment download (`/attachment`), writing a mail (`/write`), its send (`/send`) and its
|
|
|
|
|
state (`/send-status`) — plus the sign-in callback (`/signed-in`). Every changing form (today, the send)
|
|
|
|
|
carries a token bound to the session (`server.go`, `csrf.go`).
|
|
|
|
|
- **Sign-in is Keycloak's.** The module requires `oidc-client` and uses the authorisation-code flow with PKCE;
|
|
|
|
|
the identity (the ID token's own username claim) must be in the trusted setting `operators` — nobody by
|
|
|
|
|
default, and today one identity, the operator's own (ADR 0440 §3; `mesh-controller.settings` for
|
|
|
|
|
`mesh-mail-web`, read live on 2026-10-20). The session cookie is `Secure`, `HttpOnly` and `SameSite=Strict`,
|
|
|
|
|
ending 12 hours after sign-in (`auth.go`).
|
|
|
|
|
- **Reachable only from the mesh's own machines.** Its route's reach is set to `internal`, so the route proxy
|
|
|
|
|
composes only its internal name, never a public one, and the module itself answers any other host 404 (ADR
|
|
|
|
|
0440 §2; `server.go`).
|
|
|
|
|
- **A mail is shown as text.** `mail_read`'s own HTML-to-text is rendered through `html/template`'s escaping,
|
|
|
|
|
never parsed as markup; an attachment is answered only as a download, `Content-Disposition: attachment` and
|
|
|
|
|
`X-Content-Type-Options: nosniff`; every page carries a content security policy allowing nothing from
|
|
|
|
|
elsewhere (ADR 0440 §5; `mail.go`).
|
|
|
|
|
- **It writes nothing of its own.** No database, nothing on disk beyond its own settings; it reaches mail only
|
|
|
|
|
through `mail-store`'s verbs, calling as `<machine>.mesh-mail-web`, and reads one thing directly besides —
|
|
|
|
|
`mesh-mail`'s account bucket, for folders and counts, of an account its own grant already let it ask about
|
|
|
|
|
(ADR 0440 §4, §6).
|
|
|
|
|
|
|
|
|
|
## 6. `mesh-mail-notifier`
|
|
|
|
|
|
|
|
|
|
- **One notice per new inbox mail, at most once.** For each account it is granted, on hearing `mesh-mail`'s
|
|
|
|
|
`added` event, it shows the sender (name and address), the subject and the account's name through the
|
|
|
|
|
machine's `node-notifier`, under its own account (`runs-as`), never the tool runner's.
|
|
|
|
|
- **Quiet rules.** It shows nothing for a mail outside the inbox, for a mail from the account's own address, and
|
|
|
|
|
for a mail already flagged read when it arrives (`notifier.go`, `mailevent.go`). During an account's first sync
|
|
|
|
|
or a resync `mesh-mail` publishes no `added` events, so nothing reaches the notifier then (`notifier.go`'s
|
|
|
|
|
opening note, citing `mesh-mail`'s `imap.go`).
|
|
|
|
|
- **Folding.** More than three qualifying mails of one account within a trailing 60-second window become one
|
|
|
|
|
notice, shown once the window has passed since the fold's own first mail, naming how many and the latest
|
|
|
|
|
sender; a message already shown — tracked by its message id, or by its folder, UIDVALIDITY and UID where it
|
|
|
|
|
has none — is never shown twice, even redelivered. A notice that fails to send is retried at most once a
|
|
|
|
|
minute (`notifier.go`, its constants `FoldOver`, `Window`, `RetryBackoff`).
|
|
|
|
|
- **Assignment.** It is assigned today to the laptop and the workstation (live read, 2026-10-20), where it needs
|
|
|
|
|
`node-notifier` to be held beneath it.
|
|
|
|
|
|
|
|
|
|
## 7. Health
|
|
|
|
|
|
|
|
|
|
- **`mesh-mail`**'s liveness is its `keep` process's own unit health; its watching is judged once a minute by
|
|
|
|
|
its `watch` process and read through the tool `mail_health`: `waits` (naming the secret) for an account whose
|
|
|
|
|
password was never given, `unhealthy` (naming the account and why) for a connection down or an IDLE silent
|
|
|
|
|
for five minutes, healthy otherwise; its `send` process is a plain unit (ADR 0359 §6; `health.go`).
|
|
|
|
|
- **`mesh-mail-web`**'s own tool `mail_web_health` reads the one state its HTTP endpoint keeps: still setting up
|
|
|
|
|
sign-in, listening, or ended with its error — unhealthy whenever it is not listening (`main.go`).
|
|
|
|
|
- **`mesh-mail-notifier`** declares no process resource: its manifest declares only its account and its state
|
|
|
|
|
directory. Its bundle is the one process the machine's runtime launches for it and keeps alive, under its own
|
|
|
|
|
account (`runs-as`); it serves its tools and runs the watcher beside them (`main.go`'s opening note; commit
|
|
|
|
|
12d78520, issue 859). Its tool `mail_notifier_health` is unhealthy once an account's notice
|
|
|
|
|
has failed to show for more than ten minutes, and answers only when asked; no check of the node-engine judges
|
|
|
|
|
the module by it. A watching process of its own is tracked by issue 841, not yet decided (its `README.md`).
|
|
|
|
|
The comment in its `health.go` that still names a process `notify` predates that commit. It keeps one state
|
|
|
|
|
bucket, `shown`, for 30 days, to show no mail twice (`module.json`).
|