// 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, calendars, accounts and credentials are never read: a tool reads processes, // profiles.ini, and of the profile only prefs.js, user.js and extensions.json, and answers no // preference that can hold a credential or names an account. package main import ( "fmt" "os" "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 { 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); the declared accounts by count (in the profile's list, other accounts, not signed " + "in yet), never an address, host or name; 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: "The configuration the module ships, preference by preference: the value its policy locks, " + "its default (mesh-defaults.js, or Betterbird's own where the module keeps that), the node layer (the " + "setting prefs on this machine), the profile's own saved value and the value in effect. It flags drift: " + "a value the profile saved that beats the module's default without a node layer saying so. With keep " + "(names), it proposes a node layer of the setting prefs for this machine holding the profile's values of " + "those preferences, 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 preferences the module declares (setting prefs), then the declared " + "accounts' preferences under the module's own keys (setting accounts) — 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 account the profile already holds at its address is adopted under its own keys; " + "one it does not hold is created under the module's keys, which are added to the account lists when " + "Betterbird is not running; an ambiguous one is refused alone, with the remedy (novox/hq ADR 0317). " + "Accounts' values are withheld in the answer. 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; its output " + "read. Answers ok and each finding with what to do, never an address, host or name. (r)", Run: func(map[string]any) (any, error) { return machine.Check() }, }, { Name: "betterbird_accounts", Description: "The accounts of the profile Betterbird uses, by count and kind: the IMAP and POP3 accounts a " + "declaration can hold, the accounts Owl keeps (never declared), and the rest. Never an address, a host " + "or a name, and never anything of the login store. With propose, it builds the module's setting " + "accounts from them (every account but Owl's, without any password or token) and proposes it through " + "the controller's settings verb for the operator's Approve on a channel that proves who answers " + "(novox/hq ADR 0317, ADR 0277): nothing changes until then, and the operator reads the value whole at " + "their desk. (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) }, }, { 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) }, }, } }