mesh/merge-gate head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
The module owned only the accounts' server fields, so identities, signatures, calendars, address books, filters and tags could differ between desktop nodes unseen, and the operator's own preferences were shipped in the catalogue. prefs becomes one entry per preference (a node's layer overrides one key) and the defaults file keeps no value of the operator's; identities, calendars, address-books, filters and tags become settings, adopted like accounts, filters owned whole per account, and what Betterbird rewrites written only while it is closed. New tools list each kind, derive the layers from every desktop node and propose one at a time, and show drift; every answer passes a credential check.
179 lines
5.8 KiB
Go
179 lines
5.8 KiB
Go
package main
|
|
|
|
// The profile's identities, calendars, address books and tags, read from its preferences into the setting's
|
|
// shape (novox/hq ADR 0329). The profile is read as Betterbird starts with it: prefs.js, with user.js over it.
|
|
// Nothing here reads the login store, a calendar's events or an address book's contacts (§13).
|
|
|
|
import (
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// profileIdentity is one identity the profile holds.
|
|
type profileIdentity struct {
|
|
Key string
|
|
AccountKey string
|
|
// Default says it is its account's first identity.
|
|
Default bool
|
|
Owl bool
|
|
Value Identity
|
|
}
|
|
|
|
// profileIdentities reads every identity of every account in the profile's list, the default first.
|
|
func (m *Machine) profileIdentities(prefs map[string]string) []profileIdentity {
|
|
var out []profileIdentity
|
|
for _, a := range profileAccounts(prefs) {
|
|
for i, id := range a.Identities {
|
|
out = append(out, profileIdentity{Key: id, AccountKey: a.Key, Default: i == 0, Owl: a.Owl,
|
|
Value: m.readIdentity(prefs, id)})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// readIdentity reads one identity's fields. A signature the profile reads from a file is read as its text, since
|
|
// a file's path differs per node (§10).
|
|
func (m *Machine) readIdentity(prefs map[string]string, key string) Identity {
|
|
p := "mail.identity." + key + "."
|
|
text := func(name string) string { return unquote(prefs[p+name]) }
|
|
id := Identity{Address: text("useremail"), Name: text("fullName"), ReplyTo: text("reply_to"),
|
|
Organization: text("organization"), SignatureHTML: prefs[p+"htmlSigFormat"] == "true",
|
|
SignatureAboveQuote: prefs[p+"reply_on_top"] == "1" && prefs[p+"sig_bottom"] == "false"}
|
|
sig := text("htmlSigText")
|
|
if prefs[p+"attach_signature"] == "true" {
|
|
if file := text("sig_file"); file != "" {
|
|
sig = m.signatureFile(file)
|
|
}
|
|
}
|
|
if sig = strings.TrimRight(strings.ReplaceAll(sig, "\r\n", "\n"), "\n"); sig != "" {
|
|
id.Signature = strings.Split(sig, "\n")
|
|
}
|
|
return id
|
|
}
|
|
|
|
// signatureFile is a signature file's text, read only within the home and bounded.
|
|
func (m *Machine) signatureFile(file string) string {
|
|
clean := filepath.Clean(file)
|
|
if !strings.HasPrefix(clean, strings.TrimSuffix(m.Home, "/")+"/") {
|
|
return ""
|
|
}
|
|
raw, err := readBounded(m.path(clean))
|
|
if err != nil || len(raw) > 64<<10 {
|
|
return ""
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
// keysUnder are the distinct second-level keys of the preferences under a prefix: calendar.registry.<key>.….
|
|
func keysUnder(prefs map[string]string, prefix string) []string {
|
|
seen := map[string]bool{}
|
|
for name := range prefs {
|
|
if rest, ok := strings.CutPrefix(name, prefix); ok {
|
|
if key, _, found := strings.Cut(rest, "."); found && key != "" {
|
|
seen[key] = true
|
|
}
|
|
}
|
|
}
|
|
return sortedKeys(seen)
|
|
}
|
|
|
|
// profileCalendar is one calendar of the profile: Kind is Betterbird's (caldav, ics, storage, owl …).
|
|
type profileCalendar struct {
|
|
Key string
|
|
Kind string
|
|
// IdentityKey is the identity that answers its invitations.
|
|
IdentityKey string
|
|
Value Calendar
|
|
}
|
|
|
|
func profileCalendars(prefs map[string]string) []profileCalendar {
|
|
var out []profileCalendar
|
|
for _, key := range keysUnder(prefs, "calendar.registry.") {
|
|
p := "calendar.registry." + key + "."
|
|
text := func(name string) string { return unquote(prefs[p+name]) }
|
|
c := profileCalendar{Key: key, Kind: text("type"), IdentityKey: text("imip.identity.key"),
|
|
Value: Calendar{Name: text("name"), URL: text("uri"), Color: strings.ToLower(text("color")),
|
|
ReadOnly: prefs[p+"readOnly"] == "true", User: text("username")}}
|
|
if prefs[p+"suppressAlarms"] == "true" {
|
|
off := false
|
|
c.Value.Reminders = &off
|
|
}
|
|
if c.Kind == "" && c.Value.URL == "" {
|
|
continue
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// profileAddressBook is one address book of the profile: CardDAV is the kind a setting holds.
|
|
type profileAddressBook struct {
|
|
Key string
|
|
CardDAV bool
|
|
Kind string
|
|
Value AddressBook
|
|
}
|
|
|
|
// Betterbird's kinds of address book (dirType).
|
|
var addressBookKinds = map[string]string{"0": "LDAP", "101": "local", "102": "CardDAV"}
|
|
|
|
func profileAddressBooks(prefs map[string]string) []profileAddressBook {
|
|
var out []profileAddressBook
|
|
for _, key := range keysUnder(prefs, "ldap_2.servers.") {
|
|
p := "ldap_2.servers." + key + "."
|
|
if _, has := prefs[p+"dirType"]; !has && prefs[p+"description"] == "" {
|
|
continue
|
|
}
|
|
kind := addressBookKinds[prefs[p+"dirType"]]
|
|
if kind == "" {
|
|
kind = "other"
|
|
}
|
|
out = append(out, profileAddressBook{Key: key, CardDAV: kind == "CardDAV", Kind: kind,
|
|
Value: AddressBook{Name: unquote(prefs[p+"description"]), URL: unquote(prefs[p+"carddav.url"]),
|
|
User: unquote(prefs[p+"carddav.username"])}})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// profileTag is one tag of the profile.
|
|
type profileTag struct {
|
|
Key string
|
|
ordinal string
|
|
Value Tag
|
|
}
|
|
|
|
// profileTags reads the tags, each with its place as Betterbird orders them.
|
|
func profileTags(prefs map[string]string) []profileTag {
|
|
var out []profileTag
|
|
for _, key := range keysUnder(prefs, "mailnews.tags.") {
|
|
p := "mailnews.tags." + key + "."
|
|
if _, has := prefs[p+"tag"]; !has {
|
|
continue
|
|
}
|
|
out = append(out, profileTag{Key: key, ordinal: unquote(prefs[p+"ordinal"]),
|
|
Value: Tag{Name: unquote(prefs[p+"tag"]), Color: strings.ToUpper(unquote(prefs[p+"color"]))}})
|
|
}
|
|
// Betterbird orders the tags by their ordinal, a tag without one by its key in its place.
|
|
at := func(t profileTag) string {
|
|
if t.ordinal != "" {
|
|
return t.ordinal
|
|
}
|
|
return t.Key
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
if at(out[i]) != at(out[j]) {
|
|
return at(out[i]) < at(out[j])
|
|
}
|
|
return out[i].Key < out[j].Key
|
|
})
|
|
for i := range out {
|
|
out[i].Value.Order = i + 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
// tagOrdinal is the ordinal a tag's place is written as, so Betterbird's order by text is the declared one.
|
|
func tagOrdinal(order int) string { return strconv.Itoa(1000 + order) }
|