aboutsummaryrefslogtreecommitdiffstats
path: root/config/accounts.go
diff options
context:
space:
mode:
Diffstat (limited to 'config/accounts.go')
-rw-r--r--config/accounts.go256
1 files changed, 256 insertions, 0 deletions
diff --git a/config/accounts.go b/config/accounts.go
new file mode 100644
index 00000000..e168231f
--- /dev/null
+++ b/config/accounts.go
@@ -0,0 +1,256 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "os/exec"
+ "path"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "git.sr.ht/~rjarry/aerc/logging"
+ "github.com/go-ini/ini"
+)
+
+type RemoteConfig struct {
+ Value string
+ PasswordCmd string
+ CacheCmd bool
+ cache string
+}
+
+func (c *RemoteConfig) parseValue() (*url.URL, error) {
+ return url.Parse(c.Value)
+}
+
+func (c *RemoteConfig) ConnectionString() (string, error) {
+ if c.Value == "" || c.PasswordCmd == "" {
+ return c.Value, nil
+ }
+
+ u, err := c.parseValue()
+ if err != nil {
+ return "", err
+ }
+
+ // ignore the command if a password is specified
+ if _, exists := u.User.Password(); exists {
+ return c.Value, nil
+ }
+
+ // don't attempt to parse the command if the url is a path (ie /usr/bin/sendmail)
+ if !u.IsAbs() {
+ return c.Value, nil
+ }
+
+ pw := c.cache
+
+ if pw == "" {
+ cmd := exec.Command("sh", "-c", c.PasswordCmd)
+ cmd.Stdin = os.Stdin
+ output, err := cmd.Output()
+ if err != nil {
+ return "", fmt.Errorf("failed to read password: %w", err)
+ }
+ pw = strings.TrimSpace(string(output))
+ }
+ u.User = url.UserPassword(u.User.Username(), pw)
+ if c.CacheCmd {
+ c.cache = pw
+ }
+
+ return u.String(), nil
+}
+
+type AccountConfig struct {
+ Archive string
+ CopyTo string
+ Default string
+ Postpone string
+ From string
+ Aliases string
+ Name string
+ Source string
+ Folders []string
+ FoldersExclude []string
+ Params map[string]string
+ Outgoing RemoteConfig
+ SignatureFile string
+ SignatureCmd string
+ EnableFoldersSort bool `ini:"enable-folders-sort"`
+ FoldersSort []string `ini:"folders-sort" delim:","`
+ AddressBookCmd string `ini:"address-book-cmd"`
+ SendAsUTC bool `ini:"send-as-utc"`
+ LocalizedRe *regexp.Regexp
+
+ // CheckMail
+ CheckMail time.Duration `ini:"check-mail"`
+ CheckMailCmd string `ini:"check-mail-cmd"`
+ CheckMailTimeout time.Duration `ini:"check-mail-timeout"`
+ CheckMailInclude []string `ini:"check-mail-include"`
+ CheckMailExclude []string `ini:"check-mail-exclude"`
+
+ // PGP Config
+ PgpKeyId string `ini:"pgp-key-id"`
+ PgpAutoSign bool `ini:"pgp-auto-sign"`
+ PgpOpportunisticEncrypt bool `ini:"pgp-opportunistic-encrypt"`
+
+ // AuthRes
+ TrustedAuthRes []string `ini:"trusted-authres" delim:","`
+}
+
+func (config *AercConfig) parseAccounts(root string, accts []string) error {
+ filename := path.Join(root, "accounts.conf")
+ if !config.General.UnsafeAccountsConf {
+ if err := checkConfigPerms(filename); err != nil {
+ return err
+ }
+ }
+
+ logging.Infof("Parsing accounts configuration from %s", filename)
+
+ file, err := ini.Load(filename)
+ if err != nil {
+ // No config triggers account configuration wizard
+ return nil
+ }
+ file.NameMapper = mapName
+
+ for _, _sec := range file.SectionStrings() {
+ if _sec == "DEFAULT" {
+ continue
+ }
+ if len(accts) > 0 && !contains(accts, _sec) {
+ continue
+ }
+ sec := file.Section(_sec)
+ sourceRemoteConfig := RemoteConfig{}
+ account := AccountConfig{
+ Archive: "Archive",
+ Default: "INBOX",
+ Postpone: "Drafts",
+ Name: _sec,
+ Params: make(map[string]string),
+ EnableFoldersSort: true,
+ CheckMailTimeout: 10 * time.Second,
+ // localizedRe contains a list of known translations for the common Re:
+ LocalizedRe: regexp.MustCompile(`(?i)^((AW|RE|SV|VS|ODP|R): ?)+`),
+ }
+ if err = sec.MapTo(&account); err != nil {
+ return err
+ }
+ for key, val := range sec.KeysHash() {
+ switch key {
+ case "folders":
+ folders := strings.Split(val, ",")
+ sort.Strings(folders)
+ account.Folders = folders
+ case "folders-exclude":
+ folders := strings.Split(val, ",")
+ sort.Strings(folders)
+ account.FoldersExclude = folders
+ case "source":
+ sourceRemoteConfig.Value = val
+ case "source-cred-cmd":
+ sourceRemoteConfig.PasswordCmd = val
+ case "outgoing":
+ account.Outgoing.Value = val
+ case "outgoing-cred-cmd":
+ account.Outgoing.PasswordCmd = val
+ case "outgoing-cred-cmd-cache":
+ cache, err := strconv.ParseBool(val)
+ if err != nil {
+ return fmt.Errorf("%s=%s %w", key, val, err)
+ }
+ account.Outgoing.CacheCmd = cache
+ case "from":
+ account.From = val
+ case "aliases":
+ account.Aliases = val
+ case "copy-to":
+ account.CopyTo = val
+ case "archive":
+ account.Archive = val
+ case "enable-folders-sort":
+ account.EnableFoldersSort, _ = strconv.ParseBool(val)
+ case "pgp-key-id":
+ account.PgpKeyId = val
+ case "pgp-auto-sign":
+ account.PgpAutoSign, _ = strconv.ParseBool(val)
+ case "pgp-opportunistic-encrypt":
+ account.PgpOpportunisticEncrypt, _ = strconv.ParseBool(val)
+ case "address-book-cmd":
+ account.AddressBookCmd = val
+ case "subject-re-pattern":
+ re, err := regexp.Compile(val)
+ if err != nil {
+ return fmt.Errorf("%s=%s %w", key, val, err)
+ }
+ account.LocalizedRe = re
+ default:
+ if key != "name" {
+ account.Params[key] = val
+ }
+ }
+ }
+ if account.Source == "" {
+ return fmt.Errorf("Expected source for account %s", _sec)
+ }
+ if account.From == "" {
+ return fmt.Errorf("Expected from for account %s", _sec)
+ }
+
+ source, err := sourceRemoteConfig.ConnectionString()
+ if err != nil {
+ return fmt.Errorf("Invalid source credentials for %s: %w", _sec, err)
+ }
+ account.Source = source
+
+ _, err = account.Outgoing.parseValue()
+ if err != nil {
+ return fmt.Errorf("Invalid outgoing credentials for %s: %w", _sec, err)
+ }
+
+ logging.Debugf("accounts.conf: [%s] from = %s", account.Name, account.From)
+ config.Accounts = append(config.Accounts, account)
+ }
+ if len(accts) > 0 {
+ // Sort accounts struct to match the specified order, if we
+ // have one
+ if len(config.Accounts) != len(accts) {
+ return errors.New("account(s) not found")
+ }
+ sort.Slice(config.Accounts, func(i, j int) bool {
+ return strings.ToLower(accts[i]) < strings.ToLower(accts[j])
+ })
+ }
+
+ return nil
+}
+
+// checkConfigPerms checks for too open permissions
+// printing the fix on stdout and returning an error
+func checkConfigPerms(filename string) error {
+ info, err := os.Stat(filename)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil // disregard absent files
+ }
+ if err != nil {
+ return err
+ }
+
+ perms := info.Mode().Perm()
+ // group or others have read access
+ if perms&0o44 != 0 {
+ fmt.Fprintf(os.Stderr, "The file %v has too open permissions.\n", filename)
+ fmt.Fprintln(os.Stderr, "This is a security issue (it contains passwords).")
+ fmt.Fprintf(os.Stderr, "To fix it, run `chmod 600 %v`\n", filename)
+ return errors.New("account.conf permissions too lax")
+ }
+ return nil
+}