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


              
                
             
             
                 
            
                 
              
                
              
                 
                 
              
                 
 
                                     
                               
                                 
                                  
                               
                                         
 
                                              
                                        

 
                           
                                                           
                                                      
                                                              

 
                      





































                                                                                              
                                                                                



                                                                                    


                                           
 

                                                             
                                                            

 

                    
       
                                            

                          
                            
                           


                             
                               




                                  

                              

 


                          

                          

 
                                                       


                                 
                                                           


















                                                                                         
                     
 








                                                                                 
                                                        


                            
 
                              

 
                           







                                


                                           
                                      



                                                                 
                                                           
                                                      
                                        
 






                                                                 



                                                                        


                                                                 

 
                           








                                           

 






                                  
                           




                                                                   

 



                          

                                 


                          

                               

                                                          
                                                          
                                           
                                           

 





                                                 




                                                   
                            
                                                             
                                                 

                                                  

 
                        


                                              





                                                  



                                         
                                           
















                                                                 
                                                                              

                                   

                                                                  

                                 

                                    



                                                    


                                                             
                                         
                                                    
                                         





                                                                   
                                                            

                                                                                               
                 



                                                          

                                       


                                                                  
                                               


                                                                  
                                      
                                                              
                                               
                                                                    
                                        
                                                            
                                                 
                                                                  






                                                                          
                                    
                                                  
                                       
                                                     
                                       
                                                    
                                       
                                                     
                                                   
                                                                                     
                                          
                                                      
                                             
                                                                               
                                                         
                                                                                           
                                                
                                                            






                                                                          



                                                                 




                                                                                      


                                                                                    
 
                                                                    
                               
                                                                                                  


                                       
                                                      
                               
                                                                                                    
                 
 

                                                    






                                                                           
                                                                                    

                  


                            













                                                



                                                   





                                                                               



                                                                   






                                                                  

 
                                   
 


                                                       
                                              



                                  
                       
                                        
                                                             



                               


                          
                                                              





                          








                                                                           

                                                           
                                                                 
                                                                                                                                            



                                                                                            
                                                  
                                 
                                                              
                                                                 
                                                                                                                                            


                                                                                            


                                                  
                                




                                                                       









                                                                          







                                                                                    

                                                                             


                         




                                                                            



                                                                      
                                                          
                                                   

                                                                              











                                                                                         
                 
         
 
                                                         
                                                                     


                                  
 









                                                              
                                                                              

                                  


                                                

                             

                                                        





                                                                       
                                                        





                                                                                         
                        














                                                                                           




                                                                        




                                                                             


                                                                                        
         
 
                                                                  
                                        



















                                                                                      






















                                                                                                


                  















                                                                                       
                 
                                                      
         


















                                                                             












                                                                             


                  

                                                      
                                 


                                                  
                                          
                                                     
                                                                                               





                         
                                                                            



                                                            
                                                 


                                                                        
                                                                                       
                                                                           



                                       

                                                                


                                                     
                       
                               


                                 
                                        








                                                                 
                  


                                                          
                          
 
                                       
                                                       


                                                  
                             
                                                 
                                                                      
                                                                  


                                               


                                                                             
                                                    
                                                 




                                                             
                                                                   
                                                  
                                                   

                                                                                                                       





                                                    
                                                      
                                                                    
                                                  
                                                                    
                                               
                                                  
                                                        
                                                       


                                                  
                  
 

                                                   









                                                                          
                                             
                  
 





                                                     




                                                 
                                          
                  

                                          
                                                 
                                                    


                                                        

                                                   
         
 





                                                        


                                                      
         
 



                                                                 


                                                               
         
 





                                                                        
                                                                  


                                                                      






                                                                  
                                                         
                                                                             
                                                                                



                                          
 



                                                                                     
                                                 

                                                                                       
                                                                            

                                       




                                                                             
         
 
                                               







                                                                             
         



                                                            
                                                                                

                                                               
                                                                               

                                                                         
                        
                                                                                         
                 

                             
                                                                             


                                               
                 
         
 
                                              




                                                            
                                                          
 

                          
 
































                                                                         
                                                                                                      























                                                                      



                                                       














                                                                                          








                                                                                         
                                                                                             
                                        
                         
                                                                         



                                                                                       








                                                                                          



                                                    
                                           
                                                    
         



                          
                                   
                                           
                            


                                                                                            



                                                                     








                                            
 



                                                                   
                                                                             





                                                           

                                          








                                                            



                                                                                      



                                                                  
         

                     

 
                                                                               


                                  
                                                               

         
                      
 
 



                                                                      






                                                                       

                                                           

                             





                                                                                                       








                                             
package config

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

	"github.com/gdamore/tcell/v2"
	"github.com/go-ini/ini"
	"github.com/google/shlex"
	"github.com/imdario/mergo"
	"github.com/kyoh86/xdg"
	"github.com/mitchellh/go-homedir"

	"git.sr.ht/~rjarry/aerc/lib/templates"
	"git.sr.ht/~rjarry/aerc/logging"
)

