diff options
Diffstat (limited to 'config')
-rw-r--r-- | config/accounts.go | 256 | ||||
-rw-r--r-- | config/config.go | 251 |
2 files changed, 257 insertions, 250 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 +} diff --git a/config/config.go b/config/config.go index 17c88834..6a16234b 100644 --- a/config/config.go +++ b/config/config.go @@ -4,13 +4,9 @@ import ( "errors" "fmt" "log" - "net/url" "os" - "os/exec" "path" "regexp" - "sort" - "strconv" "strings" "time" "unicode" @@ -106,93 +102,6 @@ const ( FILTER_HEADER ) -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:","` -} - type BindingConfig struct { Global *KeyBindings AccountWizard *KeyBindings @@ -287,127 +196,6 @@ func mapName(raw string) string { return string(newstr) } -func loadAccountConfig(path string, accts []string) ([]AccountConfig, error) { - file, err := ini.Load(path) - if err != nil { - // No config triggers account configuration wizard - return nil, nil - } - file.NameMapper = mapName - - var accounts []AccountConfig - 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 nil, 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 nil, 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 nil, fmt.Errorf( - "%s=%s %w", key, val, err) - } - account.LocalizedRe = re - default: - if key != "name" { - account.Params[key] = val - } - } - } - if account.Source == "" { - return nil, fmt.Errorf("Expected source for account %s", _sec) - } - if account.From == "" { - return nil, fmt.Errorf("Expected from for account %s", _sec) - } - - source, err := sourceRemoteConfig.ConnectionString() - if err != nil { - return nil, fmt.Errorf("Invalid source credentials for %s: %w", _sec, err) - } - account.Source = source - - _, err = account.Outgoing.parseValue() - if err != nil { - return nil, fmt.Errorf("Invalid outgoing credentials for %s: %w", _sec, err) - } - - accounts = append(accounts, account) - } - if len(accts) > 0 { - // Sort accounts struct to match the specified order, if we - // have one - if len(accounts) != len(accts) { - return nil, errors.New("account(s) not found") - } - sort.Slice(accounts, func(i, j int) bool { - return strings.ToLower(accts[i]) < strings.ToLower(accts[j]) - }) - } - return accounts, nil -} - // Set at build time var shareDir string @@ -898,23 +686,8 @@ func LoadConfigFromFile(root *string, accts []string) (*AercConfig, error) { logging.Debugf("aerc.conf: [triggers] %#v", config.Triggers) logging.Debugf("aerc.conf: [templates] %#v", config.Templates) - filename = path.Join(*root, "accounts.conf") - if !config.General.UnsafeAccountsConf { - if err := checkConfigPerms(filename); err != nil { - return nil, err - } - } - - accountsPath := path.Join(*root, "accounts.conf") - logging.Infof("Parsing accounts configuration from %s", accountsPath) - if accounts, err := loadAccountConfig(accountsPath, accts); err != nil { + if err := config.parseAccounts(*root, accts); err != nil { return nil, err - } else { - config.Accounts = accounts - } - - for _, acct := range config.Accounts { - logging.Debugf("accounts.conf: [%s] from = %s", acct.Name, acct.From) } filename = path.Join(*root, "binds.conf") @@ -1076,28 +849,6 @@ func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup 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 -} - func parseLayout(layout string) [][]string { rows := strings.Split(layout, ",") l := make([][]string, len(rows)) |