// The betterbird module's Go tools bundle (novox/hq ADR 0188, ADR 0193, ADR 0208): Betterbird, the // mail and calendar client, in the operator's session, served by the node's runtime as the operator // account. The module holds no seat, so every tool is its own. // // The operator's mail, calendar events, contacts and credentials are never read: a tool reads processes, // profiles.ini, and of the profile its prefs.js, user.js, extensions.json, each account's msgFilterRules.dat and // of logins.json only the server each kept sign-in is for. The tools show the operator's own configuration, an // account's, an identity's, a calendar's, an address book's, a filter's and a tag's values, and never a credential // (novox/hq ADR 0329 §1, §2): every answer passes credentialChecked. package main import ( "fmt" "os" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) func main() { // The module's start runs this before Betterbird (novox/hq ADR 0316 §11, §12): the user.js placed and the // declared accounts written into the account lists, while no Betterbird of this account runs. if len(os.Args) == 2 && os.Args[1] == "prepare" { if err := machine.Prepare(); err != nil { fmt.Fprintln(os.Stderr, "betterbird: "+err.Error()) os.Exit(1) } return } if err := stdio.Serve("", tools()); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } var machine = NewMachine() func tools() []stdio.Tool { list := toolList() for i := range list { run := list[i].Run list[i].Run = func(args map[string]any) (any, error) { out, err := run(args) return credentialChecked(out), err } } return list } // listTool is the listing tool of one of the five kinds (novox/hq ADR 0329 §12). func listTool(kind, what string) stdio.Tool { return stdio.Tool{ Name: "betterbird_" + strings.ReplaceAll(kind, "-", "_"), Description: "The " + what + " of the profile Betterbird uses beside the ones the module's setting " + kind + " declares (novox/hq ADR 0329): each of the profile's with its key, its values and the declared entry it is " + "(none: the person's own), Owl's marked; each declared one with its value and how it resolved on this node " + "(adopted, created or refused with why, and its key). The operator's own values, never a password, token " + "or cookie. betterbird_drift says how they differ. (r)", Run: func(map[string]any) (any, error) { return machine.List(kind) }, } } func toolList() []stdio.Tool { return []stdio.Tool{ { Name: "betterbird_status", Description: "Betterbird: whether it runs (the main process's pid, since, and the scope it runs in; how " + "many child processes); what is installed in /opt/betterbird and from where (the mesh's pinned release, " + "or the community package before the switch), its version and build beside the newest published build; " + "whether the enterprise policy is placed; Owl's version against the mesh's pin and whether it is active; " + "where its output goes; the profile it uses and how that was found; the module's user.js (declared, " + "placed, in effect, with every declared identity, calendar, address book and tag as it resolved); the " + "declared accounts by count (in the profile's list, other accounts, not signed in yet); the add-ons in the " + "profile; and the preferences that decide how its windows and notifications look. (r)", Run: func(map[string]any) (any, error) { return machine.Status() }, }, { Name: "betterbird_prefs", Description: "Named preferences of Betterbird's profile: each one's value and where it comes from (the " + "module's policy, which locks it; user.js, which wins at the start; prefs.js; or the default), and the " + "default itself with the file that gives it: Betterbird's own, read from omni.ja and the installation's " + "defaults/pref, or the module's defaults (mesh-defaults.js). A name ending in . or * is a prefix (at most " + "200 answered). A preference whose name can hold a credential, or that names an account, a server, a " + "calendar or a person, is answered without its value. (r)", Input: map[string]any{ "names": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "preference names or prefixes, e.g. [\"calendar.alarms.\", \"extensions.activeThemeID\"]"}, }, Run: func(args map[string]any) (any, error) { names, err := texts(args, "names") if err != nil { return nil, err } return machine.Prefs(names) }, }, { Name: "betterbird_config", Description: "Betterbird's preferences through the module's layers, preference by preference: the value its " + "policy locks, the setting prefs (the operator's setup, mesh-wide with this node's layer over it, one entry " + "per preference), Betterbird's own default where the module keeps that, the profile's own saved value and " + "the value in effect. It flags drift: a declared value the profile's user.js does not hold yet, or a value " + "the profile saved where the module keeps Betterbird's default. With keep (names), it proposes this node's " + "layer of the setting prefs holding the profile's values of those preferences beside what the layer holds, " + "through the controller's settings verb, for the operator's Approve; it never writes user.js. (r; a with keep)", Input: map[string]any{ "keep": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "managed preferences whose profile value this machine keeps: proposed as a node layer, e.g. [\"mail.uifontsize\"]"}, }, Run: func(args map[string]any) (any, error) { keep, err := texts(args, "keep") if err != nil { return nil, err } return machine.Config(keep) }, }, { Name: "betterbird_config_apply", Description: "Bring the profile back to the module's configuration: takes the drifting values " + "(betterbird_config) out of the profile's prefs.js, so the module's defaults hold at Betterbird's next " + "start, and says what it cleared, each with the value it had and the one that now holds. Without apply " + "it lists them only. With apply it acts only while Betterbird is not running, because Betterbird " + "rewrites prefs.js when it quits; every other line of prefs.js is kept. (r; a with apply)", Input: map[string]any{ "apply": map[string]any{"type": "boolean", "description": "clear the drifting values (default false: a dry run)"}, }, Run: func(args map[string]any) (any, error) { apply, err := flag(args, "apply", false) if err != nil { return nil, err } return machine.ConfigApply(apply) }, }, { Name: "betterbird_user_js", Description: "The module's user.js — the setting prefs, then the declared accounts' preferences (setting " + "accounts), then the declared identities, calendars, address books and tags — against the profile's " + "user.js: whether it is placed, and whether the running Betterbird started after it was. With apply, place " + "it in the profile (only where the profile has no user.js or the module's own; an operator's file is never " + "overwritten). A declared entry the profile already holds is adopted under its own keys; one it does not " + "hold is created under the module's keys; an ambiguous one is refused alone, with the remedy (novox/hq ADR " + "0317, ADR 0329 §7). What Betterbird rewrites itself — the account and identity lists, the calendars' " + "order, the removals and each declared account's msgFilterRules.dat — is written only while Betterbird is " + "closed, and the answer counts what waits (§8, §9). Accounts' values are withheld in the user.js shown. " + "Betterbird reads it at its next start: betterbird_restart. (r; a with apply)", Input: map[string]any{ "apply": map[string]any{"type": "boolean", "description": "place the declared user.js in the profile (default false)"}, }, Run: func(args map[string]any) (any, error) { apply, err := flag(args, "apply", false) if err != nil { return nil, err } return machine.userJS(apply) }, }, { Name: "betterbird_log", Description: "The last lines Betterbird printed, from the journal of the unit the module starts it as " + "(mesh-betterbird; or mesh-betterbird-reminder-test). With problems, only lines that say error, " + "warning, assertion, crash or failure. Mail addresses and credentials are replaced by . A " + "Betterbird the module did not start prints elsewhere, and the answer says so. (r)", Input: map[string]any{ "lines": map[string]any{"type": "integer", "description": "how many lines (default 100, at most 2000)"}, "problems": map[string]any{"type": "boolean", "description": "only problem lines (default false)"}, "unit": map[string]any{"type": "string", "enum": []string{restartAs, testUnit}, "description": "which start's journal (default " + restartAs + ")"}, }, Run: func(args map[string]any) (any, error) { n, err := whole(args, "lines", 100, 1, 2000) if err != nil { return nil, err } problems, err := flag(args, "problems", false) if err != nil { return nil, err } unit, err := text(args, "unit", false) if err != nil { return nil, err } return machine.Log(n, problems, unit) }, }, { Name: "betterbird_restart", Description: "End Betterbird (asked to quit, forced after 8 s), place the module's user.js when the profile " + "allows it, and start Betterbird again in the operator's desktop session under the account's service " + "manager (unit mesh-betterbird), its output in the journal. Refused while a message is being written " + "(a composer window is open), because the unsent message would be lost, unless discard_unsent is true — " + "ask the operator first. Answers the pids ended and the new main process. (a)", Input: map[string]any{ "discard_unsent": map[string]any{"type": "boolean", "description": "restart even with a composer open, losing what it holds (default false)"}, }, Run: func(args map[string]any) (any, error) { discard, err := flag(args, "discard_unsent", false) if err != nil { return nil, err } return machine.Restart(discard) }, }, { Name: "betterbird_check", Description: "Check what the module promises: Betterbird installed from the mesh's pinned release and not " + "behind the newest published build; the enterprise policy placed and the module's defaults with it; no " + "managed preference drifting from them; Owl at its pin and active; one " + "Betterbird runs; its profile is found; the module's user.js on the machine, valid, placed and in " + "effect; the declared accounts in the profile's list, none refused, each signed in once; per kind, no " + "declared identity, calendar, address book, filter or tag drifting or refused, and no change waiting for " + "Betterbird to close; its output read. Answers ok and each finding with what to do. (r)", Run: func(map[string]any) (any, error) { return machine.Check() }, }, { Name: "betterbird_accounts", Description: "The accounts of the profile Betterbird uses with their values (name, address, user, servers' " + "hosts and ports) and the declared account each is, Owl's marked by name and address (never declared), " + "with counts by kind. Never a password, a token or anything of the login store (novox/hq ADR 0329 §1, §2). " + "With propose, it builds the module's setting accounts from them (every account but Owl's) and proposes " + "it in the mesh-wide layer as it stands, through the controller's settings verb, for the operator's " + "Approve on a channel that proves who answers (novox/hq ADR 0317, ADR 0277); the answer shows the value it " + "proposed. (r; a with propose)", Input: map[string]any{ "propose": map[string]any{"type": "boolean", "description": "build the setting accounts from the profile and propose it to the operator (default false)"}, }, Run: func(args map[string]any) (any, error) { propose, err := flag(args, "propose", false) if err != nil { return nil, err } return machine.ProfileAccounts(propose) }, }, listTool(kindIdentities, "identities (each account's default and its other identities, with name, address, reply-to, organisation and signature)"), listTool(kindCalendars, "calendars (CalDAV with name, address, colour and options; an ICS one by its name only)"), listTool(kindAddressBooks, "address books (CardDAV with name, address and user; a local one by its name)"), listTool(kindFilters, "filters (per declared account: its msgFilterRules.dat as filters, folders by their path within the account; where the module keeps a file it replaced)"), listTool(kindTags, "tags (key, name, colour, place)"), { Name: "betterbird_derive", Description: "Derive the module's settings identities, calendars, address-books, filters and tags (or the kinds " + "given) from the profiles of every desktop node holding Betterbird, laid out per layer: an entry on every " + "such node goes into the mesh-wide layer with this node's value, and the answer lists each field another " + "node holds differently; one on some nodes only goes into those nodes' own layers. Owl's, ICS calendars, " + "local address books and any address carrying a password are left out and named. prefs puts values of the " + "setting prefs into the mesh-wide layer. With propose, the next layer (the mesh-wide first, then each " + "node's) is proposed for the operator's Approve through the controller's settings verb, carrying the layer " + "as it stands beside the derived kinds; refused while a proposal of the module's waits, or while a desktop " + "node does not answer (novox/hq ADR 0329 §10, ADR 0277). here answers this node's profile only, as the " + "other nodes are asked. The values are the operator's own; never a credential. (r; a with propose)", Input: map[string]any{ "kinds": map[string]any{"type": "array", "items": map[string]any{"type": "string", "enum": allKinds}, "description": "the kinds to derive (default all five)"}, "here": map[string]any{"type": "boolean", "description": "this node's profile only (default false)"}, "propose": map[string]any{"type": "boolean", "description": "propose the next layer to the operator (default false)"}, "prefs": map[string]any{"type": "object", "description": "values of the setting prefs for the mesh-wide " + "layer, by preference name, e.g. {\"mail.uifontsize\": 14}"}, "layer": map[string]any{"type": "string", "description": "the layer to propose instead of the next: mesh-wide or a node's name"}, }, Run: func(args map[string]any) (any, error) { kinds, err := texts(args, "kinds") if err != nil { return nil, err } here, err := flag(args, "here", false) if err != nil { return nil, err } propose, err := flag(args, "propose", false) if err != nil { return nil, err } layer, err := text(args, "layer", false) if err != nil { return nil, err } var prefs map[string]any if v, ok := args["prefs"]; ok && v != nil { if prefs, ok = v.(map[string]any); !ok { return nil, fmt.Errorf("prefs is an object of preference names and values, not %T", v) } } return machine.Derive(kinds, here, propose, prefs, layer) }, }, { Name: "betterbird_drift", Description: "How this node's profile differs from the declared identities, calendars, address books, " + "filters and tags (novox/hq ADR 0329 §11): per declared entry whether the profile holds it, holds another " + "value (each differing field with the declared value and the profile's), lacks it, or it is refused; and " + "the profile's entries nothing declares, which stay the person's. The profile as Betterbird reads it at " + "its next start. (r)", Input: map[string]any{ "kind": map[string]any{"type": "string", "enum": allKinds, "description": "one kind (default every kind)"}, }, Run: func(args map[string]any) (any, error) { kind, err := text(args, "kind", false) if err != nil { return nil, err } return machine.Drift(kind) }, }, { Name: "betterbird_reminder_test", Description: "OPENS WINDOWS ON THE OPERATOR'S SCREEN: tell the operator before calling it. Shows a calendar " + "reminder without touching the operator's calendar: a second Betterbird in a throw-away profile under " + "the runtime directory (--no-remote, unit mesh-betterbird-reminder-test, at most 10 minutes), its main " + "window marked MESH TEST in red, with the module's user.js (no accounts) and one local calendar file " + "holding one event, \"MESH TEST reminder\", whose reminder is due after `after` seconds. carry copies " + "named preferences of the operator's profile (a theme, a colour switch) into it, never a credential or " + "an account. Then xorg_windows (class Calendar) and xorg_screenshot show the reminder window. stop ends " + "it and removes the profile. (a)", Input: map[string]any{ "after": map[string]any{"type": "integer", "description": "seconds until the reminder is due (default 40, 15 to 300)"}, "carry": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "preference names to copy from the operator's profile, e.g. [\"extensions.activeThemeID\"]"}, "stop": map[string]any{"type": "boolean", "description": "end the test Betterbird and remove its profile (default false)"}, }, Run: func(args map[string]any) (any, error) { after, err := whole(args, "after", 40, 15, 300) if err != nil { return nil, err } carry, err := texts(args, "carry") if err != nil { return nil, err } stop, err := flag(args, "stop", false) if err != nil { return nil, err } return machine.ReminderTest(time.Duration(after)*time.Second, carry, stop) }, }, } }