type GeneralConfig struct {
	DefaultSavePath    string `ini:"default-save-path"`
	PgpProvider        string `ini:"pgp-provider"`
	UnsafeAccountsConf bool   `ini:"unsafe-accounts-conf"`
}

type UIConfig struct {
	AutoMarkRead                  bool          `ini:"auto-mark-read"`
	IndexFormat                   string        `ini:"index-format"`
	TimestampFormat               string        `ini:"timestamp-format"`
	ThisDayTimeFormat             string        `ini:"this-day-time-format"`
	ThisWeekTimeFormat            string        `ini:"this-week-time-format"`
	ThisYearTimeFormat            string        `ini:"this-year-time-format"`
	MessageViewTimestampFormat    string        `ini:"message-view-timestamp-format"`
	MessageViewThisDayTimeFormat  string        `ini:"message-view-this-day-time-format"`
	MessageViewThisWeekTimeFormat string        `ini:"message-view-this-week-time-format"`
	MessageViewThisYearTimeFormat string        `ini:"message-view-this-year-time-format"`
	ShowHeaders                   []string      `delim:","`
	RenderAccountTabs             string        `ini:"render-account-tabs"`
	PinnedTabMarker               string        `ini:"pinned-tab-marker"`
	SidebarWidth                  int           `ini:"sidebar-width"`
	PreviewHeight                 int           `ini:"preview-height"`
	EmptyMessage                  string        `ini:"empty-message"`
	EmptyDirlist                  string        `ini:"empty-dirlist"`
	MouseEnabled                  bool          `ini:"mouse-enabled"`
	ThreadingEnabled              bool          `ini:"threading-enabled"`
	ForceClientThreads            bool          `ini:"force-client-threads"`
	ClientThreadsDelay            time.Duration `ini:"client-threads-delay"`
	FuzzyComplete                 bool          `ini:"fuzzy-complete"`
	NewMessageBell                bool          `ini:"new-message-bell"`
	Spinner                       string        `ini:"spinner"`
	SpinnerDelimiter              string        `ini:"spinner-delimiter"`
	IconUnencrypted               string        `ini:"icon-unencrypted"`
	IconEncrypted                 string        `ini:"icon-encrypted"`
	IconSigned                    string        `ini:"icon-signed"`
	IconSignedEncrypted           string        `ini:"icon-signed-encrypted"`
	IconUnknown                   string        `ini:"icon-unknown"`
	IconInvalid                   string        `ini:"icon-invalid"`
	DirListFormat                 string        `ini:"dirlist-format"`
	DirListDelay                  time.Duration `ini:"dirlist-delay"`
	DirListTree                   bool          `ini:"dirlist-tree"`
	DirListCollapse               int           `ini:"dirlist-collapse"`
	Sort                          []string      `delim:" "`
	NextMessageOnDelete           bool          `ini:"next-message-on-delete"`
	CompletionDelay               time.Duration `ini:"completion-delay"`
	CompletionMinChars            int           `ini:"completion-min-chars"`
	CompletionPopovers            bool          `ini:"completion-popovers"`
	StyleSetDirs                  []string      `ini:"stylesets-dirs" delim:":"`
	StyleSetName                  string        `ini:"styleset-name"`
	style                         StyleSet
	// customize border appearance
	BorderCharVertical   rune `ini:"-"`
	BorderCharHorizontal rune `ini:"-"`

	ReverseOrder       bool `ini:"reverse-msglist-order"`
	ReverseThreadOrder bool `ini:"reverse-thread-order"`
	SortThreadSiblings bool `ini:"sort-thread-siblings"`
}

