From 66bb013b853d99c388c5cfcc1e1f928faa066d4a Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 19:02:56 +0200 Subject: [PATCH 1/4] Write milestones 2 to 4's issues into design 59, as the operator confirmed them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playbook 04 step 6. Also names ADR 0378 §5 beside ADR 0440 §4 for the page's use of mail-reading, its writes with correct and undo, and issue 1419 in §8's last sentence, as the graduation's review noted. --- 03-DESIGN/01-to-be/59-the-mail-web-client.md | 36 ++++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/03-DESIGN/01-to-be/59-the-mail-web-client.md b/03-DESIGN/01-to-be/59-the-mail-web-client.md index b0ead36d..d4d9da86 100644 --- a/03-DESIGN/01-to-be/59-the-mail-web-client.md +++ b/03-DESIGN/01-to-be/59-the-mail-web-client.md @@ -169,7 +169,8 @@ mail-store (mesh-mail) ── MAIL: mail..message.added ──▶ mesh- - **The page.** Above the classical list it shows the mail that matters at the top, the mail groups, and the scam warnings in a place of their own, each with why; the classical list (accounts, folders, search, one mail) stays and always opens. `mesh-mail-web` reads these through `mail-reading`'s verbs and still keeps no mail state - (ADR 0440 §4 as ADR 0459 §13 changes it). + (ADR 0378 §5 and ADR 0440 §4 as ADR 0459 §13 changes them); it writes the operator's corrections with + `correct` and `undo`. - **It fails loudly.** Its own health check is unhealthy when a judgement is refused, times out or answers out of shape, with the cause and since when; the controller raises its condition, the page shows the cause above the classical list instead of mail groups, and no mail signal is sent. On recovery the mail that arrived meanwhile is @@ -195,17 +196,16 @@ mail-store (mesh-mail) ── MAIL: mail..message.added ──▶ mesh- ## 8. The mail conversation (ADR 0462) -A mail conversation is a thread (ADR 0383), run by the thread handler (ADR 0384), with a mail context: the -framing that a mail is data, the tools `mail_search` and `mail_read` only under a caller granted reading only (a caller of the thread's own, -issue 1419), the -operator's real senders and folder names, and the reading lessons. The operator writes a mail question on the mail -page; each answer names the mails it found, and a question with no answer says so. `mesh-mail-web` creates and -follows the thread over the bus on its server side; the page loads one script from its own origin and talks only to -it (content security policy: script from that origin, `connect-src 'self'`), so the browser never reaches the bus. -The thread keeps the text of each mail it read for good, as every thread is kept (ADR 0383 §3), which the operator -accepted; mail reading keeps none. In the same milestone `mail_search` matches folder names without regard to case -and describes its three `-any` switches as true or false only. It is built after design 58's milestone 3 (threads, -issue 880). +A mail conversation is a thread (ADR 0383), run by the thread handler (ADR 0384), with a mail context: the framing +that a mail is data, the tools `mail_search` and `mail_read` only under a caller of the thread's own granted reading +only (issue 1419), the operator's real senders and folder names, and the reading lessons. The operator writes a mail +question on the mail page; each answer names the mails it found, and a question with no answer says so. +`mesh-mail-web` creates and follows the thread over the bus on its server side; the page loads one script from its +own origin and talks only to it (content security policy: script from that origin, `connect-src 'self'`), so the +browser never reaches the bus. The thread keeps the text of each mail it read for good, as every thread is kept (ADR +0383 §3), which the operator accepted; mail reading keeps none. In the same milestone `mail_search` matches folder +names without regard to case and describes its three `-any` switches as true or false only. It is built after design +58's milestone 3 (threads, issue 880) and once issue 1419's caller is decided. ## 9. What stays outside @@ -285,8 +285,16 @@ attachments), 1267 (writing a mail), 1268 (the integration test). After its deli the operator's Approve: `mesh-mail-web`'s `reach` on its machine (`internal`), its assignment there, its `operators`, and its caller in `mesh-mail`'s `grants` for every account. -Milestones 2 to 4 are written by research 066's graduation and wait for the operator's confirmation and their issues -(playbook 04, steps 5 and 6). Milestone 2 is one release of `mesh-catalog`: the grant split, the new seat, the new +**Milestones 2 to 4, confirmed by the operator on 2026-10-20.** Their issues, one repository part each, in their +order (each depends on the one before unless said): + +- Milestone 2: 1227 (`mesh-mail`'s grants split), 1426 (the seat `mail-reading` and the module `mesh-mail-reader`), + 1427 (batches, mail entries and mail groups), 1428 (reading lessons and the seat's verbs; with 1427 after 1426), + 1429 (mail signals; after 1427), 1430 (the mail page; after 1428 and 1429), 1431 (the integration test; after + 1430 and 1227), 1418 (`mesh-mail-notifier` unassigned; after 1431 and test 13 live). +- Milestone 3: 806 (Betterbird removed; after 1418). +- Milestone 4: 1419 (the thread's reading-only caller; a decision first), 1433 (`mail_search`'s folder case and + `-any` switches), 1432 (the mail conversation; after 1431, 1419 and design 58's milestone 3, issue 880). Milestone 2 is one release of `mesh-catalog`: the grant split, the new seat, the new module and the page change ship together, and `mesh-mail-reader` is granted reading only after that delivery, on the operator's Approve; `mesh-mail-notifier` is unassigned only after test 13 passes live. Milestone 3 is issue 806. ADR 0379 §2's bar for removing Betterbird is milestone 1's; keeping Betterbird until mail reading (milestone 2) works, -- 2.54.0 From e925a843ceb2cf9a14d01df8994a12428bff8fcb Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 19:08:13 +0200 Subject: [PATCH 2/4] Write the mail modules' as-is design, so what design 59's milestone 2 replaces is recorded before it is built Playbook 04 step 3: the one grants list that gives reading and sending at once, and mesh-mail-notifier's pop-up for every new mail, read from mesh-catalog main a67f0196 and the live mesh. --- 03-DESIGN/00-as-is/18-the-mail-modules.md | 177 ++++++++++++++++++++++ 03-DESIGN/00-as-is/README.md | 1 + 2 files changed, 178 insertions(+) create mode 100644 03-DESIGN/00-as-is/18-the-mail-modules.md diff --git a/03-DESIGN/00-as-is/18-the-mail-modules.md b/03-DESIGN/00-as-is/18-the-mail-modules.md new file mode 100644 index 00000000..250f0291 --- /dev/null +++ b/03-DESIGN/00-as-is/18-the-mail-modules.md @@ -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 built +from its source, `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) · two key-value buckets (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 built from `mesh-mail`'s own source, as `mesh-mail-notifier` is, and 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 built from the same source and 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.`, + `mail-webclient.`, `mail-notifier.`); `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-` — 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`). +- **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), `.`, 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` and `send.go`, 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 `.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, for + a mail already flagged read when it arrives, and for anything during an account's first sync or a resync + (`notifier.go`, `mailevent.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`**'s own tool `mail_notifier_health` is unhealthy once an account's notice has failed to + show for more than ten minutes. As built it answers this only when asked directly: its one process (`notify`) + is still declared with a plain unit health alone, because the catalogue's gate refuses a module judged only by + its own tool with no check of another kind beside it, and the module has only the one process — a gap its own + source names; a watching process of its own is tracked by issue 841, not yet decided (its `README.md` and + `health.go`). Design 59's milestone 2 retires the module (issue 1418). + +## What design 59 changes + +- **The one `grants` list splits in two.** Reading an account's mail and asking to send from it, today one + grant, become two the operator sets apart (design 59 §5; ADR 0460). +- **The notifier's pop-up retires.** `mesh-mail-notifier`'s desktop notice for every new inbox mail is replaced + by one mail signal per batch of mail reading, and it is unassigned from the laptop and the workstation once + that proves itself live (design 59 §7; ADR 0461). diff --git a/03-DESIGN/00-as-is/README.md b/03-DESIGN/00-as-is/README.md index a0b19fa9..1dc9ff84 100644 --- a/03-DESIGN/00-as-is/README.md +++ b/03-DESIGN/00-as-is/README.md @@ -26,6 +26,7 @@ Where the two disagree, the implementation wins and the disagreement is stated. | [`15-the-agent-and-its-licences.md`](15-the-agent-and-its-licences.md) | The operator's agent on every machine and the licence manager that keeps its licences | | [`16-logins-on-the-bus.md`](16-logins-on-the-bus.md) | The bus's one list of logins, a login per module assignment, persons limited to their tools, sessions calling through the tool runner's login, and no effect on any tool | | [`17-accounts-on-a-machine.md`](17-accounts-on-a-machine.md) | The operator and agent accounts on the node record, claude-code's own login and guard, the judgement that the agent account cannot become root, and the forge accounts agents push as | +| [`18-the-mail-modules.md`](18-the-mail-modules.md) | mesh-mail's accounts and MAIL stream, the one `grants` list that gives both reading and sending at once, the send path through its own process and the operator's Approve, mesh-mail-web's pages and sign-in, and mesh-mail-notifier's pop-up and its quiet rules | ## What these documents are not -- 2.54.0 From dcc6f185d499aa24be649b4b79ca5c417239067b Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 19:11:06 +0200 Subject: [PATCH 3/4] Answer the review: the mail modules are separate modules, the notifier has no process, nothing future in the as-is; 1433 depends on none --- 03-DESIGN/00-as-is/18-the-mail-modules.md | 47 ++++++++++---------- 03-DESIGN/01-to-be/59-the-mail-web-client.md | 7 ++- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/03-DESIGN/00-as-is/18-the-mail-modules.md b/03-DESIGN/00-as-is/18-the-mail-modules.md index 250f0291..906fd567 100644 --- a/03-DESIGN/00-as-is/18-the-mail-modules.md +++ b/03-DESIGN/00-as-is/18-the-mail-modules.md @@ -15,8 +15,8 @@ decisions: # 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 built -from its source, `mesh-mail-web`, is the mesh's one mail web client. A third, `mesh-mail-notifier`, shows a +**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 @@ -26,7 +26,7 @@ process of its own, which sends only once the operator approves it on Telegram.* ``` 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) · two key-value buckets (account state, newest mails) + 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 │ @@ -44,11 +44,14 @@ mesh-mail (mail-store, once for the mesh — the control-node) 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 built from `mesh-mail`'s own source, as `mesh-mail-notifier` is, and claims - `mail-webclient`, held once for the mesh. It is assigned to the same machine as `mesh-mail` and runs as its +- **`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 built from the same source and claims `mail-notifier`, held once per machine; a +- **`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 no 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. @@ -66,6 +69,8 @@ mesh-mail (mail-store, once for the mesh — the control-node) 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). @@ -83,7 +88,7 @@ mesh-mail (mail-store, once for the mesh — the control-node) - **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` and `send.go`, confirmed on `origin/main` at `a67f0196`). There is no + `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 @@ -142,9 +147,10 @@ mesh-mail (mail-store, once for the mesh — the control-node) - **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, for - a mail already flagged read when it arrives, and for anything during an account's first sync or a resync - (`notifier.go`, `mailevent.go`). +- **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 @@ -161,17 +167,10 @@ mesh-mail (mail-store, once for the mesh — the control-node) 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`**'s own tool `mail_notifier_health` is unhealthy once an account's notice has failed to - show for more than ten minutes. As built it answers this only when asked directly: its one process (`notify`) - is still declared with a plain unit health alone, because the catalogue's gate refuses a module judged only by - its own tool with no check of another kind beside it, and the module has only the one process — a gap its own - source names; a watching process of its own is tracked by issue 841, not yet decided (its `README.md` and - `health.go`). Design 59's milestone 2 retires the module (issue 1418). - -## What design 59 changes - -- **The one `grants` list splits in two.** Reading an account's mail and asking to send from it, today one - grant, become two the operator sets apart (design 59 §5; ADR 0460). -- **The notifier's pop-up retires.** `mesh-mail-notifier`'s desktop notice for every new inbox mail is replaced - by one mail signal per batch of mail reading, and it is unassigned from the laptop and the workstation once - that proves itself live (design 59 §7; ADR 0461). +- **`mesh-mail-notifier`** has no process of its own: its manifest declares only its account and its state + directory, and its bundle, loaded by the machine's tool runner, runs the watcher inside the runner's own + invocation (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`). diff --git a/03-DESIGN/01-to-be/59-the-mail-web-client.md b/03-DESIGN/01-to-be/59-the-mail-web-client.md index d4d9da86..46e5dd12 100644 --- a/03-DESIGN/01-to-be/59-the-mail-web-client.md +++ b/03-DESIGN/01-to-be/59-the-mail-web-client.md @@ -293,8 +293,11 @@ order (each depends on the one before unless said): 1429 (mail signals; after 1427), 1430 (the mail page; after 1428 and 1429), 1431 (the integration test; after 1430 and 1227), 1418 (`mesh-mail-notifier` unassigned; after 1431 and test 13 live). - Milestone 3: 806 (Betterbird removed; after 1418). -- Milestone 4: 1419 (the thread's reading-only caller; a decision first), 1433 (`mail_search`'s folder case and - `-any` switches), 1432 (the mail conversation; after 1431, 1419 and design 58's milestone 3, issue 880). Milestone 2 is one release of `mesh-catalog`: the grant split, the new seat, the new +- Milestone 4: 1433 (`mail_search`'s folder case and `-any` switches; depends on none), 1419 (the thread's + reading-only caller; a decision first, after design 58's milestone 3, issue 880), 1432 (the mail conversation; + after 1431, 1419 and issue 880). + +Milestone 2 is one release of `mesh-catalog`: the grant split, the new seat, the new module and the page change ship together, and `mesh-mail-reader` is granted reading only after that delivery, on the operator's Approve; `mesh-mail-notifier` is unassigned only after test 13 passes live. Milestone 3 is issue 806. ADR 0379 §2's bar for removing Betterbird is milestone 1's; keeping Betterbird until mail reading (milestone 2) works, -- 2.54.0 From d2dccddc27268452bd7a5312b32388df23c3f37c Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 19:12:04 +0200 Subject: [PATCH 4/4] Say the notifier's bundle is its own process under its own account, as main.go says --- 03-DESIGN/00-as-is/18-the-mail-modules.md | 9 +++++---- 03-DESIGN/01-to-be/59-the-mail-web-client.md | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/03-DESIGN/00-as-is/18-the-mail-modules.md b/03-DESIGN/00-as-is/18-the-mail-modules.md index 906fd567..38e6bac9 100644 --- a/03-DESIGN/00-as-is/18-the-mail-modules.md +++ b/03-DESIGN/00-as-is/18-the-mail-modules.md @@ -51,7 +51,7 @@ mesh-mail (mail-store, once for the mesh — the control-node) - **`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 no code but that copied file. It claims `mail-notifier`, held once per machine; a + 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. @@ -167,9 +167,10 @@ mesh-mail (mail-store, once for the mesh — the control-node) 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`** has no process of its own: its manifest declares only its account and its state - directory, and its bundle, loaded by the machine's tool runner, runs the watcher inside the runner's own - invocation (commit 12d78520, issue 859). Its tool `mail_notifier_health` is unhealthy once an account's notice +- **`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 diff --git a/03-DESIGN/01-to-be/59-the-mail-web-client.md b/03-DESIGN/01-to-be/59-the-mail-web-client.md index 46e5dd12..e6870a49 100644 --- a/03-DESIGN/01-to-be/59-the-mail-web-client.md +++ b/03-DESIGN/01-to-be/59-the-mail-web-client.md @@ -294,7 +294,7 @@ order (each depends on the one before unless said): 1430 and 1227), 1418 (`mesh-mail-notifier` unassigned; after 1431 and test 13 live). - Milestone 3: 806 (Betterbird removed; after 1418). - Milestone 4: 1433 (`mail_search`'s folder case and `-any` switches; depends on none), 1419 (the thread's - reading-only caller; a decision first, after design 58's milestone 3, issue 880), 1432 (the mail conversation; + reading-only caller; a decision first, after 1227 and design 58's milestone 3, issue 880), 1432 (the mail conversation; after 1431, 1419 and issue 880). Milestone 2 is one release of `mesh-catalog`: the grant split, the new seat, the new -- 2.54.0