aboutsummaryrefslogblamecommitdiffstats
path: root/config/accounts.go
blob: cbdf54a89b61868b87d5ea3f8e25bebe85b5a7e6 (plain) (tree)
1
2
3
4
5
6
7
8
9








                 
                 





                 
                                    
                                             





















































                                                                                         



                                                            

                                                     












                                                                             











                                                                        
                                                              




                                                                 





                                


                                                       
                                                    
                                        




                                                                  
                                                                      
























                                                                   
                                                             







                                                                                               













                                                                                    











                                                                                    





                                                                                    










                                                                                             
                                

















                                                                       

                                                               



                                                                       



                                                                 





                                                                                             


                                                                                 
                                        


                                                                               




                                                                                               
                                                                                       
                                                     



                                                                           
                                                

                                                                 
                                                          



























                                                                                            
package config

import (
	"errors"
	"fmt"
	"net/url"
	"os"
	"os/exec"
	"path"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"git.sr.ht/~rjarry/aerc/log"
	"github.com/emersion/go-message/mail"
	"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            `ini:"archive"`
	CopyTo            string            `ini:"copy-to"`
	Default           string            `ini:"default"`
	Postpone          string            `ini:"postpone"`
	From              *mail.Address     `ini:"-"`
	Aliases           []*mail.Address   `ini:"-"`
	Name              string            `ini:"-"`
	Source            string            `ini:"-"`
	Folders           []string          `ini:"folders" delim:","`
	FoldersExclude    []string          `ini:"folders-exclude" delim:","`
	Params            map[string]string `ini:"-"`
	Outgoing          RemoteConfig      `ini:"-"`
	SignatureFile     string            `ini:"signature-file"`
	SignatureCmd      string            `ini:"signature-cmd"`
	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    `ini:"-"`

	// 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"`
	PgpErrorLevel           int    `ini:"pgp-error-level"`

	// AuthRes
	TrustedAuthRes []string `ini:"trusted-authres" delim:","`
}

const (
	PgpErrorLevelNone = iota
	PgpErrorLevelWarn
	PgpErrorLevelError
)

var Accounts []*AccountConfig

func parseAccounts(root string, accts []string) error {
	filename := path.Join(root, "accounts.conf")
	if !General.UnsafeAccountsConf {
		if err := checkConfigPerms(filename); err != nil {
			return err
		}
	}

	log.Debugf("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,
			PgpErrorLevel:     PgpErrorLevelWarn,
			// 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 "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":
				addr, err := mail.ParseAddress(val)
				if err != nil {
					return fmt.Errorf("%s=%s %w", key, val, err)
				}
				account.From = addr
			case "aliases":
				addrs, err := mail.ParseAddressList(val)
				if err != nil {
					return fmt.Errorf("%s=%s %w", key, val, err)
				}
				account.Aliases = addrs
			case "subject-re-pattern":
				re, err := regexp.Compile(val)
				if err != nil {
					return fmt.Errorf("%s=%s %w", key, val, err)
				}
				account.LocalizedRe = re
			case "pgp-error-level":
				switch strings.ToLower(val) {
				case "none":
					account.PgpErrorLevel = PgpErrorLevelNone
				case "warn":
					account.PgpErrorLevel = PgpErrorLevelWarn
				case "error":
					account.PgpErrorLevel = PgpErrorLevelError
				default:
					return fmt.Errorf("unknown pgp-error-level: %s", val)
				}
			default:
				backendSpecific := true
				typ := reflect.TypeOf(account)
				for i := 0; i < typ.NumField(); i++ {
					field := typ.Field(i)
					switch field.Tag.Get("ini") {
					case key:
						fallthrough
					case "source":
						fallthrough
					case "source-cred-cmd":
						fallthrough
					case "outgoing":
						fallthrough
					case "outgoing-cred-cmd":
						fallthrough
					case "outgoing-cred-cmd-cache":
						fallthrough
					case "subject-re-pattern":
						fallthrough
					case "pgp-error-level":
						backendSpecific = false
					}
				}
				if backendSpecific {
					account.Params[key] = val
				}
			}
		}
		source, err := sourceRemoteConfig.ConnectionString()
		if err != nil {
			return fmt.Errorf("Invalid source credentials for %s: %w", _sec, err)
		}
		account.Source = source

		if account.Source == "" {
			return fmt.Errorf("Expected source for account %s", _sec)
		}
		if account.From == nil {
			return fmt.Errorf("Expected from for account %s", _sec)
		}

		_, err = account.Outgoing.parseValue()
		if err != nil {
			return fmt.Errorf("Invalid outgoing credentials for %s: %w", _sec, err)
		}

		log.Debugf("accounts.conf: [%s] from = %s", account.Name, account.From)
		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 errors.New("account(s) not found")
		}
		sort.Slice(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
}