type ContextType int

const (
	UI_CONTEXT_FOLDER ContextType = iota
	UI_CONTEXT_ACCOUNT
	UI_CONTEXT_SUBJECT
	BIND_CONTEXT_ACCOUNT
	BIND_CONTEXT_FOLDER
)

type UIConfigContext struct {
	ContextType ContextType
	Regex       *regexp.Regexp
	UiConfig    UIConfig
}

const (
	FILTER_MIMETYPE = iota
	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
	Compose                *KeyBindings
	ComposeEditor          *KeyBindings
	ComposeReview          *KeyBindings
	MessageList            *KeyBindings
	MessageView            *KeyBindings
	MessageViewPassthrough *KeyBindings
	Terminal               *KeyBindings
}

type BindingConfigContext struct {
	ContextType ContextType
	Regex       *regexp.Regexp
	Bindings    *KeyBindings
	BindContext string
}

type ComposeConfig struct {
	Editor              string         `ini:"editor"`
	HeaderLayout        [][]string     `ini:"-"`
	AddressBookCmd      string         `ini:"address-book-cmd"`
	ReplyToSelf         bool           `ini:"reply-to-self"`
	NoAttachmentWarning *regexp.Regexp `ini:"-"`
}

type FilterConfig struct {
	FilterType int
	Filter     string
	Command    string
	Header     string
	Regex      *regexp.Regexp
}

type ViewerConfig struct {
	Pager          string
	Alternatives   []string
	ShowHeaders    bool       `ini:"show-headers"`
	AlwaysShowMime bool       `ini:"always-show-mime"`
	ParseHttpLinks bool       `ini:"parse-http-links"`
	HeaderLayout   [][]string `ini:"-"`
	KeyPassthrough bool       `ini:"-"`
}

type StatuslineConfig struct {
	RenderFormat string `ini:"render-format"`
	Separator    string
	DisplayMode  string `ini:"display-mode"`
}

type TriggersConfig struct {
	NewEmail       string `ini:"new-email"`
	ExecuteCommand func(command []string) error
}

type TemplateConfig struct {
	TemplateDirs []string `ini:"template-dirs" delim:":"`
	NewMessage   string   `ini:"new-message"`
	QuotedReply  string   `ini:"quoted-reply"`
	Forwards     string   `ini:"forwards"`
}

type AercConfig struct {
	Bindings        BindingConfig
	ContextualBinds []BindingConfigContext
	Compose         ComposeConfig
	Ini             *ini.File        `ini:"-"`
	Accounts        []AccountConfig  `ini:"-"`
	Filters         []FilterConfig   `ini:"-"`
	Viewer          ViewerConfig     `ini:"-"`
	Statusline      StatuslineConfig `ini:"-"`
	Triggers        TriggersConfig   `ini:"-"`
	Ui              UIConfig
	ContextualUis   []UIConfigContext
	General         GeneralConfig
	Templates       TemplateConfig
	Openers         map[string][]string
}

// Input: TimestampFormat
// Output: timestamp-format
func mapName(raw string) string {
	newstr := make([]rune, 0, len(raw))
	for i, chr := range raw {
		if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
			if i > 0 {
				newstr = append(newstr, '-')
			}
		}
		newstr = append(newstr, unicode.ToLower(chr))
	}
	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

func buildDefaultDirs() []string {
	var defaultDirs []string

	prefixes := []string{
		xdg.ConfigHome(),
		xdg.DataHome(),
	}

	// Add XDG_CONFIG_HOME and XDG_DATA_HOME
	for _, v := range prefixes {
		if v != "" {
			v, err := homedir.Expand(v)
			if err != nil {
				log.Println(err)
			}
			defaultDirs = append(defaultDirs, path.Join(v, "aerc"))
		}
	}

	// Add custom buildtime shareDir
	if shareDir != "" && shareDir != "/usr/local/share/aerc" {
		shareDir, err := homedir.Expand(shareDir)
		if err == nil {
			defaultDirs = append(defaultDirs, shareDir)
		}
	}

	// Add fixed fallback locations
	defaultDirs = append(defaultDirs, "/usr/local/share/aerc")
	defaultDirs = append(defaultDirs, "/usr/share/aerc")

	return defaultDirs
}

