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.
246 lines
19 KiB
Markdown
246 lines
19 KiB
Markdown
# betterbird
|
||
|
||
Betterbird, the mail and calendar client of g14 and shanks (a Thunderbird fork, with the operator's
|
||
Exchange calendar through the Owl add-on), as a module (novox/hq issue 345, ADR 0208, ADR 0316, ADR 0317,
|
||
to-be 56). It requires `x11-display`, so it is assigned only where a display server is held on the same
|
||
machine. It installs Betterbird from a pinned upstream release, ships Owl with it through Betterbird's
|
||
enterprise policy, and declares the operator's mail accounts without their credentials.
|
||
|
||
It exists because a symptom had no tools: on a workstation, a calendar reminder appeared as a
|
||
transparent window with nothing in it (2026-10-09). Nothing in the mesh could say which window that
|
||
was, how the compositor treats it, or which preferences Betterbird ran with. The diagnosis is in hq
|
||
issue 345.
|
||
|
||
## Owns
|
||
|
||
| what | where | class |
|
||
|---|---|---|
|
||
| Betterbird, the upstream release (`archive`, pinned by URL and sha256) | `/opt/betterbird` | owned, whole |
|
||
| the enterprise policy (`install/distribution/policies.json`) | `/opt/betterbird/distribution` | owned, whole |
|
||
| Owl's xpi (`owl`, pinned by the vendor's sha256) | `/opt/betterbird/owl` | owned, whole |
|
||
| the spelling dictionaries' default (`install/defaults-pref/vendor-prefs.js`) | `/opt/betterbird/defaults/pref` | owned: its one file |
|
||
| the tools' program, for the start's preparation | `/usr/local/lib/mesh-betterbird` | owned, whole |
|
||
| the start | `/usr/local/bin/betterbird` | owned, whole |
|
||
| the launcher entry | `/usr/local/share/applications/eu.betterbird.Betterbird.desktop` | owned, whole |
|
||
| the module's directory, with its `user.js` (setting `prefs`), `accounts.json` (setting `accounts`) and the record of adopted and created accounts | `~/.config/betterbird/` | owned, whole |
|
||
| the profile's `user.js`, placed by the tools | the profile | the module's, or never touched |
|
||
| the account lists in the profile's `prefs.js` | the profile | written into: the module's keys only |
|
||
| the profile: mail, address books, calendars, logins | `~/.thunderbird/` | found (ADR 0182) |
|
||
|
||
**The community package `betterbird-bin` is declared absent** (ADR 0180), before anything is placed in
|
||
`/opt/betterbird`: the node-engine removes it with `pacman -R`, then unpacks the release where the package
|
||
had it. Betterbird chooses its profile by its installation's path, so the same profile opens after the
|
||
switch. A package left in place (its removal failed) holds the path, and the release is refused there by
|
||
the node-engine rather than mixed in. The module creates no link: the start, a launcher entry naming the
|
||
icon in the installation, and the dictionaries as a default preference replace the package's links.
|
||
|
||
**Every new build waits for the operator** (`upgrade: record`): the first push to a node removes the
|
||
package and replaces what a running Betterbird has open, so the operator quits Betterbird, then pushes.
|
||
An upgrade is a change to the pins, reviewed and delivered like any other (ADR 0316 §5).
|
||
|
||
**The pins.** The release is `betterbird-153.4.0esr-bb10.en-US.linux-x86_64.tar.xz` (81,620,684 bytes) and
|
||
Owl is 1.5.6.1 (430,201 bytes), each pinned by sha256 in `module.json`. The build seat fetches each once,
|
||
refuses it when the digest differs, keeps it under its sha256, and packs it into the artifact store; a node
|
||
receives the mesh's archive by digest and never asks the vendor. **Owl is never in this repository**: its
|
||
licence forbids copying it to third parties, and its vendor serves only its newest version, at one
|
||
address. Keeping the pinned copy in the mesh's private artifact store for the operator's own machines was
|
||
the operator's call (2026-10-12, ADR 0316 §6).
|
||
|
||
**Owl's range.** Owl declares which Betterbird versions it supports (`strict_min_version` to
|
||
`strict_max_version`: 115 to 156 for 1.5.6.1). `owlSupports` in `manifest_test.go` holds the range read
|
||
from each pinned xpi, and the module's tests refuse a Betterbird pin outside it: moving Owl's pin means
|
||
reading the new range there (ADR 0316 §8).
|
||
|
||
**The policy** turns Betterbird's own updater off (`DisableAppUpdate`, which Betterbird's update check
|
||
honours) and installs Owl into every profile from `file:///opt/betterbird/owl/owl.xpi` as
|
||
`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.
|
||
|
||
**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
|
||
account running, places the user.js and writes the created accounts into the account lists; then it
|
||
starts Betterbird either way. `betterbird_restart` starts it as the transient user unit
|
||
`mesh-betterbird`, so its output then goes to the journal.
|
||
|
||
|
||
## The user.js: how a preference is declared
|
||
|
||
Betterbird reads `user.js` in its profile when it starts, and a preference set there wins over the
|
||
one it saved in `prefs.js`. The profile's directory has a random name per machine
|
||
(`<random>.default-default`), which a manifest cannot name. So:
|
||
|
||
1. the node-engine writes `~/.config/betterbird/user.js` from the module's setting `prefs`, one line of
|
||
`user_pref("name", value);` statements (default: a comment, no preference);
|
||
2. the module's tools copy that file whole into the profile: `betterbird_user_js` with `apply`, or
|
||
`betterbird_restart`, which places it before it starts Betterbird again;
|
||
3. Betterbird reads it at that start.
|
||
|
||
**The profile is found** as the one the running Betterbird holds open; with none running, as the
|
||
installation's default in `profiles.ini` (`[Install…] Default`), else the profile marked `Default=1`.
|
||
On the workstation measured these differ: `Profile1` is marked `Default=1`, and the installation's
|
||
default, the one in use, is the other.
|
||
|
||
**A user.js the module did not write is never overwritten.** The module's begins with the line
|
||
`// Betterbird's user.js (module betterbird`; any other is the operator's, and `apply` refuses with
|
||
what to do. The tools refuse a declared file that holds anything but comments and `user_pref`
|
||
statements, or that sets a preference whose name can hold a credential.
|
||
|
||
The file is **trusted** (`"trusted": true`): a preference can weaken how the mail client checks a server,
|
||
so the setting is changed only at the controller's terminal or proposed and set on the operator's warrant
|
||
(novox/hq issue 339, ADR 0277). An account's preferences (`mail.account.`, `mail.identity.`, `mail.server.`,
|
||
`mail.smtpserver.` and the account lists) are refused in `prefs`: an account is declared in `accounts`, so
|
||
it has one owner (ADR 0316 §11).
|
||
## The accounts: declared without credentials (ADR 0316 §10–§16, ADR 0317)
|
||
|
||
The setting `accounts` reaches the machine as `~/.config/betterbird/accounts.json`, a trusted mergeable
|
||
file: a map of accounts by name, each with `address`, `from-name` and `label` (optional), `incoming` (`kind`
|
||
imap or pop3, `host`, `port`, `security` tls, starttls or none, `sign-in` password, encrypted-password,
|
||
oauth, kerberos, ntlm or tls-certificate, `user`) and `outgoing` (the same, without a kind; its sign-in may
|
||
be none), and, only in a node's own layer, `key`. **Never a password or a token**: the whole file is
|
||
refused when any key in it is shaped like one, and an account signs in at its first use, in Betterbird's
|
||
own window, where Betterbird keeps the result in its login store. The work account through Owl is never
|
||
declared: the person creates it in Owl's setup (ADR 0316 §13).
|
||
|
||
At every apply (the start from the launcher, `betterbird_user_js` with apply, `betterbird_restart`), each
|
||
declared account, in its place in the setting, is (ADR 0317 §1):
|
||
|
||
1. **kept by its recorded keys**, when the module recorded it on this node for this profile;
|
||
2. **its creation**, when it is under the module's own keys (`account_mesh_<name>`, `id_mesh_<name>`,
|
||
`server_mesh_<name>`, `smtp_mesh_<name>`);
|
||
3. **adopted by the key a node's layer names** (`key`), when that account is in the list, its default
|
||
identity has the address and it is not Owl's;
|
||
4. **adopted by its address**, when exactly one account's default identity has it: its keys (account,
|
||
default identity, incoming server, and the outgoing server that identity names) become the module's, and
|
||
the account lists are left as they are;
|
||
5. **created** under the module's own keys when no account has it: its keys are added at the end of
|
||
`mail.accountmanager.accounts` and `mail.smtpservers` in `prefs.js`, every other key kept in its place,
|
||
only while Betterbird is not running.
|
||
|
||
**Refused, for that account alone and with the remedy** (§7): two accounts with the address (remove the
|
||
duplicate in Betterbird, or name the `key`), Owl's account at the address, a named key that does not
|
||
qualify, an account of another kind than declared (never converted), an identity that names no outgoing
|
||
server, and an outgoing server that another identity, neither adopted nor the module's, also uses
|
||
(Betterbird lets identities share one, and the declared values would change that other account's sending;
|
||
ADR 0317 §2 leaves the identity's server and the lists as they are, so the module refuses rather than give
|
||
it a server of its own). A recorded adoption follows the identity's current outgoing server at every apply,
|
||
under the same rule. The other accounts go ahead; the start and `betterbird_check` say it.
|
||
`betterbird_user_js` answers an account's preferences by name with their values withheld.
|
||
|
||
The declared fields are written in the user.js under the account's keys, so they win at every start; only
|
||
the fields the setting names, never the account's directory, folders, filters, caches, mail or kept
|
||
sign-in, so an adopted account keeps its sign-in (§5). The apply names, by preference name only, each one
|
||
whose value it changed (§4). What was adopted or created is recorded in
|
||
`~/.config/betterbird/accounts-record.json`. **Leaving the setting** releases an adopted account (its
|
||
lines and its record go, the account stays the person's) and takes a created account's keys out of the
|
||
lists (its mail stays in the profile).
|
||
|
||
**The value is derived, not retyped** (§9): `betterbird_accounts` with `propose` builds `accounts` from the
|
||
profile (every account but Owl's, no credential) and proposes it through the controller's settings verb;
|
||
the operator reads it whole at their desk (`settings proposals <id>`) and approves on a channel that proves
|
||
who answers. Its answer to its caller holds counts and kinds only.
|
||
|
||
**A node with no profile yet.** The preparation needs the profile Betterbird makes at its first start: on a
|
||
node set up from nothing, the declared accounts are written at the second start from the launcher (or at
|
||
`betterbird_restart`).
|
||
|
||
|
||
## Tools
|
||
|
||
They are served by the node's runtime as the operator account (ADR 0175). **No answer carries mail, a
|
||
calendar entry, an address, a host, an account's name or a credential**: accounts are said by count, place
|
||
and key.
|
||
|
||
| tool | does |
|
||
|---|---|
|
||
| `betterbird_status` (r) | whether it runs (pid, since, scope, children); what is installed and from where (the mesh's release, or the community package before the switch), its version and build beside the newest published build (Betterbird's own `versions.txt`); the policy; Owl's version against the pin and whether it is active; where its output goes; the profile and how it was found; the user.js (declared, placed, in effect); the declared accounts by count and, per place, adopted, created or refused with their keys; the add-ons in the profile (ids, versions); the preferences that decide how its windows and notifications look |
|
||
| `betterbird_prefs` (r) | named preferences or prefixes (ending in `.` or `*`, at most 200; an empty name, `*` or `.` is refused): value and source (user.js, prefs.js or the default). Withheld without its value: a name that can hold a credential (`password`, `token`, `oauth`, `signon`, …); one that names an account, server, calendar, recent folder or person (`mail.server.`, `mail.identity.`, `mail.last_msg…`, `messenger.account.`, `mail.cloud_files.`, `calendar.registry.`, …); and every `extensions.*` but the theme's id (`extensions.activeThemeID`). In every value answered, the home is `~`, and a mail address, a `user%40domain@host` URI and a host name are masked |
|
||
| `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; 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) |
|
||
|
||
### A reminder on demand
|
||
|
||
**It opens windows on the operator's screen**, and its description says so first, so a caller tells the
|
||
operator before calling it. 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 is **visibly a test**: its
|
||
profile carries a `userChrome.css` that frames every window of the test Betterbird in red and runs a red
|
||
band reading MESH TEST across its top, and its event is called "MESH TEST reminder (a throw-away
|
||
Betterbird of the mesh, not your calendar)". The reminder window itself is left unstyled, because how
|
||
Betterbird draws it is what the test looks at; its title is the event's. A window title cannot be set by a
|
||
preference, so the marking is the band, the frame and the event's name.
|
||
|
||
`betterbird_reminder_test` starts a **second** Betterbird with `--no-remote` in a throw-away profile
|
||
under `/run/user/<uid>/mesh-betterbird-reminder-test/`, as the user unit
|
||
`mesh-betterbird-reminder-test` (ended after 10 minutes at most). The profile holds the module's
|
||
user.js (the setting `prefs`; never a declared account) and one calendar: a local `.ics` file with one
|
||
event, "MESH TEST reminder", whose reminder is
|
||
due `after` seconds from the call (default 40). `carry` copies named preferences of the operator's
|
||
profile into it (a theme, a colour switch), under the same limits as `betterbird_prefs`. The test's own
|
||
preferences come last in its user.js, so nothing declared or carried overrides them; one of them turns
|
||
Betterbird's update check off (`betterbird.update.checkIntervalDays` 0), because a fresh profile checks
|
||
at its start and, when a newer build is published, opens the About window on the operator's screen
|
||
(seen 2026-10-12). Then:
|
||
|
||
- `xorg_windows` with class `Calendar` answers the reminder window: role `AlarmWindow`, type `Dialog`,
|
||
its depth (32: a visual with alpha, which the compositor blends), its map state;
|
||
- `xorg_screenshot` with that window shows it as the screen shows it.
|
||
|
||
The unit removes its profile when it ends, however it ends (`ExecStopPost`): at its 10 minutes, on
|
||
`stop`, or when the test window is closed. The test Betterbird is never counted as the operator's: its
|
||
command line names its own profile, and `status`, `check`, `restart` and the user.js leave it out.
|
||
|
||
The running Betterbird, its profile and its calendars are not touched: `--no-remote` keeps the two
|
||
apart. **`profiles.ini` and `installs.ini` are not rewritten either**, measured on the workstation
|
||
(2026-10-09): their content and modification time (June 2023) were the same before a `--profile` start,
|
||
while it ran, and after it ended.
|
||
|
||
## The reminder window, as measured (2026-10-09, workstation, i3 and picom with glx)
|
||
|
||
| | |
|
||
|---|---|
|
||
| X class / instance | `eu.betterbird.Betterbird` / `Calendar` |
|
||
| role, type | `AlarmWindow`, `_NET_WM_WINDOW_TYPE_DIALOG` (i3 floats it) |
|
||
| depth | 32 (ARGB visual), like every Betterbird window there |
|
||
| size | 600×300, kept in the profile's `xulstore.json` |
|
||
|
||
A window of depth 32 that its program has not drawn is transparent under a compositor, not black:
|
||
that is why the symptom looks the way it does. Why Betterbird did not draw it is hq issue 345's
|
||
question.
|
||
|
||
## What changes when it is pushed
|
||
|
||
The module is assigned to shanks today (look-only, from issue 345's first part); g14 runs Betterbird
|
||
outside the mesh. With this version, a push to a node (each waits for the operator: `upgrade: record`):
|
||
|
||
| | on a node with the community package (g14, shanks) |
|
||
|---|---|
|
||
| the package | `betterbird-bin` removed (quit Betterbird first) |
|
||
| `/opt/betterbird` | the mesh's 153.4.0esr-bb10, in the same place, so the same profile opens |
|
||
| the policy, Owl | placed; Owl installed into the profile by Betterbird at its next start where it is missing |
|
||
| `/usr/local` | the start, the launcher entry, the tools' program |
|
||
| `~/.config/betterbird/` | `user.js`, `accounts.json` (no account until the setting `accounts` is approved) |
|
||
| the profile | untouched until the next start from the launcher, `betterbird_user_js` with apply, or `betterbird_restart` |
|
||
|
||
## Relies on
|
||
|
||
- A display server on the same machine (`x11-display`, ADR 0208 §3).
|
||
- **`xorg`** for `xorg_windows` and `xorg_screenshot`, which look at Betterbird's windows. This module
|
||
carries no window code of its own.
|
||
- `systemd-run` and `journalctl` of the account's user manager.
|
||
- The build seat's archive pinned by URL and sha256 (mesh-controller, ADR 0316 §2): a controller that does
|
||
not know it refuses this manifest.
|
||
- The controller's `settings` verb, for `betterbird_accounts` with `propose` (`invokes`).
|