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..…. 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) }