var SearchDirs = buildDefaultDirs()

func installTemplate(root, name string) error {
	var err error
	if _, err = os.Stat(root); os.IsNotExist(err) {
		err = os.MkdirAll(root, 0o755)
		if err != nil {
			return err
		}
	}
	var data []byte
	for _, dir := range SearchDirs {
		data, err = os.ReadFile(path.Join(dir, name))
		if err == nil {
			break
		}
	}
	if err != nil {
		return err
	}
	err = os.WriteFile(path.Join(root, name), data, 0o644)
	if err != nil {
		return err
	}
	return nil
}

func (config *AercConfig) LoadConfig(file *ini.File) error {
	if filters, err := file.GetSection("filters"); err == nil {
		// TODO: Parse the filter more finely, e.g. parse the regex
		for _, match := range filters.KeyStrings() {
			cmd := filters.KeysHash()[match]
			filter := FilterConfig{
				Command: cmd,
				Filter:  match,
			}
			switch {
			case strings.Contains(match, ",~"):
				filter.FilterType = FILTER_HEADER
				header := filter.Filter[:strings.Index(filter.Filter, ",")] //nolint:gocritic // guarded by strings.Contains
				regex := filter.Filter[strings.Index(filter.Filter, "~")+1:]
				filter.Header = strings.ToLower(header)
				filter.Regex, err = regexp.Compile(regex)
				if err != nil {
					return err
				}
			case strings.ContainsRune(match, ','):
				filter.FilterType = FILTER_HEADER
				header := filter.Filter[:strings.Index(filter.Filter, ",")] //nolint:gocritic // guarded by strings.Contains
				value := filter.Filter[strings.Index(filter.Filter, ",")+1:]
				filter.Header = strings.ToLower(header)
				filter.Regex, err = regexp.Compile(regexp.QuoteMeta(value))
				if err != nil {
					return err
				}
			default:
				filter.FilterType = FILTER_MIMETYPE
			}
			config.Filters = append(config.Filters, filter)
		}
	}
	if openers, err := file.GetSection("openers"); err == nil {
		for mimeType, command := range openers.KeysHash() {
			mimeType = strings.ToLower(mimeType)
			if args, err := shlex.Split(command); err != nil {
				return err
			} else {
				config.Openers[mimeType] = args
			}
		}
	}
	if viewer, err := file.GetSection("viewer"); err == nil {
		if err := viewer.MapTo(&config.Viewer); err != nil {
			return err
		}
		for key, val := range viewer.KeysHash() {
			switch key {
			case "alternatives":
				config.Viewer.Alternatives = strings.Split(val, ",")
			case "header-layout":
				config.Viewer.HeaderLayout = parseLayout(val)
			}
		}
	}
	if statusline, err := file.GetSection("statusline"); err == nil {
		if err := statusline.MapTo(&config.Statusline); err != nil {
			return err
		}
	}
	if compose, err := file.GetSection("compose"); err == nil {
		if err := compose.MapTo(&config.Compose); err != nil {
			return err
		}
		for key, val := range compose.KeysHash() {
			if key == "header-layout" {
				config.Compose.HeaderLayout = parseLayout(val)
			}

			if key == "no-attachment-warning" && len(val) > 0 {
				re, err := regexp.Compile("(?im)" + val)
				if err != nil {
					return fmt.Errorf(
						"Invalid no-attachment-warning '%s': %w",
						val, err,
					)
				}

				config.Compose.NoAttachmentWarning = re
			}
		}
	}

	if ui, err := file.GetSection("ui"); err == nil {
		if err := parseUiConfig(ui, &config.Ui); err != nil {
			return err
		}
	}

	for _, sectionName := range file.SectionStrings() {
		if !strings.Contains(sectionName, "ui:") {
			continue
		}

		uiSection, err := file.GetSection(sectionName)
		if err != nil {
			return err
		}
		uiSubConfig := UIConfig{}
		if err := parseUiConfig(uiSection, &uiSubConfig); err != nil {
			return err
		}
		contextualUi := UIConfigContext{
			UiConfig: uiSubConfig,
		}

		var index int
		switch {
		case strings.Contains(sectionName, "~"):
			index = strings.Index(sectionName, "~")
			regex := string(sectionName[index+1:])
			contextualUi.Regex, err = regexp.Compile(regex)
			if err != nil {
				return err
			}
		case strings.Contains(sectionName, "="):
			index = strings.Index(sectionName, "=")
			value := string(sectionName[index+1:])
			contextualUi.Regex, err = regexp.Compile(regexp.QuoteMeta(value))
			if err != nil {
				return err
			}
		default:
			return fmt.Errorf("Invalid Ui Context regex in %s", sectionName)
		}

		switch sectionName[3:index] {
		case "account":
			contextualUi.ContextType = UI_CONTEXT_ACCOUNT
		case "folder":
			contextualUi.ContextType = UI_CONTEXT_FOLDER
		case "subject":
			contextualUi.ContextType = UI_CONTEXT_SUBJECT
		default:
			return fmt.Errorf("Unknown Contextual Ui Section: %s", sectionName)
		}
		config.ContextualUis = append(config.ContextualUis, contextualUi)
	}
	if triggers, err := file.GetSection("triggers"); err == nil {
		if err := triggers.MapTo(&config.Triggers); err != nil {
			return err
		}
	}
	if templatesSec, err := file.GetSection("templates"); err == nil {
		if err := templatesSec.MapTo(&config.Templates); err != nil {
			return err
		}
		templateDirs := templatesSec.Key("template-dirs").String()
		if templateDirs != "" {
			config.Templates.TemplateDirs = strings.Split(templateDirs, ":")
		}
	}

	// append default paths to template-dirs and styleset-dirs
	for _, dir := range SearchDirs {
		config.Ui.StyleSetDirs = append(
			config.Ui.StyleSetDirs, path.Join(dir, "stylesets"),
		)
		config.Templates.TemplateDirs = append(
			config.Templates.TemplateDirs, path.Join(dir, "templates"),
		)
	}

	// we want to fail during startup if the templates are not ok
	// hence we do dummy executes here
	t := config.Templates
	if err := templates.CheckTemplate(t.NewMessage, t.TemplateDirs); err != nil {
		return err
	}
	if err := templates.CheckTemplate(t.QuotedReply, t.TemplateDirs); err != nil {
		return err
	}
	if err := templates.CheckTemplate(t.Forwards, t.TemplateDirs); err != nil {
		return err
	}
	if err := config.Ui.loadStyleSet(
		config.Ui.StyleSetDirs); err != nil {
		return err
	}

	for idx, contextualUi := range config.ContextualUis {
		if contextualUi.UiConfig.StyleSetName == "" &&
			len(contextualUi.UiConfig.StyleSetDirs) == 0 {
			continue // no need to do anything if nothing is overridden
		}
		// fill in the missing part from the base
		if contextualUi.UiConfig.StyleSetName == "" {
			config.ContextualUis[idx].UiConfig.StyleSetName = config.Ui.StyleSetName
		} else if len(contextualUi.UiConfig.StyleSetDirs) == 0 {
			config.ContextualUis[idx].UiConfig.StyleSetDirs = config.Ui.StyleSetDirs
		}
		// since at least one of them has changed, load the styleset
		if err := config.ContextualUis[idx].UiConfig.loadStyleSet(
			config.ContextualUis[idx].UiConfig.StyleSetDirs); err != nil {
			return err
		}
	}

	return nil
}

