Files
mesh-catalog/modules/betterbird
jochen ec4ab7afe7 betterbird: install Betterbird from its pinned release, ship Owl by policy, and declare the accounts without credentials (hq issue 345, ADR 0316, ADR 0317)
A module that cannot set Betterbird up is of little use (the operator,
2026-10-12). The module now declares the community package absent and
unpacks the pinned upstream release where that package had it, so the same
profile opens; places the enterprise policy that turns Betterbird's updater
off and force-installs Owl from the xpi the mesh pinned by its vendor's
digest (never in a repository); and takes the operator's accounts from the
trusted setting accounts, without any password or token: an account the
profile already holds is adopted under its own keys, a new one is created
under the module's mesh_ keys, an ambiguous one is refused with the remedy,
and the account lists are only written into. betterbird_accounts says the
profile's accounts by count and proposes the setting built from them for the
operator's Approve. Every new build waits for the operator's push. The
reminder test is visibly a test (MESH TEST band, event name) and its
description says it opens a window on the operator's screen.
2026-10-12 14:49:21 +02:00
..

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, and betterbird_check says when the profile's Owl is not the pin. 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. The other accounts go ahead; the start and betterbird_check say it.

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; 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).