aboutsummaryrefslogblamecommitdiffstats
path: root/config/accounts.go
blob: 8aadb329fbcdffe13e29962d9956264ca834ea46 (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"
	"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 {
	Name string
	// backend specific
	Params map[string]string

	Archive           string          `ini:"archive" default:"Archive"`
	CopyTo            string          `ini:"copy-to"`
	Default           string          `ini:"default" default:"INBOX"`
	Postpone          string          `ini:"postpone" default:"Drafts"`
	From              *mail.Address   `ini:"from"`
	Aliases           []*mail.Address `ini:"aliases"`
	Source            string          `ini:"source" parse:"ParseSource"`
	Folders           []string        `ini:"folders" delim:","`
	FoldersExclude    []string        `ini:"folders-exclude" delim:","`
	Outgoing          RemoteConfig    `ini:"outgoing" parse:"ParseOutgoing"`
	SignatureFile     string          `ini:"signature-file"`
	SignatureCmd      string          `ini:"signature-cmd"`
	EnableFoldersSort bool            `ini:"enable-folders-sort" default:"true"`
	FoldersSort       []string        `ini:"folders-sort" delim:","`
	AddressBookCmd    string          `ini:"address-book-cmd"`
	SendAsUTC         bool            `ini:"send-as-utc" default:"false"`
	LocalizedRe       *regexp.Regexp  `ini:"subject-re-pattern" default:"(?i)^((AW|RE|SV|VS|ODP|R): ?)+"`

	// CheckMail
	CheckMail        time.Duration `ini:"check-mail"`
	CheckMailCmd     string        `ini:"check-mail-cmd"`
	CheckMailTimeout time.Duration `ini:"check-mail-timeout" default:"10s"`
	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" parse:"ParsePgpErrorLevel" default:"warn"`

	// 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
	}

	for _, _sec := range file.SectionStrings() {
		if _sec == "DEFAULT" {
			continue
		}
		if len(accts) > 0 && !contains(accts, _sec) {
			continue
		}
		sec := file.Section(_sec)
		account := AccountConfig{
			Name:   _sec,
			Params: make(map[string]string),
		}
		if err = MapToStruct(sec, &account, true); err != nil {
			return err
		}
		for key, val := range sec.KeysHash() {
			backendSpecific := true
			typ := reflect.TypeOf(account)
			for i := 0; i < typ.NumField(); i++ {
				field := typ.Field(i)
				if field.Tag.Get("ini") == key {
					backendSpecific = false
					break
				}
			}
			if backendSpecific {
				account.Params[key] = val
			}
		}
		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)
		}

		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
}

func (a *AccountConfig) ParseSource(sec *ini.Section, key *ini.Key) (string, error) {
	var remote RemoteConfig
	remote.Value = key.String()
	if k, err := sec.GetKey("source-cred-cmd"); err == nil {
		remote.PasswordCmd = k.String()
	}
	return remote.ConnectionString()
}

func (a *AccountConfig) ParseOutgoing(sec *ini.Section, key *ini.Key) (RemoteConfig, error) {
	var remote RemoteConfig
	remote.Value = key.String()
	if k, err := sec.GetKey("outgoing-cred-cmd"); err == nil {
		remote.PasswordCmd = k.String()
	}
	if k, err := sec.GetKey("outgoing-cred-cmd-cache"); err == nil {
		cache, err := k.Bool()
		if err != nil {
			return remote, err
		}
		remote.CacheCmd = cache
	}
	_, err := remote.parseValue()
	return remote, err
}

func (a *AccountConfig) ParsePgpErrorLevel(sec *ini.Section, key *ini.Key) (int, error) {
	var level int
	var err error
	switch strings.ToLower(key.String()) {
	case "none":
		level = PgpErrorLevelNone
	case "warn":
		level = PgpErrorLevelWarn
	case "error":
		level = PgpErrorLevelError
	default:
		err = fmt.Errorf("unknown level: %s", key.String())
	}
	return level, err
}

// 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
}