package main // A reminder on demand, without the operator's calendar: a second Betterbird, in a profile of its own // under the account's runtime directory, with one local calendar file holding one event whose reminder // is due a few seconds after the start. The running Betterbird, its profile, its accounts and its // calendars are not touched: the second one starts with --no-remote, so it neither hands over to the // first nor is handed to. // // The test profile gets the module's user.js too, so the reminder is drawn with the preferences the // module declares. A theme or another preference of the operator's profile can be carried over by // name (carry), within the same limits as betterbird_prefs: never a credential, never an account. // // **It is visibly a test** (the operator, 2026-10-12): on 2026-10-12 at 12:05 a test Betterbird with an empty // profile carrying the operator's theme looked like the operator's configuration lost. So the test profile // carries a userChrome.css that marks its main window with a red band reading MESH TEST, and the event is // called "MESH TEST reminder". The reminder window itself is left as Betterbird draws it, because how it is // drawn is what the test looks at. The tool's description says a window opens on the operator's screen, so // a caller tells the operator first. // // It lives for at most ten minutes (the unit's RuntimeMaxSec), and stop ends it early and removes the // profile. The reminder window is then found with xorg_windows (class Calendar, role AlarmWindow) and // looked at with xorg_screenshot. import ( "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "time" ) // testDir is the test's profile and calendar, under the account's runtime directory. func (m *Machine) testDir() string { return m.path(fmt.Sprintf("/run/user/%d/%s", m.UID, testUnit)) } // carryName is a preference name the test may carry over from the operator's profile. var carryName = regexp.MustCompile(`^[A-Za-z0-9_.\-]+$`) // ReminderAnswer is what betterbird_reminder_test answers. type ReminderAnswer struct { Unit string `json:"unit"` Profile string `json:"profile"` Event string `json:"event"` Due string `json:"reminder_due"` Carried []string `json:"carried,omitempty"` Withheld []string `json:"withheld,omitempty"` Look string `json:"look"` OnScreen string `json:"on_screen,omitempty"` Stopped bool `json:"stopped,omitempty"` } // ics is the test calendar: one event, its reminder `after` from now. func ics(now time.Time, after time.Duration, stamp string) string { f := func(t time.Time) string { return t.UTC().Format("20060102T150405Z") } start := now.Add(after + 10*time.Minute) return strings.Join([]string{"BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//novox mesh//betterbird reminder test//EN", "BEGIN:VEVENT", "UID:mesh-reminder-test-" + stamp + "@mesh", "DTSTAMP:" + f(now), "DTSTART:" + f(start), "DTEND:" + f(start.Add(30*time.Minute)), "SUMMARY:" + testTitle, "BEGIN:VALARM", "ACTION:DISPLAY", "DESCRIPTION:" + testTitle, "TRIGGER:-PT10M", "END:VALARM", "END:VEVENT", "END:VCALENDAR", ""}, "\r\n") } // testTitle is the test event's title: what the reminder window shows. const testTitle = "MESH TEST reminder (a throw-away Betterbird of the mesh, not your calendar)" // testChrome is the test profile's userChrome.css: a red band reading MESH TEST across the top of every // Betterbird window but the reminder (windowtype Calendar:AlarmWindow), and a red frame around it, so the test // Betterbird is never taken for the operator's. const testChrome = `/* The mesh's reminder test (module betterbird, novox/hq issue 345): a throw-away Betterbird, not yours. */ :root:not([windowtype="Calendar:AlarmWindow"]) { outline: 6px solid #d00000 !important; outline-offset: -6px !important; } :root:not([windowtype="Calendar:AlarmWindow"]) > body::before { content: "MESH TEST \2014 a throw-away Betterbird started by the mesh for a reminder test; your mail and calendar are not here. It ends by itself within 10 minutes."; display: block !important; background: #d00000 !important; color: #ffffff !important; font: bold 15px sans-serif !important; padding: 6px 12px !important; text-align: center !important; } ` // testPrefs are the test profile's own preferences: its one calendar, and no first-run questions. // // betterbird.update.checkIntervalDays 0 is Betterbird's own switch for its update check (MailGlue): a // fresh profile has never checked, so Betterbird checks at its start and, when betterbird.eu publishes a // newer build than the installed one, opens the About window on the operator's screen and gives it the // focus (seen on the workstation 2026-10-12, novox/hq issue 345). The test is about the reminder only. func testPrefs(calendar string) string { return strings.Join([]string{ `user_pref("calendar.registry.meshtest.type", "ics");`, `user_pref("calendar.registry.meshtest.uri", "file://` + calendar + `");`, `user_pref("calendar.registry.meshtest.name", "Mesh reminder test");`, `user_pref("calendar.registry.meshtest.calendar-main-in-composite", true);`, `user_pref("calendar.registry.meshtest.suppressAlarms", false);`, `user_pref("calendar.list.sortOrder", "meshtest");`, `user_pref("mail.provider.suppress_dialog_on_startup", true);`, `user_pref("mail.shell.checkDefaultClient", false);`, `user_pref("datareporting.policy.dataSubmissionEnabled", false);`, `user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);`, `user_pref("app.update.auto", false);`, `user_pref("betterbird.update.checkIntervalDays", 0);`, `user_pref("mail.rights.version", 1);`, `user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);`, "", }, "\n") } // ReminderTest starts the test Betterbird, or with stop ends it and removes its profile. func (m *Machine) ReminderTest(after time.Duration, carry []string, stop bool) (ReminderAnswer, error) { dir := m.testDir() a := ReminderAnswer{Unit: testUnit + ".service", Profile: strings.TrimPrefix(filepath.Join(dir, "profile"), m.Root), Look: "xorg_windows with class Calendar answers the reminder window (role AlarmWindow, its depth and map state); " + "xorg_screenshot with that window shows it as the screen does"} if stop { _ = m.cmd(5*time.Second, m.bus(), "systemctl", "--user", "stop", testUnit+".service") if err := os.RemoveAll(dir); err != nil { return a, err } a.Stopped, a.Look = true, "" return a, nil } s, err := m.session() if err != nil { return a, err } var carried []string if len(carry) > 0 { p, err := m.profile() if err != nil { return a, err } prefsJS, _ := readBounded(filepath.Join(p.path, "prefs.js")) have := readPrefs(prefsJS) for _, n := range carry { switch { case !carryName.MatchString(n): return a, fmt.Errorf("carry holds preference names, not %q", n) case withheld(n) != "": a.Withheld = append(a.Withheld, n+": "+withheld(n)) case have[n] == "": a.Withheld = append(a.Withheld, n+": not set in the operator's profile") default: carried = append(carried, fmt.Sprintf("user_pref(%s, %s);", strconv.Quote(n), have[n])) a.Carried = append(a.Carried, n) } } } _ = m.cmd(5*time.Second, s.Env(), "systemctl", "--user", "stop", testUnit+".service") if err := os.RemoveAll(dir); err != nil { return a, err } if err := os.MkdirAll(filepath.Join(dir, "profile"), 0o700); err != nil { return a, err } now := m.Now() calendar := filepath.Join(dir, "test.ics") if err := os.WriteFile(calendar, []byte(ics(now, after, strconv.FormatInt(now.Unix(), 10))), 0o600); err != nil { return a, err } // The module's user.js first, then what is carried, then the test's own preferences: the last // statement wins, so neither can take the test's calendar or its quiet start away. user := "" if mine, present, err := m.declaredText(); err == nil && present && validUserJS(mine) == nil { user = mine + "\n" } user += strings.Join(carried, "\n") + "\n" + testPrefs(strings.TrimPrefix(calendar, m.Root)) if err := os.WriteFile(filepath.Join(dir, "profile", "user.js"), []byte(user), 0o600); err != nil { return a, err } if err := os.MkdirAll(filepath.Join(dir, "profile", "chrome"), 0o700); err != nil { return a, err } if err := os.WriteFile(filepath.Join(dir, "profile", "chrome", "userChrome.css"), []byte(testChrome), 0o600); err != nil { return a, err } // The unit removes the profile when it ends, however it ends: at its 10 minutes, on stop, or when the // test Betterbird is closed. call := []string{"--user", "--collect", "--quiet", "--unit=" + testUnit, "-p", "RuntimeMaxSec=600", "-p", "ExecStopPost=/usr/bin/rm -rf " + strings.TrimPrefix(dir, m.Root), "--setenv=MOZ_NO_REMOTE=1"} for _, kv := range [][2]string{{"DISPLAY", s.Display}, {"XAUTHORITY", s.XAuthority}} { if kv[1] != "" { call = append(call, "--setenv="+kv[0]+"="+kv[1]) } } call = append(call, "--", startBin, "--no-remote", "--profile", strings.TrimPrefix(filepath.Join(dir, "profile"), m.Root)) if err := failed(m.cmd(8*time.Second, s.Env(), "systemd-run", call...), "systemd-run", call...); err != nil { return a, err } a.Event = testTitle + ", in a calendar file of the test profile only" a.OnScreen = "a second Betterbird's window is now on the operator's screen, marked MESH TEST in red; its reminder " + "window follows when the reminder is due. Both go when the test ends (stop, or 10 minutes)" a.Due = now.Add(after).UTC().Format(time.RFC3339) return a, nil }