aerc/config/config.go

366 lines
9 KiB
Go
Raw Normal View History

2018-01-10 01:18:19 +01:00
package config
import (
2019-03-21 22:36:42 +01:00
"errors"
2018-01-10 03:24:50 +01:00
"fmt"
"net/url"
"os"
"os/exec"
2018-01-10 01:18:19 +01:00
"path"
2019-03-31 20:42:18 +02:00
"regexp"
2018-01-10 03:24:50 +01:00
"strings"
2018-01-10 01:18:19 +01:00
"unicode"
2018-01-10 17:19:45 +01:00
"github.com/go-ini/ini"
"github.com/kyoh86/xdg"
2018-01-10 01:18:19 +01:00
)
type UIConfig struct {
2019-03-16 01:40:28 +01:00
IndexFormat string `ini:"index-format"`
TimestampFormat string `ini:"timestamp-format"`
2018-01-10 01:18:19 +01:00
ShowHeaders []string `delim:","`
2019-03-16 01:40:28 +01:00
RenderAccountTabs string `ini:"render-account-tabs"`
SidebarWidth int `ini:"sidebar-width"`
PreviewHeight int `ini:"preview-height"`
EmptyMessage string `ini:"empty-message"`
2018-01-10 01:18:19 +01:00
}
2019-03-31 20:24:53 +02:00
const (
FILTER_MIMETYPE = iota
FILTER_HEADER
)
2018-01-10 01:18:19 +01:00
type AccountConfig struct {
CopyTo string
Default string
From string
Name string
Source string
SourceCredCmd string
Folders []string
Params map[string]string
Outgoing string
OutgoingCredCmd string
2018-01-10 01:18:19 +01:00
}
2019-03-21 22:36:42 +01:00
type BindingConfig struct {
Global *KeyBindings
Compose *KeyBindings
ComposeEditor *KeyBindings
ComposeReview *KeyBindings
MessageList *KeyBindings
MessageView *KeyBindings
Terminal *KeyBindings
2019-03-21 22:36:42 +01:00
}
type ComposeConfig struct {
Editor string `ini:"editor"`
}
2019-03-31 20:24:53 +02:00
type FilterConfig struct {
FilterType int
Filter string
Command string
2019-03-31 20:42:18 +02:00
Header string
Regex *regexp.Regexp
2019-03-31 20:24:53 +02:00
}
type ViewerConfig struct {
Pager string
Alternatives []string
}
2018-01-10 01:18:19 +01:00
type AercConfig struct {
2019-03-21 22:36:42 +01:00
Bindings BindingConfig
Compose ComposeConfig
2018-01-10 01:18:19 +01:00
Ini *ini.File `ini:"-"`
Accounts []AccountConfig `ini:"-"`
2019-03-31 20:24:53 +02:00
Filters []FilterConfig `ini:"-"`
Viewer ViewerConfig `ini:"-"`
2018-01-10 01:18:19 +01:00
Ui UIConfig
}
// 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)
}
2018-01-10 03:24:50 +01:00
func loadAccountConfig(path string) ([]AccountConfig, error) {
2018-01-10 17:19:45 +01:00
file, err := ini.Load(path)
if err != nil {
2018-01-10 03:24:50 +01:00
return nil, err
}
file.NameMapper = mapName
2018-01-10 17:19:45 +01:00
var accounts []AccountConfig
2018-01-10 03:24:50 +01:00
for _, _sec := range file.SectionStrings() {
if _sec == "DEFAULT" {
continue
}
sec := file.Section(_sec)
2019-03-16 02:33:08 +01:00
account := AccountConfig{
Default: "INBOX",
Name: _sec,
Params: make(map[string]string),
}
2018-01-10 03:24:50 +01:00
if err = sec.MapTo(&account); err != nil {
return nil, err
}
for key, val := range sec.KeysHash() {
2019-03-16 02:33:08 +01:00
if key == "folders" {
2018-01-10 03:24:50 +01:00
account.Folders = strings.Split(val, ",")
} else if key == "source_cred_cmd" {
account.SourceCredCmd = val
2019-05-13 05:35:36 +02:00
} else if key == "outgoing" {
account.Outgoing = val
} else if key == "outgoing_cred_cmd" {
account.OutgoingCredCmd = val
} else if key == "from" {
account.From = val
} else if key == "copy-to" {
account.CopyTo = val
2018-01-10 03:24:50 +01:00
} else if key != "name" {
account.Params[key] = val
}
}
if account.Source == "" {
return nil, fmt.Errorf("Expected source for account %s", _sec)
}
source, err := parseCredential(account.Source, account.SourceCredCmd)
if err != nil {
return nil, fmt.Errorf("Invalid source credentials for %s: %s", _sec, err)
}
account.Source = source
outgoing, err := parseCredential(account.Outgoing, account.OutgoingCredCmd)
if err != nil {
return nil, fmt.Errorf("Invalid outgoing credentials for %s: %s", _sec, err)
}
account.Outgoing = outgoing
2018-01-10 03:24:50 +01:00
accounts = append(accounts, account)
}
if len(accounts) == 0 {
err = errors.New("No accounts configured in accounts.conf")
return nil, err
}
2018-01-10 03:24:50 +01:00
return accounts, nil
}
func parseCredential(cred, command string) (string, error) {
if cred == "" || command == "" {
return cred, nil
}
u, err := url.Parse(cred)
if err != nil {
return "", err
}
// ignore the command if a password is specified
if _, exists := u.User.Password(); exists {
return cred, nil
}
// don't attempt to parse the command if the url is a path (ie /usr/bin/sendmail)
if !u.IsAbs() {
return cred, nil
}
cmd := exec.Command("sh", "-c", command)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to read password: %s", err)
}
pw := strings.TrimSpace(string(output))
u.User = url.UserPassword(u.User.Username(), pw)
return u.String(), nil
}
2018-01-10 01:18:19 +01:00
func LoadConfig(root *string) (*AercConfig, error) {
if root == nil {
_root := path.Join(xdg.ConfigHome(), "aerc")
root = &_root
}
filename := path.Join(*root, "accounts.conf")
if err := checkConfigPerms(filename); err != nil {
return nil, err
}
filename = path.Join(*root, "aerc.conf")
file, err := ini.Load(filename)
2018-01-10 17:19:45 +01:00
if err != nil {
2018-01-10 01:18:19 +01:00
return nil, err
}
file.NameMapper = mapName
config := &AercConfig{
2019-03-21 22:36:42 +01:00
Bindings: BindingConfig{
Global: NewKeyBindings(),
Compose: NewKeyBindings(),
ComposeEditor: NewKeyBindings(),
ComposeReview: NewKeyBindings(),
MessageList: NewKeyBindings(),
MessageView: NewKeyBindings(),
Terminal: NewKeyBindings(),
2019-03-21 22:36:42 +01:00
},
Ini: file,
2018-01-10 01:18:19 +01:00
Ui: UIConfig{
IndexFormat: "%4C %Z %D %-17.17n %s",
TimestampFormat: "%F %l:%M %p",
ShowHeaders: []string{
"From", "To", "Cc", "Bcc", "Subject", "Date",
},
RenderAccountTabs: "auto",
SidebarWidth: 20,
PreviewHeight: 12,
EmptyMessage: "(no messages)",
},
}
2019-03-31 20:24:53 +02:00
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]
2019-03-31 20:24:53 +02:00
filter := FilterConfig{
Command: cmd,
Filter: match,
}
2019-03-31 20:42:18 +02:00
if strings.Contains(match, ",~") {
2019-03-31 20:24:53 +02:00
filter.FilterType = FILTER_HEADER
2019-03-31 20:42:18 +02:00
header := filter.Filter[:strings.Index(filter.Filter, ",")]
regex := filter.Filter[strings.Index(filter.Filter, "~")+1:]
filter.Header = strings.ToLower(header)
filter.Regex, err = regexp.Compile(regex)
if err != nil {
panic(err)
}
} else if strings.ContainsRune(match, ',') {
filter.FilterType = FILTER_HEADER
header := filter.Filter[:strings.Index(filter.Filter, ",")]
value := filter.Filter[strings.Index(filter.Filter, ",")+1:]
filter.Header = strings.ToLower(header)
filter.Regex, err = regexp.Compile(regexp.QuoteMeta(value))
2019-03-31 20:24:53 +02:00
} else {
filter.FilterType = FILTER_MIMETYPE
}
config.Filters = append(config.Filters, filter)
}
}
if viewer, err := file.GetSection("viewer"); err == nil {
if err := viewer.MapTo(&config.Viewer); err != nil {
return nil, err
}
for key, val := range viewer.KeysHash() {
switch key {
case "alternatives":
config.Viewer.Alternatives = strings.Split(val, ",")
}
}
}
if compose, err := file.GetSection("compose"); err == nil {
if err := compose.MapTo(&config.Compose); err != nil {
return nil, err
}
}
if ui, err := file.GetSection("ui"); err == nil {
2019-03-16 01:40:28 +01:00
if err := ui.MapTo(&config.Ui); err != nil {
return nil, err
}
2018-01-10 01:18:19 +01:00
}
2018-01-10 03:24:50 +01:00
accountsPath := path.Join(*root, "accounts.conf")
if accounts, err := loadAccountConfig(accountsPath); err != nil {
return nil, err
} else {
config.Accounts = accounts
}
2019-03-21 22:36:42 +01:00
binds, err := ini.Load(path.Join(*root, "binds.conf"))
if err != nil {
return nil, err
}
groups := map[string]**KeyBindings{
"default": &config.Bindings.Global,
"compose": &config.Bindings.Compose,
"messages": &config.Bindings.MessageList,
"terminal": &config.Bindings.Terminal,
"view": &config.Bindings.MessageView,
"compose::editor": &config.Bindings.ComposeEditor,
"compose::review": &config.Bindings.ComposeReview,
2019-03-21 22:36:42 +01:00
}
for _, name := range binds.SectionStrings() {
sec, err := binds.GetSection(name)
if err != nil {
return nil, err
}
group, ok := groups[strings.ToLower(name)]
if !ok {
return nil, errors.New("Unknown keybinding group " + name)
}
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(
"Error: only one keystroke supported for $ex")
}
bindings.ExKey = strokes[0]
continue
}
if key == "$noinherit" {
if value == "false" {
continue
}
if value != "true" {
return nil, errors.New(
"Error: expected 'true' or 'false' for $noinherit")
}
bindings.Globals = false
}
binding, err := ParseBinding(key, value)
if err != nil {
return nil, err
}
bindings.Add(binding)
}
*group = MergeBindings(bindings, *group)
}
// Globals can't inherit from themselves
config.Bindings.Global.Globals = false
2018-01-10 01:18:19 +01:00
return config, 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 err != nil {
return err
}
perms := info.Mode().Perm()
goPerms := perms >> 3
// group or others have read access
if goPerms&0x44 != 0 {
fmt.Printf("The file %v has too open permissions.\n", filename)
fmt.Println("This is a security issue (it contains passwords).")
fmt.Printf("To fix it, run `chmod 600 %v`\n", filename)
return errors.New("account.conf permissions too lax")
}
return nil
}