mesh-mail: say mail events on its own stream MAIL, granted per account by grants (hq ADR 0363)
mesh/delivery checking
mesh/merge-gate out of date: judged against trunk fadf5025cc5a, the trunk is now 2edef5a00ae0; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk fadf5025cc5a, the trunk is now 2edef5a00ae0; judged again on a recheck or when it is merged

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.
This commit is contained in:
jochen
2026-10-14 22:17:09 +02:00
parent ed5690cee4
commit 2130f99612
3 changed files with 40 additions and 15 deletions
+5 -2
View File
@@ -14,7 +14,9 @@ This is the first stage: accounts of the kind `imap`, read-only. Nothing here fl
(`s3-bucket`, from `minio`).
- **Says each change.** A message added, its flags changed, or removed is said as the event
`mail.<account>.message.<added|changed|removed>`: account, folder, uid, message id, date, sender, recipients,
subject, size, flags and attachment references; never the body, never an attachment's bytes. The first look at a
subject, size, flags and attachment references; never the body, never an attachment's bytes. The events go to the
module's own stream `MAIL`, kept seven days, never to the shared EVENTS stream (novox/hq ADR 0363); a module hears an
account's part only when its `consumes` names mesh-mail and the setting `grants` names it for that account. The first look at a
folder indexes what is there and says nothing: the bus hears what changed since the watch began. The keeper writes
each event in the same step as the change it says; the bundle says it through the runtime and removes it once the
bus took it.
@@ -34,7 +36,8 @@ This is the first stage: accounts of the kind `imap`, read-only. Nothing here fl
more password; no code changes. `user` signs in under another name than the address.
`grants` — per account, who may read its mail: a module's name (on every machine), `<machine>.<module>`, or
`person.<name>`. Nobody by default.
`person.<name>`. Nobody by default. It restricts the stream `MAIL` on the bus (a person's entry grants nothing there)
and the tools `mail_search` and `mail_read`.
The password of an account is the own secret `mail-credential-<account>`, given at the desk; the mesh never makes one.
@@ -21,11 +21,20 @@ type manifest struct {
OwnSecrets map[string]map[string]string `json:"own-secrets"`
Tools []string `json:"tools"`
Emits []string `json:"emits"`
Consumes []string `json:"consumes"`
Invokes []string `json:"invokes"`
Claims []json.RawMessage `json:"claims"`
Resources []map[string]any `json:"resources"`
Build struct {
Streams []struct {
Name string `json:"name"`
Events []string `json:"events"`
MaxAgeDays int `json:"max-age-days"`
Grant struct {
Config string `json:"config"`
Events string `json:"events"`
} `json:"grant"`
} `json:"streams"`
Consumes []string `json:"consumes"`
Invokes []string `json:"invokes"`
Claims []json.RawMessage `json:"claims"`
Resources []map[string]any `json:"resources"`
Build struct {
Artifacts []struct {
From string `json:"from"`
Binary string `json:"binary"`
@@ -72,12 +81,15 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
if !reflect.DeepEqual(m.Tools, names) || !reflect.DeepEqual(names, []string{"mail_accounts", "mail_search", "mail_read", "mail_health"}) {
t.Fatalf("tools %v, the code serves %v", m.Tools, names)
}
var emits []string
// Its events go to its own stream MAIL, kept a week, each account's part heard only by the consumers the trusted
// config value grants names for it (ADR 0359 §2, §4; ADR 0363): none on the shared EVENTS stream.
var events []string
for _, c := range Changes {
emits = append(emits, EventKey("mesh", c))
events = append(events, EventKey("*", c))
}
if !reflect.DeepEqual(m.Emits, emits) {
t.Fatalf("emits %v, the code says %v for the account mesh", m.Emits, emits)
if len(m.Emits) != 0 || len(m.Streams) != 1 || m.Streams[0].Name != "mail" || !reflect.DeepEqual(m.Streams[0].Events, events) ||
m.Streams[0].MaxAgeDays != 7 || m.Streams[0].Grant.Config != "grants" || m.Streams[0].Grant.Events != "mail.{key}.**" {
t.Fatalf("emits %v, streams %+v; the code says %v", m.Emits, m.Streams, events)
}
// It reads its own mail and calls nothing, consumes nothing and claims no seat.
if len(m.Consumes)+len(m.Invokes)+len(m.Claims) != 0 {
+14 -4
View File
@@ -31,10 +31,20 @@
"mail_read",
"mail_health"
],
"emits": [
"mail.mesh.message.added",
"mail.mesh.message.changed",
"mail.mesh.message.removed"
"streams": [
{
"name": "mail",
"events": [
"mail.*.message.added",
"mail.*.message.changed",
"mail.*.message.removed"
],
"max-age-days": 7,
"grant": {
"config": "grants",
"events": "mail.{key}.**"
}
}
],
"resources": [
{