Issue 710. Builds hq ADR 0359 (accepted, operator-approved): the new module modules/mesh-mail. Its stream and bus grants follow hq ADR 0363. It is assigned to no node; assigning it is the operator's step.
Delivery order
Merge and deliver this only after mesh-controller #338 (86057f45) and mesh-tools #34 (5d70eb3e) report delivered. Those two resolve issue 718: the controller creates module streams and composes their grants, and the runtime publishes and binds as the membership says (ADR 0363 §8). Before they are delivered, a manifest declaring a stream must not reach the nodes.
Before the operator grants any consumer, issues 727 and 728 must be fixed.
What
A Go module with one bundle and three processes:
prepare (run once): brings the index's schema to this version's. It uses numbered migrations, plus schema.sql for a fresh database.
keep (unit health): runs the imap adapter for each account in the setting accounts. It keeps IDLE on INBOX, ended and begun again every 2 minutes so each cycle proves the server still answers. It looks at every other folder every 5 minutes. EXAMINE and BODY.PEEK only. It indexes into its own database and keeps each attachment once, by its SHA-256, in its bucket. It writes every added, changed or removed message as an event in an outbox in the same transaction as the change.
watch (tool health mail_health, needs postgres-database): judges every minute and writes the verdict that the tool reads.
The bundle serves mail_accounts, mail_search, mail_read and mail_health. It says the outbox's events through the runtime (stdio.Emit) and removes each one once the bus took it.
ADR 0359, clause by clause
One module, an adapter per kind.Kinds = [imap]. Any other kind is refused by name. An account is {kind, address, user?, incoming{host,port,security}, outgoing?, folders?}. A second imap account (Gmail, ADR 0361) is one more config entry and one more secret mail-credential-<account>. It needs no code change and no manifest change, because the stream's events are one family pattern per change. outgoing is read and never used.
Watches each account and says each mail on its own stream. The manifest declares streams: [{name: mail, events: [mail.*.message.added|changed|removed], max-age-days: 7, grant: {config: grants, events: "mail.{key}.**"}}] (ADR 0363 §1-§4). The controller creates MAIL; the shared EVENTS stream never carries these events. They carry account, folder, uid, uidvalidity, message-id, in-reply-to, date, from, to, cc, subject, size, flags and attachment references (bucket, SHA-256 key, size, type, name). A changed event also carries the flags before and after. An event never carries a body or an attachment's bytes, and a test checks this. The first look at a folder indexes it without events.
Index and attachment store. The index is in its own database (postgres-database, name mail). Attachments are in the bucket from s3-bucket (minio), keyed by SHA-256 and written once. S3 SigV4 uses the standard library and is tested against AWS's published example signature.
Grants per account, governed by ADR 0363. The trusted config value grants (default {}: nobody) has an object per account listing a module or <machine>.<module>.
On the bus, a consumer hears an account's part of MAIL only when its consumes names mesh-mail and grants names it for that account (ADR 0363 §5). A withdrawal removes its consumer (§6).
In the tools, mail_search and mail_read enforce the same value on stdio.CallerOf, never on an argument. A person.<name> entry is honoured by the tools only; it grants nothing on the bus (§4).
Read-only, first stage. The client is used only through an interface without write commands. A test fails on any Store/Move/Copy/Expunge/Append/Create/Delete/Rename/... call or net/smtp in non-test code. The password is the own-secret family mail-credential-*, issued-by: outside.
Health.keep uses unit and watch uses the tool mail_health, as mesh-knowledge does. mail_health answers one of three:
waits, naming mail-credential-<account>, while no password is given;
unhealthy, naming the account and why, when the connection is down, the sign-in is refused, or the server has been silent for 5 minutes;
healthy otherwise.
A fault beside a wait is unhealthy and says both.
Nothing for Gmail, Graph or sending is built.
Tests and checks
go test -race passes. The tests cover:
A real IMAP conversation against go-imap's in-process server, over memory and over postgres:
the first look is silent;
a delivered mail is said through IDLE with metadata and an attachment reference, and no body;
the attachment is kept by SHA-256;
a flag is said as changed and an expunge as removed;
another folder is said at the poll;
the message is never marked \Seen.
Password states: not given (waits), wrong (refused sign-in), given later (picked up without a restart).
Grants through the real MCP loop with _meta callers: a granted caller reads, an ungranted or unproven one is refused, and an argument cannot name the caller.
Events published through the runtime: kept when the bus refuses, removed once taken.
Health states, the stale verdict, the schema (consolidated equals migrations), search, the manifest against the code (stream, grant, no emits), and the SigV4 vector.
With the controller built from mesh-controller main 86057f45:
module check says mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s. The check reads the stream: with grant.config pointed at a value that is not trusted, it refuses the manifest.
merge-check.sh exits 0, with resources without health at 62, as kept.
The branch is brought up to date by a merge of main, not a rebase. The PR adds github.com/emersion/go-imap/v2 v2.0.0-beta.8 and go-message v0.18.2.
What the operator does after merge (and after #338 and #34 are delivered)
Approve the config value accounts for mesh-mail: {"mesh": {"kind": "imap", "address": "jschoubben@novox.be", "incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"}}}. The controller refuses a domain in a manifest, so this is not a default.
Assign mesh-mail to novox.
Give the IMAP password at novox's desk: nox secret ask novox mesh-mail mail-credential-mesh. Until then module.mesh-mail.novox.needs-operator stands, as a warning.
Only after issues 727 and 728 are fixed, approve grants for each consumer.
Acceptance
Once assigned and given its password, a new mail to jschoubben@novox.be is on MAIL as mail.mesh.message.added within a minute, with metadata and no body. mail_search finds it. mail_read gives its body to a granted caller and refuses any other. Stopping the IMAP connection makes mail_health unhealthy within five minutes.
Issue 710. Builds hq ADR 0359 (accepted, operator-approved): the new module `modules/mesh-mail`. Its stream and bus grants follow hq ADR 0363. It is **assigned to no node**; assigning it is the operator's step.
## Delivery order
Merge and deliver this **only after** mesh-controller #338 (86057f45) and mesh-tools #34 (5d70eb3e) report delivered. Those two resolve issue 718: the controller creates module streams and composes their grants, and the runtime publishes and binds as the membership says (ADR 0363 §8). Before they are delivered, a manifest declaring a stream must not reach the nodes.
**Before the operator grants any consumer, issues 727 and 728 must be fixed.**
## What
A Go module with one bundle and three processes:
- `prepare` (run once): brings the index's schema to this version's. It uses numbered migrations, plus `schema.sql` for a fresh database.
- `keep` (unit health): runs the `imap` adapter for each account in the setting `accounts`. It keeps IDLE on INBOX, ended and begun again every 2 minutes so each cycle proves the server still answers. It looks at every other folder every 5 minutes. EXAMINE and BODY.PEEK only. It indexes into its own database and keeps each attachment once, by its SHA-256, in its bucket. It writes every added, changed or removed message as an event in an outbox in the same transaction as the change.
- `watch` (tool health `mail_health`, needs `postgres-database`): judges every minute and writes the verdict that the tool reads.
- The bundle serves `mail_accounts`, `mail_search`, `mail_read` and `mail_health`. It says the outbox's events through the runtime (`stdio.Emit`) and removes each one once the bus took it.
## ADR 0359, clause by clause
1. **One module, an adapter per kind.** `Kinds = [imap]`. Any other kind is refused by name. An account is `{kind, address, user?, incoming{host,port,security}, outgoing?, folders?}`. A second `imap` account (Gmail, ADR 0361) is one more config entry and one more secret `mail-credential-<account>`. It needs no code change and no manifest change, because the stream's events are one family pattern per change. `outgoing` is read and never used.
2. **Watches each account and says each mail on its own stream.** The manifest declares `streams: [{name: mail, events: [mail.*.message.added|changed|removed], max-age-days: 7, grant: {config: grants, events: "mail.{key}.**"}}]` (ADR 0363 §1-§4). The controller creates `MAIL`; the shared EVENTS stream never carries these events. They carry account, folder, uid, uidvalidity, message-id, in-reply-to, date, from, to, cc, subject, size, flags and attachment references (bucket, SHA-256 key, size, type, name). A changed event also carries the flags before and after. An event never carries a body or an attachment's bytes, and a test checks this. The first look at a folder indexes it without events.
3. **Index and attachment store.** The index is in its own database (`postgres-database`, name `mail`). Attachments are in the bucket from `s3-bucket` (minio), keyed by SHA-256 and written once. S3 SigV4 uses the standard library and is tested against AWS's published example signature.
4. **Grants per account, governed by ADR 0363.** The trusted config value `grants` (default `{}`: nobody) has an object per account listing a module or `<machine>.<module>`.
- On the bus, a consumer hears an account's part of `MAIL` only when its `consumes` names mesh-mail and `grants` names it for that account (ADR 0363 §5). A withdrawal removes its consumer (§6).
- In the tools, `mail_search` and `mail_read` enforce the same value on `stdio.CallerOf`, never on an argument. A `person.<name>` entry is honoured by the tools only; it grants nothing on the bus (§4).
5. **Read-only, first stage.** The client is used only through an interface without write commands. A test fails on any `Store/Move/Copy/Expunge/Append/Create/Delete/Rename/...` call or `net/smtp` in non-test code. The password is the own-secret family `mail-credential-*`, `issued-by: outside`.
6. **Health.** `keep` uses `unit` and `watch` uses the tool `mail_health`, as mesh-knowledge does. `mail_health` answers one of three:
- `waits`, naming `mail-credential-<account>`, while no password is given;
- `unhealthy`, naming the account and why, when the connection is down, the sign-in is refused, or the server has been silent for 5 minutes;
- `healthy` otherwise.
A fault beside a wait is unhealthy and says both.
7. Nothing for Gmail, Graph or sending is built.
## Tests and checks
`go test -race` passes. The tests cover:
- A real IMAP conversation against go-imap's in-process server, over memory and over postgres:
- the first look is silent;
- a delivered mail is said through IDLE with metadata and an attachment reference, and no body;
- the attachment is kept by SHA-256;
- a flag is said as changed and an expunge as removed;
- another folder is said at the poll;
- the message is never marked \Seen.
- Password states: not given (waits), wrong (refused sign-in), given later (picked up without a restart).
- Grants through the real MCP loop with `_meta` callers: a granted caller reads, an ungranted or unproven one is refused, and an argument cannot name the caller.
- Events published through the runtime: kept when the bus refuses, removed once taken.
- Health states, the stale verdict, the schema (consolidated equals migrations), search, the manifest against the code (stream, grant, no emits), and the SigV4 vector.
With the controller built from mesh-controller main 86057f45:
- `module check` says `mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s`. The check reads the stream: with `grant.config` pointed at a value that is not trusted, it refuses the manifest.
- `merge-check.sh` exits 0, with resources without health at 62, as kept.
The branch is brought up to date by a merge of main, not a rebase. The PR adds `github.com/emersion/go-imap/v2` v2.0.0-beta.8 and `go-message` v0.18.2.
## What the operator does after merge (and after #338 and #34 are delivered)
1. Approve the config value `accounts` for mesh-mail: `{"mesh": {"kind": "imap", "address": "jschoubben@novox.be", "incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"}}}`. The controller refuses a domain in a manifest, so this is not a default.
2. Assign mesh-mail to novox.
3. Give the IMAP password at novox's desk: `nox secret ask novox mesh-mail mail-credential-mesh`. Until then `module.mesh-mail.novox.needs-operator` stands, as a warning.
4. Only after issues 727 and 728 are fixed, approve `grants` for each consumer.
## Acceptance
Once assigned and given its password, a new mail to jschoubben@novox.be is on `MAIL` as `mail.mesh.message.added` within a minute, with metadata and no body. `mail_search` finds it. `mail_read` gives its body to a granted caller and refuses any other. Stopping the IMAP connection makes `mail_health` unhealthy within five minutes.
ADR 0359 decided one module holds the operator's mail accounts. This is its
first stage: an imap adapter that keeps IDLE on the inbox and polls the other
folders read-only, indexes each message in its own database, keeps each
attachment once by its SHA-256 in its bucket, and says every added, changed or
removed message as a metadata-only event. Bodies and attachments are served
only by its tools, to callers the trusted setting grants names. Its health
waits for the account's own secret until it is given at the desk (issue 710).
Deliverynovox/mesh-catalog@2130f9961291 — delivered since 2026-10-15T08:44:10Z
Delivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Transitions
2026-10-14 20:17 (new) → proposed (announced): novox/mesh-catalog#300's head announced
2026-10-15 08:43 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-15 08:43 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-15 08:44 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-15 08:44 published → delivered (done): nothing for a walk to move
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-catalog@2130f9961291` — **delivered** since 2026-10-15T08:44:10Z
**Delivery plan** — builds new: modules/mesh-mail, sent nowhere; no bus step
**Transitions**
- 2026-10-14 20:17 (new) → proposed (announced): novox/mesh-catalog#300's head announced
- 2026-10-15 08:43 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-15 08:43 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-15 08:44 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-15 08:44 published → delivered (done): nothing for a walk to move
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Merge check at 70c1110c
Judged: head 70c1110c5372 merged into trunk e22326240833; read beside it: mesh-controller 622fb1e9cbac, mesh-controller-main 622fb1e9cbac, mesh-host 1a2677882380, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on ace as build-1792001736329212314 (builds --log build-1792001736329212314).
--- the gate: new:modules/mesh-mail
mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-14T18:13:10Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-14T18:13:10Z
modules the change moves:
- mesh-mail (added)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 0 module(s) in 0 tier(s)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.002s
words: 12 retired words and 70 daemons' lines, none in the 47498 strings of 729 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.011s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.013s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 62, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.200s
testing modules/mesh-mail
ok mesh-mail/cmd/mesh-mail 2.962s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `70c1110c`
Judged: head `70c1110c5372` merged into trunk `e22326240833`; read beside it: mesh-controller `622fb1e9cbac`, mesh-controller-main `622fb1e9cbac`, mesh-host `1a2677882380`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on ace as `build-1792001736329212314` (`builds --log build-1792001736329212314`).
```
--- the gate: new:modules/mesh-mail
mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-14T18:13:10Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-14T18:13:10Z
modules the change moves:
- mesh-mail (added)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 0 module(s) in 0 tier(s)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.002s
words: 12 retired words and 70 daemons' lines, none in the 47498 strings of 729 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.011s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.013s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 62, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.200s
testing modules/mesh-mail
ok mesh-mail/cmd/mesh-mail 2.962s
--- its merge-check-typescript.sh (typescript toolchain)
```
mesh/merge-gate head 2130f9961291 merged into trunk c71c513027e5 — pass: builds new: modules/mesh-mail, sent nowhere; no bus step; every machine composes w…
mesh/repo-check head 2130f9961291 merged into trunk c71c513027e5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
ADR 0359 asked for a stream of its own and a bus grant from the module's
grants; ADR 0363 built both. The events leave the shared EVENTS stream, and
one family pattern per change replaces the literal per-account emits, so a
new account changes no manifest.
Merge check at 2130f996
Judged: head 2130f9961291 merged into trunk fadf5025cc5a; read beside it: mesh-controller 86057f455194, mesh-controller-main 86057f455194, mesh-host 1a2677882380, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on shanks as build-1792009116545584149 (builds --log build-1792009116545584149).
--- the gate: new:modules/mesh-mail
mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-14T20:16:51Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-14T20:16:51Z
modules the change moves:
- mesh-mail (added)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 0 module(s) in 0 tier(s)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 47641 strings of 731 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.015s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.013s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 62, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.215s
testing modules/mesh-mail
ok mesh-mail/cmd/mesh-mail 3.072s
--- its merge-check-typescript.sh (typescript toolchain)
Out of date when it landed: out of date: judged against trunk fadf5025cc, the trunk is now 2edef5a00ae0; judged again on a recheck or when it is merged
**Merge check** at `2130f996`
Judged: head `2130f9961291` merged into trunk `fadf5025cc5a`; read beside it: mesh-controller `86057f455194`, mesh-controller-main `86057f455194`, mesh-host `1a2677882380`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on shanks as `build-1792009116545584149` (`builds --log build-1792009116545584149`).
```
--- the gate: new:modules/mesh-mail
mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-14T20:16:51Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-14T20:16:51Z
modules the change moves:
- mesh-mail (added)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 0 module(s) in 0 tier(s)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 47641 strings of 731 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.015s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.013s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 62, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.215s
testing modules/mesh-mail
ok mesh-mail/cmd/mesh-mail 3.072s
--- its merge-check-typescript.sh (typescript toolchain)
```
**Out of date when it landed**: out of date: judged against trunk fadf5025cc5a, the trunk is now 2edef5a00ae0; judged again on a recheck or when it is merged
Merge check at 2130f996
Judged: head 2130f9961291 merged into trunk c71c513027e5; read beside it: mesh-controller 0b7e740b299c, mesh-controller-main 20e58907b0f8, mesh-host 198fd34ec497, mesh-tools 6b3e15042fee
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on novox as build-1792053596551454408 (builds --log build-1792053596551454408).
--- the gate: new:modules/mesh-mail
mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-15T01:09:40Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-15T01:09:40Z
modules the change moves:
- mesh-mail (added)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 0 module(s) in 0 tier(s)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 48140 strings of 737 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.016s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.015s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.007s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 62, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.319s
testing modules/mesh-mail
ok mesh-mail/cmd/mesh-mail 3.237s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `2130f996`
Judged: head `2130f9961291` merged into trunk `c71c513027e5`; read beside it: mesh-controller `0b7e740b299c`, mesh-controller-main `20e58907b0f8`, mesh-host `198fd34ec497`, mesh-tools `6b3e15042fee`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on novox as `build-1792053596551454408` (`builds --log build-1792053596551454408`).
```
--- the gate: new:modules/mesh-mail
mesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-15T01:09:40Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-15T01:09:40Z
modules the change moves:
- mesh-mail (added)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 0 module(s) in 0 tier(s)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 48140 strings of 737 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.016s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.015s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.007s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 62, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.319s
testing modules/mesh-mail
ok mesh-mail/cmd/mesh-mail 3.237s
--- its merge-check-typescript.sh (typescript toolchain)
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Issue 710. Builds hq ADR 0359 (accepted, operator-approved): the new module
modules/mesh-mail. Its stream and bus grants follow hq ADR 0363. It is assigned to no node; assigning it is the operator's step.Delivery order
Merge and deliver this only after mesh-controller #338 (86057f45) and mesh-tools #34 (5d70eb3e) report delivered. Those two resolve issue 718: the controller creates module streams and composes their grants, and the runtime publishes and binds as the membership says (ADR 0363 §8). Before they are delivered, a manifest declaring a stream must not reach the nodes.
Before the operator grants any consumer, issues 727 and 728 must be fixed.
What
A Go module with one bundle and three processes:
prepare(run once): brings the index's schema to this version's. It uses numbered migrations, plusschema.sqlfor a fresh database.keep(unit health): runs theimapadapter for each account in the settingaccounts. It keeps IDLE on INBOX, ended and begun again every 2 minutes so each cycle proves the server still answers. It looks at every other folder every 5 minutes. EXAMINE and BODY.PEEK only. It indexes into its own database and keeps each attachment once, by its SHA-256, in its bucket. It writes every added, changed or removed message as an event in an outbox in the same transaction as the change.watch(tool healthmail_health, needspostgres-database): judges every minute and writes the verdict that the tool reads.mail_accounts,mail_search,mail_readandmail_health. It says the outbox's events through the runtime (stdio.Emit) and removes each one once the bus took it.ADR 0359, clause by clause
Kinds = [imap]. Any other kind is refused by name. An account is{kind, address, user?, incoming{host,port,security}, outgoing?, folders?}. A secondimapaccount (Gmail, ADR 0361) is one more config entry and one more secretmail-credential-<account>. It needs no code change and no manifest change, because the stream's events are one family pattern per change.outgoingis read and never used.streams: [{name: mail, events: [mail.*.message.added|changed|removed], max-age-days: 7, grant: {config: grants, events: "mail.{key}.**"}}](ADR 0363 §1-§4). The controller createsMAIL; the shared EVENTS stream never carries these events. They carry account, folder, uid, uidvalidity, message-id, in-reply-to, date, from, to, cc, subject, size, flags and attachment references (bucket, SHA-256 key, size, type, name). A changed event also carries the flags before and after. An event never carries a body or an attachment's bytes, and a test checks this. The first look at a folder indexes it without events.postgres-database, namemail). Attachments are in the bucket froms3-bucket(minio), keyed by SHA-256 and written once. S3 SigV4 uses the standard library and is tested against AWS's published example signature.grants(default{}: nobody) has an object per account listing a module or<machine>.<module>.MAILonly when itsconsumesnames mesh-mail andgrantsnames it for that account (ADR 0363 §5). A withdrawal removes its consumer (§6).mail_searchandmail_readenforce the same value onstdio.CallerOf, never on an argument. Aperson.<name>entry is honoured by the tools only; it grants nothing on the bus (§4).Store/Move/Copy/Expunge/Append/Create/Delete/Rename/...call ornet/smtpin non-test code. The password is the own-secret familymail-credential-*,issued-by: outside.keepusesunitandwatchuses the toolmail_health, as mesh-knowledge does.mail_healthanswers one of three:waits, namingmail-credential-<account>, while no password is given;unhealthy, naming the account and why, when the connection is down, the sign-in is refused, or the server has been silent for 5 minutes;healthyotherwise.A fault beside a wait is unhealthy and says both.
Tests and checks
go test -racepasses. The tests cover:_metacallers: a granted caller reads, an ungranted or unproven one is refused, and an argument cannot name the caller.With the controller built from mesh-controller main 86057f45:
module checksaysmesh-mail: ok, 4 tool(s), ready: keep by unit every 30s; watch by its tool mail_health every 1m0s. The check reads the stream: withgrant.configpointed at a value that is not trusted, it refuses the manifest.merge-check.shexits 0, with resources without health at 62, as kept.The branch is brought up to date by a merge of main, not a rebase. The PR adds
github.com/emersion/go-imap/v2v2.0.0-beta.8 andgo-messagev0.18.2.What the operator does after merge (and after #338 and #34 are delivered)
accountsfor mesh-mail:{"mesh": {"kind": "imap", "address": "jschoubben@novox.be", "incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"}}}. The controller refuses a domain in a manifest, so this is not a default.nox secret ask novox mesh-mail mail-credential-mesh. Until thenmodule.mesh-mail.novox.needs-operatorstands, as a warning.grantsfor each consumer.Acceptance
Once assigned and given its password, a new mail to jschoubben@novox.be is on
MAILasmail.mesh.message.addedwithin a minute, with metadata and no body.mail_searchfinds it.mail_readgives its body to a granted caller and refuses any other. Stopping the IMAP connection makesmail_healthunhealthy within five minutes.Delivery
novox/mesh-catalog@2130f9961291— delivered since 2026-10-15T08:44:10ZDelivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
70c1110cJudged: head
70c1110c5372merged into trunke22326240833; read beside it: mesh-controller622fb1e9cbac, mesh-controller-main622fb1e9cbac, mesh-host1a2677882380, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on ace as
build-1792001736329212314(builds --log build-1792001736329212314).Merge check at
2130f996Judged: head
2130f9961291merged into trunkfadf5025cc5a; read beside it: mesh-controller86057f455194, mesh-controller-main86057f455194, mesh-host1a2677882380, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on shanks as
build-1792009116545584149(builds --log build-1792009116545584149).Out of date when it landed: out of date: judged against trunk
fadf5025cc, the trunk is now 2edef5a00ae0; judged again on a recheck or when it is mergedMerge check at
2130f996Judged: head
2130f9961291merged into trunkc71c513027e5; read beside it: mesh-controller0b7e740b299c, mesh-controller-main20e58907b0f8, mesh-host198fd34ec497, mesh-tools6b3e15042feemesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: new:modules/mesh-mail)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds new: modules/mesh-mail, sent nowhere; no bus step
Run by the build seat on novox as
build-1792053596551454408(builds --log build-1792053596551454408).