diff --git a/modules/betterbird/README.md b/modules/betterbird/README.md index b86e73b9..082e2ec2 100644 --- a/modules/betterbird/README.md +++ b/modules/betterbird/README.md @@ -56,7 +56,15 @@ honours) and installs Owl into every profile from `file:///opt/betterbird/owl/ow `force_installed`, with `updates_disabled`. A profile that already runs another Owl keeps it: the policy installs the pinned one where Owl is missing. A newer Owl in the profile (g14 runs 1.5.7) is said as a fact by `betterbird_status`; Owl installed by policy cannot be removed in Betterbird, so the remedy is -moving the pin. An older one is a finding of `betterbird_check`, with the same remedy. **Owl's licence stays the person's**: the mesh holds no licence, key or payment (ADR 0316 §9). +moving the pin. An older one is a finding of `betterbird_check`, with the same remedy. + +**Betterbird never asks to be the default client.** The policy locks `mail.shell.checkDefaultClient` off +(`Preferences`, `Status: locked`): Betterbird's own default (`all-thunderbird.js`) is to ask at every start, +with a window to make it the default for mail, news, feeds and calendar files, while that preference is true +(`messenger.js`). Betterbird 153 has no policy of its own for it (its `policies-schema.json`), and its +`Preferences` policy accepts the `mail.` prefix. A lock, not a default: the window's own "always check" box +writes the preference into the profile, which wins over a default; a lock wins over the profile and greys the +box out in the settings. `betterbird_check` says when Betterbird would ask (novox/hq issue 345). **Owl's licence stays the person's**: the mesh holds no licence, key or payment (ADR 0316 §9). **No start of its own.** Betterbird is started by the operator from the launcher. The start (`/usr/local/bin/betterbird`) first runs `betterbird-tools prepare`, which, with no Betterbird of the @@ -159,7 +167,7 @@ and key. | `betterbird_user_js` (r; a with `apply`) | the declared user.js against the profile's; `apply` places it | | `betterbird_log` (r) | the last lines of the journal of `mesh-betterbird` (or of the reminder test), addresses and credentials masked (the value after `password=`, `token:` and the like, and the token after `Bearer`/`Basic`). A Betterbird the module did not start prints elsewhere, and the answer says where | | `betterbird_restart` (a) | ends Betterbird (asked to quit, forced after 8 s), places the user.js where allowed, starts it as `mesh-betterbird`. **Refused while a composer window is open** (X instance `Msgcompose`, whatever its class), because the unsent message would be lost, unless `discard_unsent` is true, which only the operator's yes justifies. It answers within the runtime's 30 s: a start not yet seen after 4 s is said in `note`, and `betterbird_status` shows it | -| `betterbird_check` (r) | installed from the mesh's release and not behind the newest published build; the policy placed; Owl at its pin and active; one running; profile found; the user.js on the machine, valid, placed, in effect; the declared accounts in the list, none refused, each signed in once; its output read | +| `betterbird_check` (r) | installed from the mesh's release and not behind the newest published build; the policy placed; no ask at the start to be the default mail client; Owl at its pin and active; one running; profile found; the user.js on the machine, valid, placed, in effect; the declared accounts in the list, none refused, each signed in once; its output read | | `betterbird_accounts` (r; a with `propose`) | the profile's accounts by count and kind (declarable, Owl's, others); with `propose`, the setting `accounts` built from them and proposed to the operator (ADR 0317 §9) | | `betterbird_reminder_test` (a) | a reminder on demand without the operator's calendar (below) | diff --git a/modules/betterbird/cmd/betterbird-tools/betterbird.go b/modules/betterbird/cmd/betterbird-tools/betterbird.go index 3759e49c..a84c73c1 100644 --- a/modules/betterbird/cmd/betterbird-tools/betterbird.go +++ b/modules/betterbird/cmd/betterbird-tools/betterbird.go @@ -849,7 +849,8 @@ type CheckAnswer struct { } // Check verifies what the module promises: Betterbird installed, one running in the session, its -// profile found, the module's user.js declared, valid, placed and in effect, and its output read. +// profile found, the module's user.js declared, valid, placed and in effect, its output read, and no ask at its +// start to be the default mail client (novox/hq issue 345). func (m *Machine) Check() (CheckAnswer, error) { a := CheckAnswer{Findings: []Finding{}} add := func(what, do string) { a.Findings = append(a.Findings, Finding{what, do}) } @@ -875,6 +876,10 @@ func (m *Machine) Check() (CheckAnswer, error) { if in.From == "the mesh" && !strings.HasPrefix(in.Policy, "placed:") { add("the enterprise policy is "+in.Policy, "push the module to this node") } + if why := m.defaultClientAsk(profilePath); why != "" && in.From != "not installed" { + add("Betterbird asks at its start to be the default mail client: "+why, + "push the module to this node: its policy locks "+defaultClientPref+" off (novox/hq issue 345)") + } switch { case in.From != "the mesh": case in.Owl == nil || in.Owl.Pinned == "": diff --git a/modules/betterbird/cmd/betterbird-tools/install.go b/modules/betterbird/cmd/betterbird-tools/install.go index 9e852c5b..1ec0204d 100644 --- a/modules/betterbird/cmd/betterbird-tools/install.go +++ b/modules/betterbird/cmd/betterbird-tools/install.go @@ -30,6 +30,10 @@ const ( // owlFile is Owl's xpi as the mesh placed it, and the URL the policy installs it from. owlFile = installDir + "/owl/owl.xpi" owlID = "owl@beonex.com" + // defaultClientPref is Betterbird's ask at its start to be the default mail, news, feed and calendar + // client (Thunderbird's all-thunderbird.js: pref("mail.shell.checkDefaultClient", true); messenger.js opens + // systemIntegrationDialog.xhtml while it is true). The module's policy locks it off (novox/hq issue 345). + defaultClientPref = "mail.shell.checkDefaultClient" // preparation is the tools' program as the module places it for its start (betterbird-tools prepare). preparation = "/usr/local/lib/mesh-betterbird/betterbird-tools" // versionsURL is where Betterbird publishes its newest build per major version: what its own updater @@ -167,7 +171,8 @@ func (m *Machine) policyState() string { if why := policyProblem(raw); why != "" { return "placed, and " + why } - return "placed: Betterbird's updater off, Owl installed into every profile from the mesh's xpi, its updates off" + return "placed: Betterbird's updater off, Owl installed into every profile from the mesh's xpi, its updates off, " + + "and the ask to be the default mail client locked off" } // policyProblem is what the policy fails to say of what ADR 0316 asks, or "". @@ -180,6 +185,10 @@ func policyProblem(raw []byte) string { URL string `json:"install_url"` Disabled bool `json:"updates_disabled"` } `json:"ExtensionSettings"` + Preferences map[string]struct { + Value any `json:"Value"` + Status string `json:"Status"` + } `json:"Preferences"` } `json:"policies"` } if err := json.Unmarshal(raw, &p); err != nil { @@ -196,9 +205,37 @@ func policyProblem(raw []byte) string { case !owl.Disabled: return "it leaves Owl's updates on" } + if pr, ok := p.Policies.Preferences[defaultClientPref]; !ok || pr.Value != false || pr.Status != "locked" { + return "it does not lock Betterbird's ask to be the default mail client off (Preferences " + defaultClientPref + ": false, locked)" + } return "" } +// defaultClientAsk says why Betterbird asks at its start to be the default mail client, or "" when it does +// not: the module's policy locks the ask off, else the profile's user.js (which wins at the start) or prefs.js +// turns it off. Betterbird's own default is to ask. +func (m *Machine) defaultClientAsk(profile string) string { + raw, err := readBounded(m.path(policyFile)) + if err == nil && !strings.Contains(policyProblem(raw), defaultClientPref) { + return "" + } + if profile != "" { + for _, f := range []string{"user.js", "prefs.js"} { + b, err := readBounded(filepath.Join(profile, f)) + if err != nil { + continue + } + if v, ok := readPrefs(b)[defaultClientPref]; ok { + if v == "false" { + return "" + } + return "the profile's " + f + " sets " + defaultClientPref + " to " + v + " and no policy locks it off" + } + } + } + return "no policy locks " + defaultClientPref + " off and Betterbird's default is to ask" +} + // xpiVersion is the version an add-on's manifest.json says, or "" when it cannot be read. func xpiVersion(path string) string { r, err := zip.OpenReader(path) diff --git a/modules/betterbird/cmd/betterbird-tools/install_test.go b/modules/betterbird/cmd/betterbird-tools/install_test.go index 94838b04..ffbb2884 100644 --- a/modules/betterbird/cmd/betterbird-tools/install_test.go +++ b/modules/betterbird/cmd/betterbird-tools/install_test.go @@ -172,3 +172,60 @@ func TestTheUserJSDescriptionSaysAdoption(t *testing.T) { } } } + +// Betterbird never asks at its start to be the default mail client: the module's policy locks +// mail.shell.checkDefaultClient off, and the check says so when it is on (novox/hq issue 345). +func TestTheDefaultClientAskIsLockedOffAndTheCheckSaysWhenItIsOn(t *testing.T) { + f := newFake(t) + f.betterbird() + f.placedByTheMesh() + links(t, nil) + asks := func() string { + t.Helper() + c, err := f.Check() + if err != nil { + t.Fatal(err) + } + for _, x := range c.Findings { + if strings.Contains(x.What, "asks at its start") { + return x.What + " | " + x.Do + } + } + return "" + } + if got := asks(); got != "" { + t.Fatalf("the module's policy locks the ask off, yet: %s", got) + } + unlocked := `{"policies": {"DisableAppUpdate": true, "ExtensionSettings": {"owl@beonex.com": {"installation_mode": "force_installed", + "install_url": "file:///opt/betterbird/owl/owl.xpi", "updates_disabled": true}}}}` + if why := policyProblem([]byte(unlocked)); !strings.Contains(why, defaultClientPref) { + t.Fatalf("a policy that leaves the ask on is said: %q", why) + } + f.write(policyFile, unlocked) + if got := asks(); !strings.Contains(got, "default mail client") || !strings.Contains(got, "push the module") { + t.Fatalf("the ask on is a finding with its remedy: %q", got) + } + f.write(policyFile, `{"policies": {"DisableAppUpdate": true, "Preferences": {"mail.shell.checkDefaultClient": {"Value": false, "Status": "default"}}, + "ExtensionSettings": {"owl@beonex.com": {"installation_mode": "force_installed", "install_url": "file:///opt/betterbird/owl/owl.xpi", "updates_disabled": true}}}}`) + if why := policyProblem(mustRead(t, f, policyFile)); !strings.Contains(why, "locked") { + t.Fatalf("a default the profile can override is not a lock: %q", why) + } + f.write(profileDir+"/prefs.js", `user_pref("mail.shell.checkDefaultClient", true);`+"\n") + if got := asks(); got == "" { + t.Fatal("a profile that turned the ask back on over an unlocked default is a finding") + } + f.write(policyFile, unlocked) + f.write(profileDir+"/prefs.js", `user_pref("mail.shell.checkDefaultClient", false);`+"\n") + if got := asks(); got != "" { + t.Fatalf("a profile with the ask off does not ask: %s", got) + } +} + +func mustRead(t *testing.T, f *fake, p string) []byte { + t.Helper() + raw, err := os.ReadFile(filepath.Join(f.Root, p)) + if err != nil { + t.Fatal(err) + } + return raw +} diff --git a/modules/betterbird/install/distribution/policies.json b/modules/betterbird/install/distribution/policies.json index b2b65390..e75b4c84 100644 --- a/modules/betterbird/install/distribution/policies.json +++ b/modules/betterbird/install/distribution/policies.json @@ -7,6 +7,12 @@ "install_url": "file:///opt/betterbird/owl/owl.xpi", "updates_disabled": true } + }, + "Preferences": { + "mail.shell.checkDefaultClient": { + "Value": false, + "Status": "locked" + } } } }