aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/accounts.go256
-rw-r--r--config/config.go251
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))