func parseUiConfig(section *ini.Section, config *UIConfig) error {
	if err := section.MapTo(config); err != nil {
		return err
	}

	if key, err := section.GetKey("border-char-vertical"); err == nil {
		chars := []rune(key.String())
		if len(chars) != 1 {
			return fmt.Errorf("%v must be one and only one character", key)
		}
		config.BorderCharVertical = chars[0]
	}
	if key, err := section.GetKey("border-char-horizontal"); err == nil {
		chars := []rune(key.String())
		if len(chars) != 1 {
			return fmt.Errorf("%v must be one and only one character", key)
		}
		config.BorderCharHorizontal = chars[0]
	}

	// Values with type=time.Duration must be explicitly set. If these
	// values are given a default in the struct passed to ui.MapTo, which
	// they are, a zero-value in the config won't overwrite the default.
	if key, err := section.GetKey("dirlist-delay"); err == nil {
		dur, err := key.Duration()
		if err != nil {
			return err
		}
		config.DirListDelay = dur
	}
	if key, err := section.GetKey("completion-delay"); err == nil {
		dur, err := key.Duration()
		if err != nil {
			return err
		}
		config.CompletionDelay = dur
	}

	if config.MessageViewTimestampFormat == "" {
		config.MessageViewTimestampFormat = config.TimestampFormat
	}
	if config.MessageViewThisDayTimeFormat == "" {
		config.MessageViewThisDayTimeFormat = config.TimestampFormat
	}
	if config.MessageViewThisWeekTimeFormat == "" {
		config.MessageViewThisWeekTimeFormat = config.TimestampFormat
	}
	if config.MessageViewThisDayTimeFormat == "" {
		config.MessageViewThisDayTimeFormat = config.TimestampFormat
	}

	return nil
}

