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.
252 lines
9.1 KiB
Go
252 lines
9.1 KiB
Go
package main
|
|
|
|
// Which account of the profile a declared account is (novox/hq ADR 0317): adopted, created or refused.
|
|
//
|
|
// At every apply, per declared account in its place in the setting (§1):
|
|
//
|
|
// 1. an account the module recorded on this node for this profile, adopted or created, is kept by its
|
|
// recorded keys without matching again, while its account key is still in the profile's account list;
|
|
// 2. otherwise an account under the module's own mesh_<name> keys is the module's creation;
|
|
// 3. otherwise a node's layer may name the key to adopt (§8): adopted only when it is in the account list,
|
|
// its default identity has the declared address and it is not Owl's;
|
|
// 4. otherwise the profile's accounts whose default identity has the declared address: one is adopted, none
|
|
// means the account is created under mesh_<name> keys (ADR 0316 §11).
|
|
//
|
|
// Refused, with the remedy, for that account alone (§7): two or more accounts with the address, the one that
|
|
// has it being Owl's, a named key that does not qualify, an account of another kind than the declaration says
|
|
// (the module never converts one: the review of ADR 0317), and an identity that names no outgoing server. The
|
|
// other declared accounts go ahead.
|
|
//
|
|
// Adopting takes the account's keys as the module's (§2): its account key, its default identity, its incoming
|
|
// server and the outgoing server that identity names. The account lists are left as they are, because the keys
|
|
// are in them already; the declared fields are written under those keys (§4, §5). What was adopted or created
|
|
// is recorded in the module's directory in the home, by the tools and never by a setting (§3); leaving the
|
|
// setting releases an adopted account (its lines and its record go, the account stays the person's, §6).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// recordFile is where the module records, per profile, what it adopted or created (ADR 0317 §3).
|
|
const recordFile = ".config/betterbird/accounts-record.json"
|
|
|
|
// How a declared account resolved.
|
|
const (
|
|
adopted = "adopted"
|
|
created = "created"
|
|
refused = "refused"
|
|
)
|
|
|
|
// keySet are an account's four keys.
|
|
type keySet struct {
|
|
Account string `json:"account"`
|
|
Identity string `json:"identity"`
|
|
Server string `json:"server"`
|
|
SMTP string `json:"smtp"`
|
|
}
|
|
|
|
// resolvedAccount is one declared account as it resolved, said by its place and keys, never its name, address
|
|
// or host (ADR 0316 §16, ADR 0317 §3).
|
|
type resolvedAccount struct {
|
|
Place int `json:"place"`
|
|
How string `json:"how"`
|
|
Keys keySet `json:"keys,omitempty"`
|
|
// Why is a refusal's reason and its remedy.
|
|
Why string `json:"why,omitempty"`
|
|
// Changed are the preferences an apply changed for it, by name only (ADR 0317 §4).
|
|
Changed []string `json:"changed,omitempty"`
|
|
name string
|
|
}
|
|
|
|
// record is what the module adopted or created, per profile (its directory as the tools say it), per
|
|
// declared account's name.
|
|
type record map[string]map[string]recorded
|
|
|
|
type recorded struct {
|
|
How string `json:"how"`
|
|
Keys keySet `json:"keys"`
|
|
}
|
|
|
|
func (m *Machine) readRecord(profileDir string) map[string]recorded {
|
|
raw, err := readBounded(m.home(recordFile))
|
|
if err != nil {
|
|
return map[string]recorded{}
|
|
}
|
|
var r record
|
|
if json.Unmarshal(raw, &r) != nil || r[profileDir] == nil {
|
|
return map[string]recorded{}
|
|
}
|
|
return r[profileDir]
|
|
}
|
|
|
|
// writeRecord keeps what this apply resolved for the profile: a refused account is not recorded, and an
|
|
// account no longer declared is dropped (§6).
|
|
func (m *Machine) writeRecord(profileDir string, resolved []resolvedAccount) error {
|
|
r := record{}
|
|
if raw, err := readBounded(m.home(recordFile)); err == nil {
|
|
_ = json.Unmarshal(raw, &r)
|
|
}
|
|
mine := map[string]recorded{}
|
|
for _, a := range resolved {
|
|
if a.How != refused {
|
|
mine[a.name] = recorded{How: a.How, Keys: a.Keys}
|
|
}
|
|
}
|
|
if len(mine) == 0 {
|
|
delete(r, profileDir)
|
|
} else {
|
|
r[profileDir] = mine
|
|
}
|
|
body, err := json.MarshalIndent(r, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := m.home(recordFile)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".mesh-new"
|
|
if err := os.WriteFile(tmp, append(body, '\n'), 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(tmp, path); err != nil {
|
|
os.Remove(tmp)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// resolveAccounts resolves every declared account, in its place in the setting (the file's order, by name).
|
|
func resolveAccounts(accounts map[string]Account, prefs map[string]string, rec map[string]recorded) []resolvedAccount {
|
|
have := profileAccounts(prefs)
|
|
inList := map[string]profileAccount{}
|
|
for _, a := range have {
|
|
inList[a.Key] = a
|
|
}
|
|
names := sortedNames(accounts)
|
|
taken := map[string]bool{}
|
|
var out []resolvedAccount
|
|
for i, name := range names {
|
|
r := resolveOne(i+1, len(names), name, accounts[name], prefs, have, inList, rec, taken)
|
|
r.name = name
|
|
if r.How != refused {
|
|
taken[r.Keys.Account] = true
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func resolveOne(place, of int, name string, a Account, prefs map[string]string, have []profileAccount,
|
|
inList map[string]profileAccount, rec map[string]recorded, taken map[string]bool) resolvedAccount {
|
|
r := resolvedAccount{Place: place}
|
|
refuse := func(format string, args ...any) resolvedAccount {
|
|
r.How, r.Keys = refused, keySet{}
|
|
r.Why = fmt.Sprintf("declared account %d of %d: ", place, of) + fmt.Sprintf(format, args...)
|
|
return r
|
|
}
|
|
// 1. Recorded, while its account is still in the list.
|
|
if was, ok := rec[name]; ok {
|
|
if _, there := inList[was.Keys.Account]; there || was.How == created {
|
|
r.How, r.Keys = was.How, was.Keys
|
|
return r
|
|
}
|
|
}
|
|
// 2. Under the module's own keys: its creation.
|
|
account, identity, server, smtp := keysOf(name)
|
|
own := keySet{account, identity, server, smtp}
|
|
if _, there := inList[account]; there {
|
|
r.How, r.Keys = created, own
|
|
return r
|
|
}
|
|
adopt := func(pa profileAccount) resolvedAccount {
|
|
if pa.Kind != a.Incoming.Kind {
|
|
return refuse("the profile's account %s is of kind %s and the declaration says %s: the module never "+
|
|
"converts an account. Declare the kind the account has, or remove the account in Betterbird",
|
|
pa.Key, pa.Kind, a.Incoming.Kind)
|
|
}
|
|
smtpKey := unquote(prefs["mail.identity."+pa.Identities[0]+".smtpServer"])
|
|
if smtpKey == "" {
|
|
return refuse("the profile's account %s has an identity that names no outgoing server: choose one for it "+
|
|
"in Betterbird's account settings, then start Betterbird again", pa.Key)
|
|
}
|
|
r.How, r.Keys = adopted, keySet{pa.Key, pa.Identities[0], pa.Server, smtpKey}
|
|
return r
|
|
}
|
|
matchesDefault := func(pa profileAccount) bool {
|
|
return len(pa.Addresses) > 0 && strings.EqualFold(pa.Addresses[0], a.Address)
|
|
}
|
|
// 3. The key a node's layer names.
|
|
if a.Key != "" {
|
|
pa, there := inList[a.Key]
|
|
switch {
|
|
case !there:
|
|
return refuse("the key %s this node's layer names is not in the profile's account list", a.Key)
|
|
case pa.Owl:
|
|
return refuse("the key %s this node's layer names is Owl's account, which is never declared "+
|
|
"(novox/hq ADR 0316 §13)", a.Key)
|
|
case !matchesDefault(pa):
|
|
return refuse("the key %s this node's layer names is an account whose default identity has another "+
|
|
"address", a.Key)
|
|
case taken[pa.Key]:
|
|
return refuse("the key %s is already another declared account's", a.Key)
|
|
}
|
|
return adopt(pa)
|
|
}
|
|
// 4. By the default identity's address.
|
|
var matches []profileAccount
|
|
for _, pa := range have {
|
|
if !pa.Ours && !taken[pa.Key] && matchesDefault(pa) {
|
|
matches = append(matches, pa)
|
|
}
|
|
}
|
|
switch {
|
|
case len(matches) == 0:
|
|
r.How, r.Keys = created, own
|
|
return r
|
|
case len(matches) > 1:
|
|
var keys []string
|
|
for _, pa := range matches {
|
|
keys = append(keys, pa.Key)
|
|
}
|
|
return refuse("%d accounts in the profile have its address (%s): remove the duplicate in Betterbird, or "+
|
|
"give the one to adopt as key in this node's layer of the setting accounts (novox/hq ADR 0317 §7, §8)",
|
|
len(matches), strings.Join(keys, ", "))
|
|
case matches[0].Owl:
|
|
return refuse("the profile's account with its address, %s, is Owl's, which is never declared (novox/hq "+
|
|
"ADR 0316 §13): take it out of the setting accounts", matches[0].Key)
|
|
}
|
|
return adopt(matches[0])
|
|
}
|
|
|
|
// changedPrefs are the preferences of a resolved account whose declared value differs from the profile's
|
|
// saved one (prefs.js), by name only (ADR 0317 §4). A preference prefs.js does not hold is changed unless the
|
|
// declared value is what Betterbird reads in its place.
|
|
func changedPrefs(r resolvedAccount, a Account, saved map[string]string) []string {
|
|
defaults := map[string]string{}
|
|
if r.How == adopted {
|
|
// What Betterbird reads when prefs.js holds no value: the same defaults betterbird_accounts reads with.
|
|
pa := profileAccount{Key: r.Keys.Account, Server: r.Keys.Server, Kind: a.Incoming.Kind,
|
|
Identities: []string{r.Keys.Identity}}
|
|
view := readAccount(saved, pa)
|
|
for _, p := range declaredPrefs(r.Keys, view, true) {
|
|
defaults[p[0]] = p[1]
|
|
}
|
|
}
|
|
var out []string
|
|
for _, p := range declaredPrefs(r.Keys, a, r.How == adopted) {
|
|
have, ok := saved[p[0]]
|
|
if !ok {
|
|
have, ok = defaults[p[0]]
|
|
}
|
|
if !ok || have != p[1] {
|
|
out = append(out, p[0])
|
|
}
|
|
}
|
|
return out
|
|
}
|