mesh/merge-gate pass: builds betterbird, sent nowhere; no bus step; 1 wait(s) for a person; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
The shared-server check refused an adopted account for its own alias, and counted Owl's, local folders' and feeds' identities as users of the default server, though they never send by SMTP. The controller now judges an upstream pin's host like any name, so each pin declares its vendor.
331 lines
12 KiB
Go
331 lines
12 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"
|
|
"sort"
|
|
"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 refuseSharedOutgoing(out, prefs, have)
|
|
}
|
|
|
|
// refuseSharedOutgoing refuses an adopted account whose outgoing server another identity uses that is neither
|
|
// adopted nor the module's (review 34 of mesh-catalog #232): Betterbird lets identities share an outgoing
|
|
// server, and the declared values written into it would change that other account's sending at every start.
|
|
// ADR 0317 §2 adopts the outgoing server the identity names and leaves the lists as they are, so the module
|
|
// does not give the identity a server of its own; it refuses, with the remedy.
|
|
func refuseSharedOutgoing(resolved []resolvedAccount, prefs map[string]string, have []profileAccount) []resolvedAccount {
|
|
// Every identity of an adopted or created account is its own, its aliases too (review 36).
|
|
accountOf := map[string]profileAccount{}
|
|
kindOf := map[string]string{}
|
|
for _, a := range have {
|
|
accountOf[a.Key] = a
|
|
for _, id := range a.Identities {
|
|
kindOf[id] = a.Kind
|
|
}
|
|
}
|
|
ours := map[string]bool{}
|
|
for _, r := range resolved {
|
|
if r.How == refused {
|
|
continue
|
|
}
|
|
ours[r.Keys.Identity] = true
|
|
for _, id := range accountOf[r.Keys.Account].Identities {
|
|
ours[id] = true
|
|
}
|
|
}
|
|
defaultServer := unquote(prefs["mail.smtp.defaultserver"])
|
|
users := map[string][]string{}
|
|
seen := map[string]bool{}
|
|
use := func(id string) {
|
|
if seen[id] || ours[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
server := unquote(prefs["mail.identity."+id+".smtpServer"])
|
|
// Only a mail account's identity sends through the default server when it names none: Owl's, local
|
|
// folders and feeds do not send by SMTP (review 36).
|
|
if server == "" && (kindOf[id] == "imap" || kindOf[id] == "pop3") {
|
|
server = defaultServer
|
|
}
|
|
if server != "" {
|
|
users[server] = append(users[server], id)
|
|
}
|
|
}
|
|
for _, a := range have {
|
|
for _, id := range a.Identities {
|
|
use(id)
|
|
}
|
|
}
|
|
for k := range prefs {
|
|
if strings.HasPrefix(k, "mail.identity.") && strings.HasSuffix(k, ".smtpServer") {
|
|
use(strings.TrimSuffix(strings.TrimPrefix(k, "mail.identity."), ".smtpServer"))
|
|
}
|
|
}
|
|
for i, r := range resolved {
|
|
if r.How != adopted {
|
|
continue
|
|
}
|
|
if others := users[r.Keys.SMTP]; len(others) > 0 {
|
|
sort.Strings(others)
|
|
resolved[i] = resolvedAccount{Place: r.Place, How: refused, name: r.name, Why: fmt.Sprintf(
|
|
"declared account %d of %d: the outgoing server %s of the profile's account %s is also used by "+
|
|
"identity %s, whose sending the declared values would change: give the account an outgoing "+
|
|
"server of its own in Betterbird's account settings, then start Betterbird again",
|
|
r.Place, len(resolved), r.Keys.SMTP, r.Keys.Account, strings.Join(others, ", "))}
|
|
}
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
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
|
|
if was.How == adopted {
|
|
// The identity's outgoing server as it is now: a person may have moved it since.
|
|
smtpKey := unquote(prefs["mail.identity."+was.Keys.Identity+".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", was.Keys.Account)
|
|
}
|
|
r.Keys.SMTP = smtpKey
|
|
}
|
|
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
|
|
}
|