func validatePgpProvider(section *ini.Section) error {
	m := map[string]bool{
		"gpg":      true,
		"internal": true,
	}
	for key, val := range section.KeysHash() {
		if key == "pgp-provider" {
			if !m[strings.ToLower(val)] {
				return fmt.Errorf("%v must be either 'gpg' or 'internal'", key)
			}
		}
	}
	return nil
}

func LoadConfigFromFile(root *string, accts []string) (*AercConfig, error) {
	if root == nil {
		_root := path.Join(xdg.ConfigHome(), "aerc")
		root = &_root
	}
	filename := path.Join(*root, "aerc.conf")

	// if it doesn't exist copy over the template, then load
	if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
		logging.Debugf("%s not found, installing the system default", filename)
		if err := installTemplate(*root, "aerc.conf"); err != nil {
			return nil, err
		}
	}

	logging.Infof("Parsing configuration from %s", filename)

	file, err := ini.LoadSources(ini.LoadOptions{
		KeyValueDelimiters: "=",
	}, filename)
	if err != nil {
		return nil, err
	}
	file.NameMapper = mapName
	config := &AercConfig{
		Bindings: BindingConfig{
			Global:                 NewKeyBindings(),
			AccountWizard:          NewKeyBindings(),
			Compose:                NewKeyBindings(),
			ComposeEditor:          NewKeyBindings(),
			ComposeReview:          NewKeyBindings(),
			MessageList:            NewKeyBindings(),
			MessageView:            NewKeyBindings(),
			MessageViewPassthrough: NewKeyBindings(),
			Terminal:               NewKeyBindings(),
		},

		ContextualBinds: []BindingConfigContext{},

		Ini: file,

		General: GeneralConfig{
			PgpProvider:        "internal",
			UnsafeAccountsConf: false,
		},

		Ui: UIConfig{
			AutoMarkRead:       true,
			IndexFormat:        "%-20.20D %-17.17n %Z %s",
			TimestampFormat:    "2006-01-02 03:04 PM",
			ThisDayTimeFormat:  "",
			ThisWeekTimeFormat: "",
			ThisYearTimeFormat: "",
			ShowHeaders: []string{
				"From", "To", "Cc", "Bcc", "Subject", "Date",
			},
			RenderAccountTabs:   "auto",
			PinnedTabMarker:     "`",
			SidebarWidth:        20,
			PreviewHeight:       12,
			EmptyMessage:        "(no messages)",
			EmptyDirlist:        "(no folders)",
			MouseEnabled:        false,
			ClientThreadsDelay:  50 * time.Millisecond,
			NewMessageBell:      true,
			FuzzyComplete:       false,
			Spinner:             "[..]    , [..]   ,  [..]  ,   [..] ,    [..],   [..] ,  [..]  , [..]   ",
			SpinnerDelimiter:    ",",
			IconUnencrypted:     "",
			IconSigned:          "[s]",
			IconEncrypted:       "[e]",
			IconSignedEncrypted: "",
			IconUnknown:         "[s?]",
			IconInvalid:         "[s!]",
			DirListFormat:       "%n %>r",
			DirListDelay:        200 * time.Millisecond,
			NextMessageOnDelete: true,
			CompletionDelay:     250 * time.Millisecond,
			CompletionMinChars:  1,
			CompletionPopovers:  true,
			StyleSetDirs:        []string{},
			StyleSetName:        "default",
			// border defaults
			BorderCharVertical:   ' ',
			BorderCharHorizontal: ' ',
		},

		ContextualUis: []UIConfigContext{},

		Viewer: ViewerConfig{
			Pager:        "less -R",
			Alternatives: []string{"text/plain", "text/html"},
			ShowHeaders:  false,
			HeaderLayout: [][]string{
				{"From", "To"},
				{"Cc", "Bcc"},
				{"Date"},
				{"Subject"},
			},
			ParseHttpLinks: true,
		},

		Statusline: StatuslineConfig{
			RenderFormat: "[%a] %S %>%T",
			Separator:    " | ",
			DisplayMode:  "",
		},

		Compose: ComposeConfig{
			HeaderLayout: [][]string{
				{"To", "From"},
				{"Subject"},
			},
			ReplyToSelf: true,
		},

		Templates: TemplateConfig{
			TemplateDirs: []string{},
			NewMessage:   "new_message",
			QuotedReply:  "quoted_reply",
			Forwards:     "forward_as_body",
		},

		Openers: make(map[string][]string),
	}

	// These bindings are not configurable
	config.Bindings.AccountWizard.ExKey = KeyStroke{
		Key: tcell.KeyCtrlE,
	}
	quit, _ := ParseBinding("<C-q>", ":quit<Enter>")
	config.Bindings.AccountWizard.Add(quit)

	if err = config.LoadConfig(file); err != nil {
		return nil, err
	}

	if ui, err := file.GetSection("general"); err == nil {
		if err := ui.MapTo(&config.General); err != nil {
			return nil, err
		}
		if err := validatePgpProvider(ui); err != nil {
			return nil, err
		}
	}

	logging.Debugf("aerc.conf: [general] %#v", config.General)
	logging.Debugf("aerc.conf: [ui] %#v", config.Ui)
	logging.Debugf("aerc.conf: [statusline] %#v", config.Statusline)
	logging.Debugf("aerc.conf: [viewer] %#v", config.Viewer)
	logging.Debugf("aerc.conf: [compose] %#v", config.Compose)
	logging.Debugf("aerc.conf: [filters] %#v", config.Filters)
	logging.Debugf("aerc.conf: [openers] %#v", config.Openers)
	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 {
		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")
	if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
		logging.Debugf("%s not found, installing the system default", filename)
		if err := installTemplate(*root, "binds.conf"); err != nil {
			return nil, err
		}
	}
	logging.Infof("Parsing key bindings configuration from %s", filename)
	binds, err := ini.Load(filename)
	if err != nil {
		return nil, err
	}

	baseGroups := map[string]**KeyBindings{
		"default":           &config.Bindings.Global,
		"compose":           &config.Bindings.Compose,
		"messages":          &config.Bindings.MessageList,
		"terminal":          &config.Bindings.Terminal,
		"view":              &config.Bindings.MessageView,
		"view::passthrough": &config.Bindings.MessageViewPassthrough,
		"compose::editor":   &config.Bindings.ComposeEditor,
		"compose::review":   &config.Bindings.ComposeReview,
	}

	// Base Bindings
	for _, sectionName := range binds.SectionStrings() {
		// Handle :: delimeter
		baseSectionName := strings.ReplaceAll(sectionName, "::", "////")
		sections := strings.Split(baseSectionName, ":")
		baseOnly := len(sections) == 1
		baseSectionName = strings.ReplaceAll(sections[0], "////", "::")

		group, ok := baseGroups[strings.ToLower(baseSectionName)]
		if !ok {
			return nil, errors.New("Unknown keybinding group " + sectionName)
		}

		if baseOnly {
			err = config.LoadBinds(binds, baseSectionName, group)
			if err != nil {
				return nil, err
			}
		}
	}

	config.Bindings.Global.Globals = false
	for _, contextBind := range config.ContextualBinds {
		if contextBind.BindContext == "default" {
			contextBind.Bindings.Globals = false
		}
	}
	logging.Debugf("binds.conf: %#v", config.Bindings)

	return config, nil
}

