package main // mail_send raises its ask on the operator channel, and messenger's content rule (ADR 0253 §3) refuses any // message that carries a mail address, a URL or a bare host name. ADR 0361 §2 needs the ask to show the // account, the recipients, the subject and the first lines regardless — so the ask must say all of that in // a form the content rule lets through (novox/hq issue 868, "Decided" note and review round 1). These tests // build real asks through mail_send and judge them with the real rule, copied verbatim as content_rule.go. import ( "encoding/json" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/asks" ) // wholeAsk is every word the operator can ever see for one ask: its Headline, Explanation and Details // together. Useful for a readability check (nothing unmasked is visible anywhere), but never for judging // whether messenger would refuse the ask as a whole — judgedWords is that (novox/hq issue 868, review // round 1): Details is filtered line by line, not judged with the rest (holder.go's sayOn). func wholeAsk(r *sendRouter, id string) string { a := r.published[id] return a.Headline + "\n" + a.Explanation + "\n" + a.Details } // judgedWords is exactly the text messenger refuses (or lets through) an ask as a whole on (asks.go's // TakeAsk): its headline, its explanation, what happens at expiry, and each option's label and effect. // Details is never part of it. func judgedWords(a asks.Ask) string { words := []string{a.Headline, a.Explanation, a.OnExpiry} for _, o := range a.Options { words = append(words, o.Label, o.Does) } return strings.Join(words, "\n") } // mustNotBeRefused fails the test naming what the content rule refused, and why — judged the way messenger // itself judges an ask as a whole, on judgedWords alone. func mustNotBeRefused(t *testing.T, a asks.Ask) { t.Helper() if refusal, ok := Check(judgedWords(a)); !ok { t.Fatalf("the content rule refuses the ask: %s\n%s", refusal, judgedWords(a)) } } // (1) A recipient that is one of the operator's own mesh-mail accounts (sendTestBundle's "mesh", address // jschoubben@novox.be) is named by the account's name alone: the ask carries no mail address at all, and // passes the real content rule. This is the first test issue 868's decision asked for; it fails today // because AskMailSend puts the raw address into the ask. func TestMailSendAskForOwnAccountRecipientCarriesNoAddress(t *testing.T) { b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"}) rt := startBundle(t, b.Tools()) args := map[string]any{"account": "mesh", "to": []any{"jschoubben@novox.be"}, "subject": "Testing issue 868", "body": "Does the ask reach the operator now?"} text, isErr := rt.call(SendTool, "novox.triage", args) if isErr { t.Fatalf("%s", text) } var out map[string]any if json.Unmarshal([]byte(text), &out) != nil { t.Fatalf("not JSON: %s", text) } id, _ := out["ask"].(string) if id == "" { t.Fatalf("%s", text) } whole := wholeAsk(router, id) if strings.Contains(whole, "@") { t.Errorf("the ask carries a mail address for its own account recipient:\n%s", whole) } if !strings.Contains(whole, "your account mesh") { t.Errorf("the ask does not name the own account:\n%s", whole) } mustNotBeRefused(t, router.published[id]) } // (2) A recipient outside the operator's own accounts is said without its address or host: its first // letter, an ellipsis and the domain's labels as words. func TestMailSendAskForOutsideRecipientCarriesNoAddress(t *testing.T) { b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"}) rt := startBundle(t, b.Tools()) text, isErr := rt.call(SendTool, "novox.triage", sendArgs) // to: ops@example.org, lead@example.org if isErr { t.Fatalf("%s", text) } var out map[string]any _ = json.Unmarshal([]byte(text), &out) id := out["ask"].(string) whole := wholeAsk(router, id) if strings.Contains(whole, "@") || strings.Contains(whole, "example.org") { t.Errorf("the ask carries the recipients' address or host:\n%s", whole) } for _, want := range []string{"o… at example org", "l… at example org"} { if !strings.Contains(whole, want) { t.Errorf("the ask does not say %q:\n%s", want, whole) } } mustNotBeRefused(t, router.published[id]) } // (3) More than three recipients are counted rather than said one by one. func TestMailSendAskForManyRecipientsCountsTheRest(t *testing.T) { b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"}) rt := startBundle(t, b.Tools()) args := map[string]any{"account": "mesh", "to": []any{"a@example.org", "b@example.org", "c@example.org", "d@example.org", "e@example.org"}, "subject": "Many recipients", "body": "body"} text, isErr := rt.call(SendTool, "novox.triage", args) if isErr { t.Fatalf("%s", text) } var out map[string]any _ = json.Unmarshal([]byte(text), &out) id := out["ask"].(string) whole := wholeAsk(router, id) if strings.Contains(whole, "@") { t.Errorf("the ask carries a mail address among many recipients:\n%s", whole) } if !strings.Contains(whole, "and 2 more") { t.Errorf("the ask does not count the recipients past the first three:\n%s", whole) } mustNotBeRefused(t, router.published[id]) } // (4) A subject holding a mail address and a link has both masked before the ask carries it. func TestMailSendAskMasksAnAddressAndALinkInTheSubject(t *testing.T) { b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"}) rt := startBundle(t, b.Tools()) args := map[string]any{"account": "mesh", "to": []any{"ops@example.org"}, "subject": "Contact ops@example.org or see https://example.com/issue/42", "body": "See https://example.com/issue/42 and write to ops@example.org for details."} text, isErr := rt.call(SendTool, "novox.triage", args) if isErr { t.Fatalf("%s", text) } var out map[string]any _ = json.Unmarshal([]byte(text), &out) id := out["ask"].(string) whole := wholeAsk(router, id) if strings.Contains(whole, "@") || strings.Contains(whole, "example.com") || strings.Contains(whole, "://") { t.Errorf("the ask carries an unmasked address or link:\n%s", whole) } if !strings.Contains(whole, "[address]") || !strings.Contains(whole, "[link]") { t.Errorf("the ask does not say [address] and [link] in place of what it masked:\n%s", whole) } mustNotBeRefused(t, router.published[id]) } // (5) review round 1: a subject the masking does not catch (an IP, a two-slash path-shaped reference, a // 40-character hex hash) does not refuse the whole ask: the real content rule (content_rule.go) still judges // the masked explanation as refused, so the subject and first lines are left out of it and pointed at Details // instead — the ask still names the account and recipients, and still reaches the operator. func TestMailSendAskFallsBackToDetailsWhenTheSubjectStillRefuses(t *testing.T) { for _, subject := range []string{ "Invoice 2026/10/15", "server 10.0.0.1", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", // 40 hex characters: reHex's shape } { t.Run(subject, func(t *testing.T) { b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"}) rt := startBundle(t, b.Tools()) args := map[string]any{"account": "mesh", "to": []any{"ops@example.org"}, "subject": subject, "body": "body"} text, isErr := rt.call(SendTool, "novox.triage", args) if isErr { t.Fatalf("%s", text) } var out map[string]any _ = json.Unmarshal([]byte(text), &out) id := out["ask"].(string) a := router.published[id] mustNotBeRefused(t, a) if !strings.Contains(a.Explanation, "mesh") { t.Errorf("the explanation does not name the account:\n%s", a.Explanation) } if !strings.Contains(a.Explanation, "o… at example org") { t.Errorf("the explanation does not name the recipient:\n%s", a.Explanation) } if strings.Contains(a.Explanation, subject) { t.Errorf("the explanation still carries the subject that refused the ask:\n%s", a.Explanation) } if !strings.Contains(a.Details, "Details") && !strings.Contains(a.Explanation, "Details") { t.Errorf("the explanation does not point at Details:\n%s", a.Explanation) } if !strings.Contains(a.Details, subject) { t.Errorf("Details does not carry the subject (filtered line by line by messenger, as the subject's own line): %s", a.Details) } }) } } // (6) review round 1: a recipient written with a display name ("Name ") is matched against the // operator's own accounts, and masked, by its address alone. func TestMailSendAskMatchesADisplayNameRecipientByItsAddress(t *testing.T) { b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"}) rt := startBundle(t, b.Tools()) args := map[string]any{"account": "mesh", "to": []any{"Jochen Schoubben "}, "subject": "Testing a display name", "body": "body"} text, isErr := rt.call(SendTool, "novox.triage", args) if isErr { t.Fatalf("%s", text) } var out map[string]any _ = json.Unmarshal([]byte(text), &out) id := out["ask"].(string) whole := wholeAsk(router, id) if strings.Contains(whole, "@") || strings.Contains(whole, "Jochen Schoubben") { t.Errorf("the ask carries the display-name recipient's address or name:\n%s", whole) } if !strings.Contains(whole, "your account mesh") { t.Errorf("the ask does not match the display-name recipient to its own account:\n%s", whole) } mustNotBeRefused(t, router.published[id]) }