betterbird: lock the missed-reminder pass off, so a passed meeting's reminder does not come back (hq issue 640)
mesh/merge-gate head 73404a3c3f21 merged into trunk 5f491e0c097d — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head 73404a3c3f21 merged into trunk 5f491e0c097d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered

With calendar.alarms.showmissed on, Betterbird's alarm service fires every
reminder of the past month that carries no later acknowledgement at each
load of a calendar. Owl never sends a dismissal to Exchange and keeps it only
in Betterbird's local cache, so on g14 September's Exchange meetings came
back about every ten minutes, Dismiss or not. The policy now locks the pass
off whatever the setting or profile says, and betterbird_check says when the
running Betterbird still has it on beside an Owl calendar.
This commit is contained in:
jochen
2026-10-13 16:32:17 +02:00
parent a84d0094a0
commit 73404a3c3f
6 changed files with 172 additions and 10 deletions
+18 -4
View File
@@ -97,7 +97,7 @@ start; `prefs.js` wins over a default.
value true, false, a whole number or a string. The node-engine merges the layers key by key, so a node's layer value true, false, a whole number or a string. The node-engine merges the layers key by key, so a node's layer
overrides one preference, or takes one out with `null`. The operator's setup approved on 2026-10-12 (compact overrides one preference, or takes one out with `null`. The operator's setup approved on 2026-10-12 (compact
density, 14-point text, the vertical view with multi-line cards and threads, no new-mail pop-up, sound or tray density, 14-point text, the vertical view with multi-line cards and threads, no new-mail pop-up, sound or tray
icon, the calendar's day from 09:00 to 20:00, reminders missed while asleep shown, chat off, remote content icon, the calendar's day from 09:00 to 20:00, reminders missed while asleep shown (since locked off: issue 640), chat off, remote content
blocked, Do Not Track; 21 preferences) moved from the catalogue's defaults file into the mesh-wide layer of blocked, Do Not Track; 21 preferences) moved from the catalogue's defaults file into the mesh-wide layer of
`prefs`, set on the operator's Approve. `prefs`, set on the operator's Approve.
@@ -109,10 +109,24 @@ desktop node writes the map's text into the old build's user.js, and `betterbird
refused until the push. refused until the push.
**Locked** (the policy): Betterbird's updater off (`DisableAppUpdate`); telemetry off (`DisableTelemetry`); **Locked** (the policy): Betterbird's updater off (`DisableAppUpdate`); telemetry off (`DisableTelemetry`);
add-ons change only through the mesh's pins (`ExtensionUpdate` false); and three preferences locked off: add-ons change only through the mesh's pins (`ExtensionUpdate` false); and four preferences locked off:
`mail.shell.checkDefaultClient` (the ask to be the default client), `mail.inappnotifications.enabled` (the `mail.shell.checkDefaultClient` (the ask to be the default client), `mail.inappnotifications.enabled` (the
vendor's in-app notices), and `mailnews.start_page.enabled` (the start page, which otherwise loads from the vendor's in-app notices), `mailnews.start_page.enabled` (the start page, which otherwise loads from the
vendor at every start). vendor at every start), and `calendar.alarms.showmissed` (the reminders of meetings that have passed, below).
**A passed meeting's reminder is not shown again** (novox/hq issue 640). With `calendar.alarms.showmissed` on,
Betterbird's alarm service fires, at every load of a calendar (each start, each refresh, and each time Owl
registers its calendar after signing in), every reminder of the past month whose item carries no later
acknowledgement (`X-MOZ-LASTACK`). Off, Betterbird's own default, a past reminder fires only while its meeting
has not ended. A dismissal is that acknowledgement, and Owl never sends it to Exchange (its changes carry the
reminder's offset alone): it lives only in Betterbird's local calendar cache, which Owl empties when it
resynchronises a calendar from scratch. With the pass on, Exchange meetings of the past month came back about
every ten minutes on g14 from the start that placed the setting's `true`, Dismiss or not. So the policy locks the
pass off, whatever the setting `prefs` or a profile says; a setting's value for it has no effect (`betterbird_config`
shows the lock). What it costs: a reminder that fell due while the laptop slept is not shown once the meeting
is over; one for a meeting still running is. `betterbird_check` says when the running Betterbird still has the
pass on beside an Owl calendar, with the remedy (the module's push, or `betterbird_restart` once the policy is
placed).
**Kept at Betterbird's default** (`// keeps Betterbird's default: <name>` in `mesh-defaults.js`): the theme, **Kept at Betterbird's default** (`// keeps Betterbird's default: <name>` in `mesh-defaults.js`): the theme,
system colours, always-underlined links, manual spam marking, and the composer's attachment pane. A value the system colours, always-underlined links, manual spam marking, and the composer's attachment pane. A value the
@@ -981,6 +981,9 @@ func (m *Machine) Check() (CheckAnswer, error) {
"take them out of install/defaults-pref/mesh-defaults.js in a reviewed catalogue change: an account is declared in the setting accounts") "take them out of install/defaults-pref/mesh-defaults.js in a reviewed catalogue change: an account is declared in the setting accounts")
} }
} }
if what, do := m.missedReminders(profilePath); what != "" {
add(what, do)
}
if why := m.defaultClientAsk(profilePath); why != "" && in.From != "not installed" { if why := m.defaultClientAsk(profilePath); why != "" && in.From != "not installed" {
add("Betterbird asks at its start to be the default mail client: "+why, 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)") "push the module to this node: its policy locks "+defaultClientPref+" off (novox/hq issue 345)")
@@ -249,8 +249,8 @@ func TestTheConfigShowsTheLayersAndTheDrift(t *testing.T) {
f.withDefaults() f.withDefaults()
f.betterbirdsOwn() f.betterbirdsOwn()
links(t, nil) links(t, nil)
f.declare(`user_pref("mail.uifontsize", 16); user_pref("mail.biff.show_alert", false); user_pref("calendar.alarms.showmissed", true);`) f.declare(`user_pref("mail.uifontsize", 16); user_pref("mail.biff.show_alert", false); user_pref("calendar.alarms.playsound", false);`)
f.write(profileDir+"/user.js", mark+")\n"+`user_pref("mail.uifontsize", 16);`+"\n"+`user_pref("calendar.alarms.showmissed", true);`+"\n") f.write(profileDir+"/user.js", mark+")\n"+`user_pref("mail.uifontsize", 16);`+"\n"+`user_pref("calendar.alarms.playsound", false);`+"\n")
f.write(profileDir+"/prefs.js", "// Mozilla User Preferences\n"+ f.write(profileDir+"/prefs.js", "// Mozilla User Preferences\n"+
`user_pref("extensions.activeThemeID", "thunderbird-compact-dark@mozilla.org");`+"\n"+ `user_pref("extensions.activeThemeID", "thunderbird-compact-dark@mozilla.org");`+"\n"+
`user_pref("mail.uifontsize", 12);`+"\n"+ `user_pref("mail.uifontsize", 12);`+"\n"+
@@ -266,7 +266,7 @@ func TestTheConfigShowsTheLayersAndTheDrift(t *testing.T) {
for _, p := range c.Prefs { for _, p := range c.Prefs {
by[p.Name] = p by[p.Name] = p
} }
if len(c.Prefs) != 3+3+len(shippedKeepsWant) || !strings.HasPrefix(c.Defaults, "placed") { if len(c.Prefs) != len(lockedOff)+3+len(shippedKeepsWant) || !strings.HasPrefix(c.Defaults, "placed") {
t.Fatalf("every lock, declared preference and kept default: %d, %s", len(c.Prefs), c.Defaults) t.Fatalf("every lock, declared preference and kept default: %d, %s", len(c.Prefs), c.Defaults)
} }
if !reflect.DeepEqual(c.Drifting, []string{"extensions.activeThemeID", "mail.spam.manualMark", "mail.biff.show_alert"}) { if !reflect.DeepEqual(c.Drifting, []string{"extensions.activeThemeID", "mail.spam.manualMark", "mail.biff.show_alert"}) {
@@ -293,7 +293,7 @@ func TestTheConfigShowsTheLayersAndTheDrift(t *testing.T) {
if p := by["mailnews.start_page.enabled"]; p.Layer != "locked" || p.Effective != "false" || p.Drift != "" { if p := by["mailnews.start_page.enabled"]; p.Layer != "locked" || p.Effective != "false" || p.Drift != "" {
t.Errorf("a lock wins over the profile: %+v", p) t.Errorf("a lock wins over the profile: %+v", p)
} }
if p := by["calendar.alarms.showmissed"]; p.Effective != "true" || p.Layer != "declared" { if p := by["calendar.alarms.playsound"]; p.Effective != "false" || p.Layer != "declared" {
t.Errorf("%+v", p) t.Errorf("%+v", p)
} }
// T5: the check says the drift, with both remedies. // T5: the check says the drift, with both remedies.
@@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@@ -172,8 +173,8 @@ func (m *Machine) policyState() string {
return "placed, and " + why return "placed, and " + why
} }
return "placed: Betterbird's updater, its telemetry and add-on updates off, Owl installed into every profile from " + return "placed: Betterbird's updater, its telemetry and add-on updates off, Owl installed into every profile from " +
"the mesh's xpi with its updates off, and the ask to be the default mail client, the vendor's in-app notices " + "the mesh's xpi with its updates off, and the ask to be the default mail client, the vendor's in-app notices, " +
"and the start page locked off" "the start page and the reminders of meetings that have passed locked off"
} }
// lockedOff are the preferences the module's policy locks to false (novox/hq issue 345), each with what it is. // lockedOff are the preferences the module's policy locks to false (novox/hq issue 345), each with what it is.
@@ -181,6 +182,70 @@ var lockedOff = []struct{ Name, What string }{
{defaultClientPref, "the ask to be the default mail client"}, {defaultClientPref, "the ask to be the default mail client"},
{"mail.inappnotifications.enabled", "the vendor's in-app notices"}, {"mail.inappnotifications.enabled", "the vendor's in-app notices"},
{"mailnews.start_page.enabled", "the start page, loaded from the vendor at every start"}, {"mailnews.start_page.enabled", "the start page, loaded from the vendor at every start"},
{missedPref, "the reminders of meetings that have passed (novox/hq issue 640)"},
}
// missedPref is Betterbird's pass over missed reminders. With it on, the calendar's alarm service fires, at
// every load of a calendar (each start, each refresh, each time Owl registers its calendar after signing in),
// every reminder of the past month whose item carries no later acknowledgement (X-MOZ-LASTACK); with it off
// (Betterbird's own default), a past reminder fires only while its meeting has not ended. Owl never sends a
// dismissal to Exchange (its changes carry the reminder's offset alone) and keeps it only in Betterbird's local
// cache, which a resynchronisation from scratch empties: a passed meeting's reminder then comes back at every
// reload, Dismiss or not (novox/hq issue 640). The policy locks it off, so no setting and no profile can turn the
// pass back on; betterbird_check says when the running Betterbird still has it on beside an Owl calendar.
const missedPref = "calendar.alarms.showmissed"
// missedReminders says why the running Betterbird shows a passed meeting's reminder again at each reload of an
// Owl calendar, with the remedy, or two empty strings: Betterbird's pass over missed reminders on (the profile's
// user.js, which wins at the start, else its prefs.js; Betterbird's default is off) while the profile holds an
// Owl calendar and the placed policy did not lock it off before Betterbird started.
func (m *Machine) missedReminders(profile string) (string, string) {
if profile == "" {
return "", ""
}
prefs := mergedPrefs(profile)
owl := 0
for _, c := range profileCalendars(prefs) {
if c.Kind == "owl" {
owl++
}
}
if owl == 0 {
return "", ""
}
value, from := "false", "Betterbird's default"
for _, f := range []string{"user.js", "prefs.js"} {
b, err := readBounded(filepath.Join(profile, f))
if err != nil {
continue
}
if v, ok := statementsOf(b)[missedPref]; ok {
value, from = v, "the profile's "+f
break
}
}
locked := m.locks()[missedPref] == "false"
ps := m.mainProcs()
if locked {
if len(ps) == 0 {
return "", ""
}
st, err := os.Stat(m.path(policyFile))
t, terr := time.Parse(time.RFC3339, ps[0].Since)
if err != nil || terr != nil || !st.ModTime().After(t) {
return "", ""
}
}
if value != "true" || len(ps) == 0 {
return "", ""
}
what := fmt.Sprintf("Betterbird shows the reminders of meetings that have passed again at every reload of its %d Owl "+
"calendar(s): %s is true (%s), and Owl keeps a dismissal only in Betterbird's local cache, never on the Exchange "+
"server (novox/hq issue 640)", owl, missedPref, from)
if locked {
return what, "betterbird_restart: the module's policy locks " + missedPref + " off, and the running Betterbird started before it was placed"
}
return what, "push the module to this node: its policy locks " + missedPref + " off; then betterbird_restart"
} }
// policyProblem is everything the policy fails to say of what ADR 0316 and issue 345 ask, or "". // policyProblem is everything the policy fails to say of what ADR 0316 and issue 345 ask, or "".
@@ -229,3 +229,79 @@ func mustRead(t *testing.T, f *fake, p string) []byte {
} }
return raw return raw
} }
// owlCalendar is an Owl calendar in the profile's prefs.js, as Owl registers its account's calendar.
func (f *fake) owlCalendar(extra string) {
f.write(profileDir+"/prefs.js", "// Mozilla User Preferences\n"+
`user_pref("calendar.registry.45b5.type", "owl");`+"\n"+
`user_pref("calendar.registry.45b5.uri", "owl://someone%40example%2Eorg@outlook.example.org/");`+"\n"+
`user_pref("calendar.registry.45b5.name", "someone@example.org");`+"\n"+extra)
}
// A passed meeting's reminder shows at most once: the policy locks Betterbird's pass over missed reminders off,
// whatever the setting prefs or the profile say, because Owl keeps a dismissal only in Betterbird's local cache
// and the pass fires every unacknowledged reminder of the past month again at each reload (novox/hq issue 640).
func TestAPassedMeetingsReminderIsNotShownAgainAtEachReloadOfAnOwlCalendar(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "install", "distribution", "policies.json"))
if err != nil {
t.Fatal(err)
}
if p := policyPrefsOf(raw)[missedPref]; p.Value != "false" || !p.Locked {
t.Fatalf("the module's policy locks %s off: %+v", missedPref, p)
}
before := strings.Replace(string(raw), `"calendar.alarms.showmissed"`, `"calendar.alarms.unrelated"`, 1)
if why := policyProblem([]byte(before)); !strings.Contains(why, missedPref) {
t.Fatalf("a policy that leaves the missed reminders on is said: %q", why)
}
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
links(t, nil)
nag := func() Finding {
t.Helper()
c, err := f.Check()
if err != nil {
t.Fatal(err)
}
for _, x := range c.Findings {
if strings.Contains(x.What, "Owl calendar") {
return x
}
}
return Finding{}
}
// g14 on 2026-10-13: the module's user.js turns the pass on, an Owl calendar is in the profile, and the
// placed policy does not lock it.
f.write(policyFile, before)
f.owlCalendar("")
f.write(profileDir+"/user.js", mark+")\n"+`user_pref("calendar.alarms.showmissed", true);`+"\n")
if x := nag(); !strings.Contains(x.What, "1 Owl calendar") || !strings.Contains(x.What, "user.js") ||
!strings.Contains(x.Do, "push the module") {
t.Fatalf("the nag is a finding with its remedy: %+v", x)
}
// The policy placed after the running Betterbird started: in effect from its next start.
f.write(policyFile, string(raw))
later := time.Unix(1_900_000_000, 0)
os.Chtimes(filepath.Join(f.Root, policyFile), later, later)
if x := nag(); !strings.Contains(x.Do, "betterbird_restart") || strings.Contains(x.Do, "push") {
t.Fatalf("a lock placed after the start waits for a restart: %+v", x)
}
// The lock in effect: nothing the profile says turns the pass back on.
f.write(policyFile, string(raw))
f.owlCalendar(`user_pref("calendar.alarms.showmissed", true);` + "\n")
if x := nag(); x.What != "" {
t.Fatalf("the lock holds whatever the profile says: %+v", x)
}
// Without the lock, the pass off is no nag, and neither is the pass on without an Owl calendar.
f.write(policyFile, before)
f.write(profileDir+"/user.js", mark+")\n"+`user_pref("calendar.alarms.showmissed", false);`+"\n")
if x := nag(); x.What != "" {
t.Fatalf("the pass off: %+v", x)
}
f.write(profileDir+"/user.js", mark+")\n"+`user_pref("calendar.alarms.showmissed", true);`+"\n")
f.write(profileDir+"/prefs.js", `user_pref("calendar.registry.c1.type", "caldav");`+"\n"+`user_pref("calendar.registry.c1.uri", "https://dav.example.org/c1/");`+"\n")
if x := nag(); x.What != "" {
t.Fatalf("a CalDAV calendar keeps its dismissals on its server: %+v", x)
}
}
@@ -22,6 +22,10 @@
"mailnews.start_page.enabled": { "mailnews.start_page.enabled": {
"Value": false, "Value": false,
"Status": "locked" "Status": "locked"
},
"calendar.alarms.showmissed": {
"Value": false,
"Status": "locked"
} }
} }
} }