func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
	bindings := NewKeyBindings()
	for key, value := range sec.KeysHash() {
		if key == "$ex" {
			strokes, err := ParseKeyStrokes(value)
			if err != nil {
				return nil, err
			}
			if len(strokes) != 1 {
				return nil, errors.New("Invalid binding")
			}
			bindings.ExKey = strokes[0]
			continue
		}
		if key == "$noinherit" {
			if value == "false" {
				continue
			}
			if value != "true" {
				return nil, errors.New("Invalid binding")
			}
			bindings.Globals = false
			continue
		}
		binding, err := ParseBinding(key, value)
		if err != nil {
			return nil, err
		}
		bindings.Add(binding)
	}
	return bindings, nil
}

func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
	if sec, err := binds.GetSection(baseName); err == nil {
		binds, err := LoadBindingSection(sec)
		if err != nil {
			return err
		}
		*baseGroup = MergeBindings(binds, *baseGroup)
	}

	for _, sectionName := range binds.SectionStrings() {
		if !strings.Contains(sectionName, baseName+":") ||
			strings.Contains(sectionName, baseName+"::") {
			continue
		}

		bindSection, err := binds.GetSection(sectionName)
		if err != nil {
			return err
		}

		binds, err := LoadBindingSection(bindSection)
		if err != nil {
			return err
		}

		contextualBind := BindingConfigContext{
			Bindings:    binds,
			BindContext: baseName,
		}

		var index int
		if strings.Contains(sectionName, "=") {
			index = strings.Index(sectionName, "=")
			value := string(sectionName[index+1:])
			contextualBind.Regex, err = regexp.Compile(value)
			if err != nil {
				return err
			}
		} else {
			return fmt.Errorf("Invalid Bind Context regex in %s", sectionName)
		}

		switch sectionName[len(baseName)+1 : index] {
		case "account":
			acctName := sectionName[index+1:]
			valid := false
			for _, acctConf := range config.Accounts {
				matches := contextualBind.Regex.FindString(acctConf.Name)
				if matches != "" {
					valid = true
				}
			}
			if !valid {
				logging.Warnf("binds.conf: unexistent account: %s", acctName)
				continue
			}
			contextualBind.ContextType = BIND_CONTEXT_ACCOUNT
		case "folder":
			// No validation needed. If the folder doesn't exist, the binds
			// never get used
			contextualBind.ContextType = BIND_CONTEXT_FOLDER
		default:
			return fmt.Errorf("Unknown Context Bind Section: %s", sectionName)
		}
		config.ContextualBinds = append(config.ContextualBinds, contextualBind)
	}

	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))
	for i, r := range rows {
		l[i] = strings.Split(r, "|")
	}
	return l
}

