diff options
Diffstat (limited to 'config/accounts.go')
-rw-r--r-- | config/accounts.go | 256 |
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 +} |