betterbird: lock the ask to be the default mail client off, so it never shows at a start (hq issue 345)
mesh/merge-gate head 481e44cdaa29 merged into trunk 23ea0f431faf — pass: builds betterbird, sent nowhere; no bus step; 1 wait(s) for a person; every machin…
mesh/repo-check head 481e44cdaa29 merged into trunk 23ea0f431faf — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered

Betterbird's own default asks at every start with a window to become the default for mail, news, feeds and
calendar files. The policy locks mail.shell.checkDefaultClient off, which wins over a profile's own value, and
betterbird_check says when a node would still ask.
This commit is contained in:
jochen
2026-10-12 20:17:38 +02:00
parent 23ea0f431f
commit 481e44cdaa
5 changed files with 117 additions and 4 deletions
@@ -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 == "":
@@ -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)
@@ -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
}