func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
	ui.style = NewStyleSet()
	err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
	if err != nil {
		return fmt.Errorf("Unable to load default styleset: %w", err)
	}

	return nil
}

func (config AercConfig) mergeContextualUi(baseUi UIConfig,
	contextType ContextType, s string,
) UIConfig {
	for _, contextualUi := range config.ContextualUis {
		if contextualUi.ContextType != contextType {
			continue
		}

		if !contextualUi.Regex.Match([]byte(s)) {
			continue
		}

		err := mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
		if err != nil {
			logging.Warnf("merge ui failed: %v", err)
		}
		if contextualUi.UiConfig.StyleSetName != "" {
			baseUi.style = contextualUi.UiConfig.style
		}
		return baseUi
	}

	return baseUi
}

func (config AercConfig) GetUiConfig(params map[ContextType]string) *UIConfig {
	baseUi := config.Ui

	for k, v := range params {
		baseUi = config.mergeContextualUi(baseUi, k, v)
	}

	return &baseUi
}

func (config *AercConfig) GetContextualUIConfigs() []UIConfigContext {
	return config.ContextualUis
}

func (uiConfig UIConfig) GetStyle(so StyleObject) tcell.Style {
	return uiConfig.style.Get(so)
}

func (uiConfig UIConfig) GetStyleSelected(so StyleObject) tcell.Style {
	return uiConfig.style.Selected(so)
}

func (uiConfig UIConfig) GetComposedStyle(base StyleObject,
	styles []StyleObject,
) tcell.Style {
	return uiConfig.style.Compose(base, styles)
}

func (uiConfig UIConfig) GetComposedStyleSelected(base StyleObject, styles []StyleObject) tcell.Style {
	return uiConfig.style.ComposeSelected(base, styles)
}

func contains(list []string, v string) bool {
	for _, item := range list {
		if item == v {
			return true
		}
	}
	return false
}