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"
|
2022-01-24 11:45:35 +01:00
|
|
|
"log"
|
2019-05-18 21:29:26 +02:00
|
|
|
"net/url"
|
2019-05-16 23:26:08 +02:00
|
|
|
"os"
|
2019-05-18 21:29:26 +02:00
|
|
|
"os/exec"
|
2018-01-10 01:18:19 +01:00
|
|
|
"path"
|
2019-03-31 20:42:18 +02:00
|
|
|
"regexp"
|
2019-06-05 19:48:00 +02:00
|
|
|
"sort"
|
2021-11-13 09:10:09 +01:00
|
|
|
"strconv"
|
2018-01-10 03:24:50 +01:00
|
|
|
"strings"
|
2019-12-20 19:21:33 +01:00
|
|
|
"time"
|
2018-01-10 01:18:19 +01:00
|
|
|
"unicode"
|
2018-01-10 17:19:45 +01:00
|
|
|
|
2020-11-30 23:07:03 +01:00
|
|
|
"github.com/gdamore/tcell/v2"
|
2018-01-10 17:19:45 +01:00
|
|
|
"github.com/go-ini/ini"
|
2020-01-23 13:56:48 +01:00
|
|
|
"github.com/imdario/mergo"
|
2018-01-10 17:19:45 +01:00
|
|
|
"github.com/kyoh86/xdg"
|
2022-03-02 04:39:01 +01:00
|
|
|
"github.com/mitchellh/go-homedir"
|
2019-11-03 13:51:14 +01:00
|
|
|
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/lib/templates"
|
2022-07-19 22:31:51 +02:00
|
|
|
"git.sr.ht/~rjarry/aerc/logging"
|
2018-01-10 01:18:19 +01:00
|
|
|
)
|
|
|
|
|
2019-06-25 09:23:51 +02:00
|
|
|
type GeneralConfig struct {
|
2022-04-19 21:14:46 +02:00
|
|
|
DefaultSavePath string `ini:"default-save-path"`
|
2022-04-25 15:30:43 +02:00
|
|
|
PgpProvider string `ini:"pgp-provider"`
|
2022-04-19 21:14:46 +02:00
|
|
|
UnsafeAccountsConf bool `ini:"unsafe-accounts-conf"`
|
2019-06-25 09:23:51 +02:00
|
|
|
}
|
|
|
|
|
2018-01-10 01:18:19 +01:00
|
|
|
type UIConfig struct {
|
2019-12-20 19:21:33 +01:00
|
|
|
IndexFormat string `ini:"index-format"`
|
|
|
|
TimestampFormat string `ini:"timestamp-format"`
|
2021-10-26 17:24:45 +02:00
|
|
|
ThisDayTimeFormat string `ini:"this-day-time-format"`
|
2021-11-06 17:32:38 +01:00
|
|
|
ThisWeekTimeFormat string `ini:"this-week-time-format"`
|
2021-10-26 17:24:45 +02:00
|
|
|
ThisYearTimeFormat string `ini:"this-year-time-format"`
|
2019-12-20 19:21:33 +01:00
|
|
|
ShowHeaders []string `delim:","`
|
|
|
|
RenderAccountTabs string `ini:"render-account-tabs"`
|
2020-03-07 17:42:41 +01:00
|
|
|
PinnedTabMarker string `ini:"pinned-tab-marker"`
|
2019-12-20 19:21:33 +01:00
|
|
|
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"`
|
2021-11-12 18:12:02 +01:00
|
|
|
ThreadingEnabled bool `ini:"threading-enabled"`
|
2022-07-05 21:48:39 +02:00
|
|
|
ForceClientThreads bool `ini:"force-client-threads"`
|
2022-07-26 15:41:13 +02:00
|
|
|
ClientThreadsDelay time.Duration `ini:"client-threads-delay"`
|
2022-03-06 03:58:07 +01:00
|
|
|
FuzzyComplete bool `ini:"fuzzy-complete"`
|
2019-12-20 19:21:33 +01:00
|
|
|
NewMessageBell bool `ini:"new-message-bell"`
|
|
|
|
Spinner string `ini:"spinner"`
|
|
|
|
SpinnerDelimiter string `ini:"spinner-delimiter"`
|
2022-06-22 12:19:39 +02:00
|
|
|
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"`
|
2019-12-20 19:21:33 +01:00
|
|
|
DirListFormat string `ini:"dirlist-format"`
|
2022-01-28 10:17:13 +01:00
|
|
|
DirListDelay time.Duration `ini:"dirlist-delay"`
|
2022-02-21 00:18:42 +01:00
|
|
|
DirListTree bool `ini:"dirlist-tree"`
|
2022-08-13 01:44:44 +02:00
|
|
|
DirListCollapse int `ini:"dirlist-collapse"`
|
2019-12-20 19:21:33 +01:00
|
|
|
Sort []string `delim:" "`
|
|
|
|
NextMessageOnDelete bool `ini:"next-message-on-delete"`
|
|
|
|
CompletionDelay time.Duration `ini:"completion-delay"`
|
|
|
|
CompletionPopovers bool `ini:"completion-popovers"`
|
2020-07-27 10:03:55 +02:00
|
|
|
StyleSetDirs []string `ini:"stylesets-dirs" delim:":"`
|
|
|
|
StyleSetName string `ini:"styleset-name"`
|
|
|
|
style StyleSet
|
2021-11-29 23:48:35 +01:00
|
|
|
// customize border appearance
|
|
|
|
BorderCharVertical rune `ini:"-"`
|
|
|
|
BorderCharHorizontal rune `ini:"-"`
|
2018-01-10 01:18:19 +01:00
|
|
|
}
|
|
|
|
|
2020-01-24 18:18:49 +01:00
|
|
|
type ContextType int
|
|
|
|
|
2020-01-23 13:56:48 +01:00
|
|
|
const (
|
2020-01-24 18:18:49 +01:00
|
|
|
UI_CONTEXT_FOLDER ContextType = iota
|
2020-01-23 13:56:48 +01:00
|
|
|
UI_CONTEXT_ACCOUNT
|
|
|
|
UI_CONTEXT_SUBJECT
|
2021-12-10 22:27:29 +01:00
|
|
|
BIND_CONTEXT_ACCOUNT
|
2022-06-19 23:41:15 +02:00
|
|
|
BIND_CONTEXT_FOLDER
|
2020-01-23 13:56:48 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type UIConfigContext struct {
|
2020-01-24 18:18:49 +01:00
|
|
|
ContextType ContextType
|
2020-01-23 13:56:48 +01:00
|
|
|
Regex *regexp.Regexp
|
|
|
|
UiConfig UIConfig
|
|
|
|
}
|
|
|
|
|
2019-03-31 20:24:53 +02:00
|
|
|
const (
|
|
|
|
FILTER_MIMETYPE = iota
|
|
|
|
FILTER_HEADER
|
|
|
|
)
|
|
|
|
|
2022-07-30 22:42:49 +02:00
|
|
|
type RemoteConfig struct {
|
|
|
|
Value string
|
|
|
|
PasswordCmd string
|
2022-09-08 20:18:31 +02:00
|
|
|
CacheCmd bool
|
|
|
|
cache string
|
2022-07-30 22:42:49 +02:00
|
|
|
}
|
|
|
|
|
2022-09-08 20:18:31 +02:00
|
|
|
func (c *RemoteConfig) parseValue() (*url.URL, error) {
|
2022-07-30 22:42:49 +02:00
|
|
|
return url.Parse(c.Value)
|
|
|
|
}
|
|
|
|
|
2022-09-08 20:18:31 +02:00
|
|
|
func (c *RemoteConfig) ConnectionString() (string, error) {
|
2022-07-30 22:42:49 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-09-08 20:18:31 +02:00
|
|
|
pw := c.cache
|
2022-07-30 22:42:49 +02:00
|
|
|
|
2022-09-08 20:18:31 +02:00
|
|
|
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))
|
|
|
|
}
|
2022-07-30 22:42:49 +02:00
|
|
|
u.User = url.UserPassword(u.User.Username(), pw)
|
2022-09-08 20:18:31 +02:00
|
|
|
if c.CacheCmd {
|
|
|
|
c.cache = pw
|
|
|
|
}
|
2022-07-30 22:42:49 +02:00
|
|
|
|
2022-09-08 20:18:31 +02:00
|
|
|
return u.String(), nil
|
2022-07-30 22:42:49 +02:00
|
|
|
}
|
|
|
|
|
2018-01-10 01:18:19 +01:00
|
|
|
type AccountConfig struct {
|
2021-11-13 09:10:09 +01:00
|
|
|
Archive string
|
|
|
|
CopyTo string
|
|
|
|
Default string
|
|
|
|
Postpone string
|
|
|
|
From string
|
|
|
|
Aliases string
|
|
|
|
Name string
|
|
|
|
Source string
|
|
|
|
Folders []string
|
|
|
|
FoldersExclude []string
|
|
|
|
Params map[string]string
|
2022-07-30 22:42:49 +02:00
|
|
|
Outgoing RemoteConfig
|
2021-11-13 09:10:09 +01:00
|
|
|
SignatureFile string
|
|
|
|
SignatureCmd string
|
|
|
|
EnableFoldersSort bool `ini:"enable-folders-sort"`
|
|
|
|
FoldersSort []string `ini:"folders-sort" delim:","`
|
2022-07-11 11:11:37 +02:00
|
|
|
AddressBookCmd string `ini:"address-book-cmd"`
|
2022-09-13 17:12:12 +02:00
|
|
|
SendAsUTC bool `ini:"send-as-utc"`
|
2022-05-05 19:53:14 +02:00
|
|
|
|
2022-05-30 14:34:18 +02:00
|
|
|
// 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"`
|
|
|
|
|
2022-05-05 19:53:14 +02:00
|
|
|
// PGP Config
|
|
|
|
PgpKeyId string `ini:"pgp-key-id"`
|
|
|
|
PgpAutoSign bool `ini:"pgp-auto-sign"`
|
|
|
|
PgpOpportunisticEncrypt bool `ini:"pgp-opportunistic-encrypt"`
|
msgviewer: parse and display authentication results
Parse the Authentication-Results header and display it in the message
viewer (not enabled by default). DKIM, SPF and DMARC authentication
methods are supported. Implement recommendation from RFC 7601 Sec 7.1 to
have an explicit list of trustworthy hostnames before displaying the
authentication results. Be aware that the authentication headers can be
forged.
To display the results for a specific authentication method, add the
corresponding name to the layout of headers in the viewer section of
aerc.conf, e.g. to display all three, use:
header-layout = From|To,Cc|Bcc,Date,Subject,DKIM|SPF|DMARC
More information will be displayed when "+" is appended to the
authentication method name, e.g. DKIM+ or SPF+ or DMARC+.
Also, add the trustworthy hosts per account with the trusted-authres
parameter, e.g.
trusted-authres = *
to trust every host or use regular expressions for a finer control.
Multiple hosts can be entered as a comma-separated list. Authentication
results will only be displayed when the host is listed in the
trusted-authres list.
Link: https://datatracker.ietf.org/doc/html/rfc7601
Signed-off-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-05-30 00:20:41 +02:00
|
|
|
|
|
|
|
// AuthRes
|
|
|
|
TrustedAuthRes []string `ini:"trusted-authres" delim:","`
|
2018-01-10 01:18:19 +01:00
|
|
|
}
|
|
|
|
|
2019-03-21 22:36:42 +01:00
|
|
|
type BindingConfig struct {
|
2022-03-14 04:03:34 +01:00
|
|
|
Global *KeyBindings
|
|
|
|
AccountWizard *KeyBindings
|
|
|
|
Compose *KeyBindings
|
|
|
|
ComposeEditor *KeyBindings
|
|
|
|
ComposeReview *KeyBindings
|
|
|
|
MessageList *KeyBindings
|
|
|
|
MessageView *KeyBindings
|
|
|
|
MessageViewPassthrough *KeyBindings
|
|
|
|
Terminal *KeyBindings
|
2019-03-21 22:36:42 +01:00
|
|
|
}
|
|
|
|
|
2021-12-10 22:27:29 +01:00
|
|
|
type BindingConfigContext struct {
|
|
|
|
ContextType ContextType
|
|
|
|
Regex *regexp.Regexp
|
|
|
|
Bindings *KeyBindings
|
|
|
|
BindContext string
|
|
|
|
}
|
|
|
|
|
2019-05-14 21:25:30 +02:00
|
|
|
type ComposeConfig struct {
|
2019-12-20 19:21:35 +01:00
|
|
|
Editor string `ini:"editor"`
|
|
|
|
HeaderLayout [][]string `ini:"-"`
|
|
|
|
AddressBookCmd string `ini:"address-book-cmd"`
|
2022-01-31 15:28:58 +01:00
|
|
|
ReplyToSelf bool `ini:"reply-to-self"`
|
2019-05-14 21:25:30 +02:00
|
|
|
}
|
|
|
|
|
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 {
|
2019-07-17 22:51:02 +02:00
|
|
|
Pager string
|
|
|
|
Alternatives []string
|
2019-07-18 00:42:03 +02:00
|
|
|
ShowHeaders bool `ini:"show-headers"`
|
|
|
|
AlwaysShowMime bool `ini:"always-show-mime"`
|
2022-06-14 21:10:48 +02:00
|
|
|
ParseHttpLinks bool `ini:"parse-http-links"`
|
2019-07-17 22:51:02 +02:00
|
|
|
HeaderLayout [][]string `ini:"-"`
|
2022-03-14 04:03:34 +01:00
|
|
|
KeyPassthrough bool `ini:"-"`
|
2019-03-31 20:24:53 +02:00
|
|
|
}
|
|
|
|
|
2022-04-18 16:06:27 +02:00
|
|
|
type StatuslineConfig struct {
|
|
|
|
RenderFormat string `ini:"render-format"`
|
|
|
|
Separator string
|
|
|
|
DisplayMode string `ini:"display-mode"`
|
|
|
|
}
|
|
|
|
|
2019-07-21 22:01:51 +02:00
|
|
|
type TriggersConfig struct {
|
|
|
|
NewEmail string `ini:"new-email"`
|
|
|
|
ExecuteCommand func(command []string) error
|
|
|
|
}
|
|
|
|
|
2019-11-03 13:51:14 +01:00
|
|
|
type TemplateConfig struct {
|
2022-03-18 09:53:04 +01:00
|
|
|
TemplateDirs []string `ini:"template-dirs" delim:":"`
|
2022-01-19 21:28:06 +01:00
|
|
|
NewMessage string `ini:"new-message"`
|
2020-01-23 18:18:40 +01:00
|
|
|
QuotedReply string `ini:"quoted-reply"`
|
|
|
|
Forwards string `ini:"forwards"`
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
2018-01-10 01:18:19 +01:00
|
|
|
type AercConfig struct {
|
2021-12-10 22:27:29 +01:00
|
|
|
Bindings BindingConfig
|
|
|
|
ContextualBinds []BindingConfigContext
|
|
|
|
Compose ComposeConfig
|
2022-04-18 16:06:27 +02:00
|
|
|
Ini *ini.File `ini:"-"`
|
|
|
|
Accounts []AccountConfig `ini:"-"`
|
|
|
|
Filters []FilterConfig `ini:"-"`
|
|
|
|
Viewer ViewerConfig `ini:"-"`
|
|
|
|
Statusline StatuslineConfig `ini:"-"`
|
|
|
|
Triggers TriggersConfig `ini:"-"`
|
2021-12-10 22:27:29 +01:00
|
|
|
Ui UIConfig
|
|
|
|
ContextualUis []UIConfigContext
|
|
|
|
General GeneralConfig
|
|
|
|
Templates TemplateConfig
|
2018-01-10 01:18:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2022-08-22 17:38:24 +02:00
|
|
|
func loadAccountConfig(path string, accts []string) ([]AccountConfig, error) {
|
2018-01-10 17:19:45 +01:00
|
|
|
file, err := ini.Load(path)
|
|
|
|
if err != nil {
|
2019-05-22 17:35:55 +02:00
|
|
|
// No config triggers account configuration wizard
|
|
|
|
return nil, nil
|
2018-01-10 03:24:50 +01:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2022-08-22 17:38:24 +02:00
|
|
|
if len(accts) > 0 && !contains(accts, _sec) {
|
|
|
|
continue
|
|
|
|
}
|
2018-01-10 03:24:50 +01:00
|
|
|
sec := file.Section(_sec)
|
2022-07-30 22:42:49 +02:00
|
|
|
sourceRemoteConfig := RemoteConfig{}
|
2019-03-16 02:33:08 +01:00
|
|
|
account := AccountConfig{
|
2021-11-13 09:10:09 +01:00
|
|
|
Archive: "Archive",
|
|
|
|
Default: "INBOX",
|
|
|
|
Postpone: "Drafts",
|
|
|
|
Name: _sec,
|
|
|
|
Params: make(map[string]string),
|
|
|
|
EnableFoldersSort: true,
|
2022-05-30 14:34:18 +02:00
|
|
|
CheckMailTimeout: 10 * time.Second,
|
2019-03-16 02:33:08 +01:00
|
|
|
}
|
2018-01-10 03:24:50 +01:00
|
|
|
if err = sec.MapTo(&account); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for key, val := range sec.KeysHash() {
|
2022-07-31 14:32:48 +02:00
|
|
|
switch key {
|
|
|
|
case "folders":
|
2019-06-05 19:48:00 +02:00
|
|
|
folders := strings.Split(val, ",")
|
|
|
|
sort.Strings(folders)
|
|
|
|
account.Folders = folders
|
2022-07-31 14:32:48 +02:00
|
|
|
case "folders-exclude":
|
2020-07-01 09:52:14 +02:00
|
|
|
folders := strings.Split(val, ",")
|
|
|
|
sort.Strings(folders)
|
|
|
|
account.FoldersExclude = folders
|
2022-07-31 14:32:48 +02:00
|
|
|
case "source":
|
2022-07-30 22:42:49 +02:00
|
|
|
sourceRemoteConfig.Value = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "source-cred-cmd":
|
2022-07-30 22:42:49 +02:00
|
|
|
sourceRemoteConfig.PasswordCmd = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "outgoing":
|
2022-07-30 22:42:49 +02:00
|
|
|
account.Outgoing.Value = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "outgoing-cred-cmd":
|
2022-07-30 22:42:49 +02:00
|
|
|
account.Outgoing.PasswordCmd = val
|
2022-09-08 20:18:31 +02:00
|
|
|
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
|
2022-07-31 14:32:48 +02:00
|
|
|
case "from":
|
2019-05-13 22:04:01 +02:00
|
|
|
account.From = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "aliases":
|
2020-08-20 19:22:50 +02:00
|
|
|
account.Aliases = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "copy-to":
|
2019-05-16 01:41:21 +02:00
|
|
|
account.CopyTo = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "archive":
|
2019-06-08 19:41:56 +02:00
|
|
|
account.Archive = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "enable-folders-sort":
|
2021-11-13 09:10:09 +01:00
|
|
|
account.EnableFoldersSort, _ = strconv.ParseBool(val)
|
2022-07-31 14:32:48 +02:00
|
|
|
case "pgp-key-id":
|
2022-04-25 15:30:44 +02:00
|
|
|
account.PgpKeyId = val
|
2022-07-31 14:32:48 +02:00
|
|
|
case "pgp-auto-sign":
|
2022-05-05 19:53:14 +02:00
|
|
|
account.PgpAutoSign, _ = strconv.ParseBool(val)
|
2022-07-31 14:32:48 +02:00
|
|
|
case "pgp-opportunistic-encrypt":
|
2022-05-05 19:53:14 +02:00
|
|
|
account.PgpOpportunisticEncrypt, _ = strconv.ParseBool(val)
|
2022-07-31 14:32:48 +02:00
|
|
|
case "address-book-cmd":
|
2022-07-11 11:11:37 +02:00
|
|
|
account.AddressBookCmd = val
|
2022-07-31 14:32:48 +02:00
|
|
|
default:
|
|
|
|
if key != "name" {
|
|
|
|
account.Params[key] = val
|
|
|
|
}
|
2018-01-10 03:24:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if account.Source == "" {
|
|
|
|
return nil, fmt.Errorf("Expected source for account %s", _sec)
|
|
|
|
}
|
2020-11-12 22:58:02 +01:00
|
|
|
if account.From == "" {
|
|
|
|
return nil, fmt.Errorf("Expected from for account %s", _sec)
|
|
|
|
}
|
2019-05-18 21:29:26 +02:00
|
|
|
|
2022-07-30 22:42:49 +02:00
|
|
|
source, err := sourceRemoteConfig.ConnectionString()
|
2019-05-18 21:29:26 +02:00
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("Invalid source credentials for %s: %w", _sec, err)
|
2019-05-18 21:29:26 +02:00
|
|
|
}
|
|
|
|
account.Source = source
|
|
|
|
|
2022-07-30 22:42:49 +02:00
|
|
|
_, err = account.Outgoing.parseValue()
|
2019-05-18 21:29:26 +02:00
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("Invalid outgoing credentials for %s: %w", _sec, err)
|
2019-05-18 21:29:26 +02:00
|
|
|
}
|
|
|
|
|
2018-01-10 03:24:50 +01:00
|
|
|
accounts = append(accounts, account)
|
|
|
|
}
|
2022-08-22 17:38:24 +02:00
|
|
|
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 {
|
2022-09-07 08:31:12 +02:00
|
|
|
return strings.ToLower(accts[i]) < strings.ToLower(accts[j])
|
2022-08-22 17:38:24 +02:00
|
|
|
})
|
|
|
|
}
|
2018-01-10 03:24:50 +01:00
|
|
|
return accounts, nil
|
|
|
|
}
|
|
|
|
|
2022-03-02 04:38:59 +01:00
|
|
|
// 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 != "" {
|
2022-03-02 04:39:01 +01:00
|
|
|
v, err := homedir.Expand(v)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
}
|
2022-03-02 04:38:59 +01:00
|
|
|
defaultDirs = append(defaultDirs, path.Join(v, "aerc"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add custom buildtime shareDir
|
|
|
|
if shareDir != "" && shareDir != "/usr/local/share/aerc" {
|
2022-03-02 04:39:01 +01:00
|
|
|
shareDir, err := homedir.Expand(shareDir)
|
|
|
|
if err == nil {
|
|
|
|
defaultDirs = append(defaultDirs, shareDir)
|
|
|
|
}
|
2022-03-02 04:38:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add fixed fallback locations
|
|
|
|
defaultDirs = append(defaultDirs, "/usr/local/share/aerc")
|
|
|
|
defaultDirs = append(defaultDirs, "/usr/share/aerc")
|
|
|
|
|
|
|
|
return defaultDirs
|
2022-02-19 14:06:57 +01:00
|
|
|
}
|
|
|
|
|
2022-03-02 04:38:59 +01:00
|
|
|
var searchDirs = buildDefaultDirs()
|
|
|
|
|
2022-02-19 14:06:57 +01:00
|
|
|
func installTemplate(root, name string) error {
|
|
|
|
var err error
|
|
|
|
if _, err = os.Stat(root); os.IsNotExist(err) {
|
2022-07-31 22:16:40 +02:00
|
|
|
err = os.MkdirAll(root, 0o755)
|
2019-05-22 18:35:44 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2022-02-19 14:06:57 +01:00
|
|
|
var data []byte
|
2022-03-02 04:38:59 +01:00
|
|
|
for _, dir := range searchDirs {
|
2022-08-17 16:19:45 +02:00
|
|
|
data, err = os.ReadFile(path.Join(dir, name))
|
2022-02-19 14:06:57 +01:00
|
|
|
if err == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2019-05-22 18:35:44 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-08-17 16:19:45 +02:00
|
|
|
err = os.WriteFile(path.Join(root, name), data, 0o644)
|
2019-05-22 18:35:44 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-13 19:42:22 +02:00
|
|
|
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,
|
|
|
|
}
|
2022-07-31 14:32:48 +02:00
|
|
|
switch {
|
|
|
|
case strings.Contains(match, ",~"):
|
2019-07-13 19:42:22 +02:00
|
|
|
filter.FilterType = FILTER_HEADER
|
2022-07-31 14:32:48 +02:00
|
|
|
header := filter.Filter[:strings.Index(filter.Filter, ",")] //nolint:gocritic // guarded by strings.Contains
|
2019-07-13 19:42:22 +02:00
|
|
|
regex := filter.Filter[strings.Index(filter.Filter, "~")+1:]
|
|
|
|
filter.Header = strings.ToLower(header)
|
|
|
|
filter.Regex, err = regexp.Compile(regex)
|
|
|
|
if err != nil {
|
2021-04-09 21:26:09 +02:00
|
|
|
return err
|
2019-07-13 19:42:22 +02:00
|
|
|
}
|
2022-07-31 14:32:48 +02:00
|
|
|
case strings.ContainsRune(match, ','):
|
2019-07-13 19:42:22 +02:00
|
|
|
filter.FilterType = FILTER_HEADER
|
2022-07-31 14:32:48 +02:00
|
|
|
header := filter.Filter[:strings.Index(filter.Filter, ",")] //nolint:gocritic // guarded by strings.Contains
|
2019-07-13 19:42:22 +02:00
|
|
|
value := filter.Filter[strings.Index(filter.Filter, ",")+1:]
|
|
|
|
filter.Header = strings.ToLower(header)
|
|
|
|
filter.Regex, err = regexp.Compile(regexp.QuoteMeta(value))
|
2021-04-09 21:26:09 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-07-31 14:32:48 +02:00
|
|
|
default:
|
2019-07-13 19:42:22 +02:00
|
|
|
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 err
|
|
|
|
}
|
|
|
|
for key, val := range viewer.KeysHash() {
|
|
|
|
switch key {
|
|
|
|
case "alternatives":
|
|
|
|
config.Viewer.Alternatives = strings.Split(val, ",")
|
2019-07-15 20:56:44 +02:00
|
|
|
case "header-layout":
|
|
|
|
config.Viewer.HeaderLayout = parseLayout(val)
|
2019-07-13 19:42:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-18 16:06:27 +02:00
|
|
|
if statusline, err := file.GetSection("statusline"); err == nil {
|
|
|
|
if err := statusline.MapTo(&config.Statusline); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2019-07-13 19:42:22 +02:00
|
|
|
if compose, err := file.GetSection("compose"); err == nil {
|
|
|
|
if err := compose.MapTo(&config.Compose); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-07-23 01:29:07 +02:00
|
|
|
for key, val := range compose.KeysHash() {
|
2022-07-31 14:32:48 +02:00
|
|
|
if key == "header-layout" {
|
2019-07-23 01:29:07 +02:00
|
|
|
config.Compose.HeaderLayout = parseLayout(val)
|
|
|
|
}
|
|
|
|
}
|
2019-07-13 19:42:22 +02:00
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
|
2019-07-13 19:42:22 +02:00
|
|
|
if ui, err := file.GetSection("ui"); err == nil {
|
2022-09-04 22:04:40 +02:00
|
|
|
if err := parseUiConfig(ui, &config.Ui); err != nil {
|
2019-07-13 19:42:22 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
|
2020-01-23 13:56:48 +01:00
|
|
|
for _, sectionName := range file.SectionStrings() {
|
|
|
|
if !strings.Contains(sectionName, "ui:") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
uiSection, err := file.GetSection(sectionName)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
uiSubConfig := UIConfig{}
|
2022-09-04 22:04:40 +02:00
|
|
|
if err := parseUiConfig(uiSection, &uiSubConfig); err != nil {
|
2021-11-29 23:48:35 +01:00
|
|
|
return err
|
|
|
|
}
|
2022-07-31 22:16:40 +02:00
|
|
|
contextualUi := UIConfigContext{
|
|
|
|
UiConfig: uiSubConfig,
|
|
|
|
}
|
2020-01-23 13:56:48 +01:00
|
|
|
|
|
|
|
var index int
|
2022-07-31 14:32:48 +02:00
|
|
|
switch {
|
|
|
|
case strings.Contains(sectionName, "~"):
|
2020-01-23 13:56:48 +01:00
|
|
|
index = strings.Index(sectionName, "~")
|
|
|
|
regex := string(sectionName[index+1:])
|
|
|
|
contextualUi.Regex, err = regexp.Compile(regex)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-07-31 14:32:48 +02:00
|
|
|
case strings.Contains(sectionName, "="):
|
2020-01-23 13:56:48 +01:00
|
|
|
index = strings.Index(sectionName, "=")
|
|
|
|
value := string(sectionName[index+1:])
|
|
|
|
contextualUi.Regex, err = regexp.Compile(regexp.QuoteMeta(value))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-07-31 14:32:48 +02:00
|
|
|
default:
|
2020-01-23 13:56:48 +01:00
|
|
|
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)
|
|
|
|
}
|
2019-07-21 22:01:51 +02:00
|
|
|
if triggers, err := file.GetSection("triggers"); err == nil {
|
|
|
|
if err := triggers.MapTo(&config.Triggers); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2019-11-03 13:51:14 +01:00
|
|
|
if templatesSec, err := file.GetSection("templates"); err == nil {
|
|
|
|
if err := templatesSec.MapTo(&config.Templates); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
templateDirs := templatesSec.Key("template-dirs").String()
|
2019-11-10 17:00:21 +01:00
|
|
|
if templateDirs != "" {
|
|
|
|
config.Templates.TemplateDirs = strings.Split(templateDirs, ":")
|
|
|
|
}
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
|
2022-02-19 14:06:57 +01:00
|
|
|
// append default paths to template-dirs and styleset-dirs
|
2022-03-02 04:38:59 +01:00
|
|
|
for _, dir := range searchDirs {
|
2022-02-19 14:06:57 +01:00
|
|
|
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
|
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-13 19:42:22 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-09-04 22:04:40 +02:00
|
|
|
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)
|
2021-11-29 23:48:35 +01:00
|
|
|
}
|
2022-09-04 22:04:40 +02:00
|
|
|
config.BorderCharHorizontal = chars[0]
|
2021-11-29 23:48:35 +01:00
|
|
|
}
|
2022-09-04 22:04:40 +02:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-11-29 23:48:35 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:43 +02:00
|
|
|
func validatePgpProvider(section *ini.Section) error {
|
|
|
|
m := map[string]bool{
|
2022-04-25 15:30:44 +02:00
|
|
|
"gpg": true,
|
2022-04-25 15:30:43 +02:00
|
|
|
"internal": true,
|
|
|
|
}
|
|
|
|
for key, val := range section.KeysHash() {
|
2022-07-31 14:32:48 +02:00
|
|
|
if key == "pgp-provider" {
|
2022-04-25 15:30:43 +02:00
|
|
|
if !m[strings.ToLower(val)] {
|
2022-04-25 15:30:44 +02:00
|
|
|
return fmt.Errorf("%v must be either 'gpg' or 'internal'", key)
|
2022-04-25 15:30:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-08-22 17:38:24 +02:00
|
|
|
func LoadConfigFromFile(root *string, accts []string) (*AercConfig, error) {
|
2018-01-10 01:18:19 +01:00
|
|
|
if root == nil {
|
|
|
|
_root := path.Join(xdg.ConfigHome(), "aerc")
|
|
|
|
root = &_root
|
|
|
|
}
|
2022-04-19 21:14:46 +02:00
|
|
|
filename := path.Join(*root, "aerc.conf")
|
2021-04-17 18:50:35 +02:00
|
|
|
|
|
|
|
// if it doesn't exist copy over the template, then load
|
|
|
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
2022-07-19 21:11:33 +02:00
|
|
|
logging.Debugf("%s not found, installing the system default", filename)
|
2022-02-19 14:06:57 +01:00
|
|
|
if err := installTemplate(*root, "aerc.conf"); err != nil {
|
2021-04-17 18:50:35 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-19 21:11:33 +02:00
|
|
|
logging.Infof("Parsing configuration from %s", filename)
|
|
|
|
|
2020-04-28 16:06:38 +02:00
|
|
|
file, err := ini.LoadSources(ini.LoadOptions{
|
|
|
|
KeyValueDelimiters: "=",
|
|
|
|
}, filename)
|
2018-01-10 17:19:45 +01:00
|
|
|
if err != nil {
|
2021-04-17 18:50:35 +02:00
|
|
|
return nil, err
|
2018-01-10 01:18:19 +01:00
|
|
|
}
|
|
|
|
file.NameMapper = mapName
|
|
|
|
config := &AercConfig{
|
2019-03-21 22:36:42 +01:00
|
|
|
Bindings: BindingConfig{
|
2022-03-14 04:03:34 +01:00
|
|
|
Global: NewKeyBindings(),
|
|
|
|
AccountWizard: NewKeyBindings(),
|
|
|
|
Compose: NewKeyBindings(),
|
|
|
|
ComposeEditor: NewKeyBindings(),
|
|
|
|
ComposeReview: NewKeyBindings(),
|
|
|
|
MessageList: NewKeyBindings(),
|
|
|
|
MessageView: NewKeyBindings(),
|
|
|
|
MessageViewPassthrough: NewKeyBindings(),
|
|
|
|
Terminal: NewKeyBindings(),
|
2019-03-21 22:36:42 +01:00
|
|
|
},
|
2021-12-10 22:27:29 +01:00
|
|
|
|
|
|
|
ContextualBinds: []BindingConfigContext{},
|
|
|
|
|
2019-03-21 22:36:42 +01:00
|
|
|
Ini: file,
|
2019-03-15 06:12:06 +01:00
|
|
|
|
2022-04-19 21:14:46 +02:00
|
|
|
General: GeneralConfig{
|
2022-04-25 15:30:43 +02:00
|
|
|
PgpProvider: "internal",
|
2022-04-19 21:14:46 +02:00
|
|
|
UnsafeAccountsConf: false,
|
|
|
|
},
|
|
|
|
|
2018-01-10 01:18:19 +01:00
|
|
|
Ui: UIConfig{
|
2022-09-04 21:18:21 +02:00
|
|
|
IndexFormat: "%-20.20D %-17.17n %Z %s",
|
2021-10-26 17:24:45 +02:00
|
|
|
TimestampFormat: "2006-01-02 03:04 PM",
|
2022-09-25 10:51:22 +02:00
|
|
|
ThisDayTimeFormat: "",
|
|
|
|
ThisWeekTimeFormat: "",
|
|
|
|
ThisYearTimeFormat: "",
|
2018-01-10 01:18:19 +01:00
|
|
|
ShowHeaders: []string{
|
|
|
|
"From", "To", "Cc", "Bcc", "Subject", "Date",
|
|
|
|
},
|
2019-09-20 21:22:09 +02:00
|
|
|
RenderAccountTabs: "auto",
|
2020-03-07 17:42:41 +01:00
|
|
|
PinnedTabMarker: "`",
|
2019-09-20 21:22:09 +02:00
|
|
|
SidebarWidth: 20,
|
|
|
|
PreviewHeight: 12,
|
|
|
|
EmptyMessage: "(no messages)",
|
|
|
|
EmptyDirlist: "(no folders)",
|
|
|
|
MouseEnabled: false,
|
2022-07-26 15:41:13 +02:00
|
|
|
ClientThreadsDelay: 50 * time.Millisecond,
|
2019-09-20 21:22:09 +02:00
|
|
|
NewMessageBell: true,
|
2022-03-06 03:58:07 +01:00
|
|
|
FuzzyComplete: false,
|
2019-09-20 21:22:09 +02:00
|
|
|
Spinner: "[..] , [..] , [..] , [..] , [..], [..] , [..] , [..] ",
|
|
|
|
SpinnerDelimiter: ",",
|
2022-06-22 12:19:39 +02:00
|
|
|
IconUnencrypted: "",
|
|
|
|
IconSigned: "[s]",
|
|
|
|
IconEncrypted: "[e]",
|
|
|
|
IconSignedEncrypted: "",
|
|
|
|
IconUnknown: "[s?]",
|
|
|
|
IconInvalid: "[s!]",
|
2019-09-20 21:22:09 +02:00
|
|
|
DirListFormat: "%n %>r",
|
2022-01-28 10:17:13 +01:00
|
|
|
DirListDelay: 200 * time.Millisecond,
|
2019-09-20 21:22:09 +02:00
|
|
|
NextMessageOnDelete: true,
|
2019-12-20 19:21:33 +01:00
|
|
|
CompletionDelay: 250 * time.Millisecond,
|
|
|
|
CompletionPopovers: true,
|
2022-02-19 14:06:57 +01:00
|
|
|
StyleSetDirs: []string{},
|
2020-07-27 10:03:55 +02:00
|
|
|
StyleSetName: "default",
|
2021-11-29 23:48:35 +01:00
|
|
|
// border defaults
|
|
|
|
BorderCharVertical: ' ',
|
|
|
|
BorderCharHorizontal: ' ',
|
2018-01-10 01:18:19 +01:00
|
|
|
},
|
2019-07-15 20:56:44 +02:00
|
|
|
|
2020-01-23 13:56:48 +01:00
|
|
|
ContextualUis: []UIConfigContext{},
|
|
|
|
|
2019-07-15 20:56:44 +02:00
|
|
|
Viewer: ViewerConfig{
|
|
|
|
Pager: "less -R",
|
|
|
|
Alternatives: []string{"text/plain", "text/html"},
|
|
|
|
ShowHeaders: false,
|
|
|
|
HeaderLayout: [][]string{
|
|
|
|
{"From", "To"},
|
|
|
|
{"Cc", "Bcc"},
|
|
|
|
{"Date"},
|
|
|
|
{"Subject"},
|
|
|
|
},
|
2022-06-14 21:10:48 +02:00
|
|
|
ParseHttpLinks: true,
|
2019-07-15 20:56:44 +02:00
|
|
|
},
|
2019-07-23 01:29:07 +02:00
|
|
|
|
2022-04-18 16:06:27 +02:00
|
|
|
Statusline: StatuslineConfig{
|
|
|
|
RenderFormat: "[%a] %S %>%T",
|
|
|
|
Separator: " | ",
|
|
|
|
DisplayMode: "",
|
|
|
|
},
|
|
|
|
|
2019-07-23 01:29:07 +02:00
|
|
|
Compose: ComposeConfig{
|
|
|
|
HeaderLayout: [][]string{
|
|
|
|
{"To", "From"},
|
|
|
|
{"Subject"},
|
|
|
|
},
|
2022-01-31 15:28:58 +01:00
|
|
|
ReplyToSelf: true,
|
2019-07-23 01:29:07 +02:00
|
|
|
},
|
2019-11-10 17:00:21 +01:00
|
|
|
|
|
|
|
Templates: TemplateConfig{
|
2022-02-19 14:06:57 +01:00
|
|
|
TemplateDirs: []string{},
|
2022-01-19 21:28:06 +01:00
|
|
|
NewMessage: "new_message",
|
2019-11-10 17:00:21 +01:00
|
|
|
QuotedReply: "quoted_reply",
|
|
|
|
Forwards: "forward_as_body",
|
|
|
|
},
|
2018-01-10 01:18:19 +01:00
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
|
2019-05-21 22:31:04 +02:00
|
|
|
// These bindings are not configurable
|
|
|
|
config.Bindings.AccountWizard.ExKey = KeyStroke{
|
|
|
|
Key: tcell.KeyCtrlE,
|
|
|
|
}
|
|
|
|
quit, _ := ParseBinding("<C-q>", ":quit<Enter>")
|
|
|
|
config.Bindings.AccountWizard.Add(quit)
|
2019-07-13 19:42:22 +02:00
|
|
|
|
|
|
|
if err = config.LoadConfig(file); err != nil {
|
|
|
|
return nil, err
|
2018-01-10 01:18:19 +01:00
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
|
2019-06-25 09:23:51 +02:00
|
|
|
if ui, err := file.GetSection("general"); err == nil {
|
|
|
|
if err := ui.MapTo(&config.General); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-04-25 15:30:43 +02:00
|
|
|
if err := validatePgpProvider(ui); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-06-25 09:23:51 +02:00
|
|
|
}
|
2019-07-13 19:42:22 +02:00
|
|
|
|
2022-07-19 21:11:33 +02:00
|
|
|
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: [triggers] %#v", config.Triggers)
|
|
|
|
logging.Debugf("aerc.conf: [templates] %#v", config.Templates)
|
|
|
|
|
2022-04-19 21:14:46 +02:00
|
|
|
filename = path.Join(*root, "accounts.conf")
|
|
|
|
if !config.General.UnsafeAccountsConf {
|
|
|
|
if err := checkConfigPerms(filename); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-10 03:24:50 +01:00
|
|
|
accountsPath := path.Join(*root, "accounts.conf")
|
2022-07-19 21:11:33 +02:00
|
|
|
logging.Infof("Parsing accounts configuration from %s", accountsPath)
|
2022-08-22 17:38:24 +02:00
|
|
|
if accounts, err := loadAccountConfig(accountsPath, accts); err != nil {
|
2018-01-10 03:24:50 +01:00
|
|
|
return nil, err
|
|
|
|
} else {
|
|
|
|
config.Accounts = accounts
|
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
|
2022-07-19 21:11:33 +02:00
|
|
|
for _, acct := range config.Accounts {
|
|
|
|
logging.Debugf("accounts.conf: [%s] from = %s", acct.Name, acct.From)
|
|
|
|
}
|
|
|
|
|
2019-05-22 18:35:44 +02:00
|
|
|
filename = path.Join(*root, "binds.conf")
|
2022-07-19 21:11:33 +02:00
|
|
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
|
|
|
logging.Debugf("%s not found, installing the system default", filename)
|
2022-02-19 14:06:57 +01:00
|
|
|
if err := installTemplate(*root, "binds.conf"); err != nil {
|
2019-05-22 18:35:44 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
2022-07-19 21:11:33 +02:00
|
|
|
}
|
|
|
|
logging.Infof("Parsing key bindings configuration from %s", filename)
|
|
|
|
binds, err := ini.Load(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-03-21 22:36:42 +01:00
|
|
|
}
|
2019-05-14 20:27:28 +02:00
|
|
|
|
2021-12-10 22:27:29 +01:00
|
|
|
baseGroups := map[string]**KeyBindings{
|
2022-03-14 04:03:34 +01:00
|
|
|
"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,
|
2019-03-21 22:36:42 +01:00
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
|
|
|
|
// Base Bindings
|
|
|
|
for _, sectionName := range binds.SectionStrings() {
|
|
|
|
// Handle :: delimeter
|
2022-07-31 14:32:48 +02:00
|
|
|
baseSectionName := strings.ReplaceAll(sectionName, "::", "////")
|
2021-12-10 22:27:29 +01:00
|
|
|
sections := strings.Split(baseSectionName, ":")
|
|
|
|
baseOnly := len(sections) == 1
|
2022-07-31 14:32:48 +02:00
|
|
|
baseSectionName = strings.ReplaceAll(sections[0], "////", "::")
|
2021-12-10 22:27:29 +01:00
|
|
|
|
|
|
|
group, ok := baseGroups[strings.ToLower(baseSectionName)]
|
2019-03-21 22:36:42 +01:00
|
|
|
if !ok {
|
2021-12-10 22:27:29 +01:00
|
|
|
return nil, errors.New("Unknown keybinding group " + sectionName)
|
2019-03-21 22:36:42 +01:00
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
|
|
|
|
if baseOnly {
|
2022-07-19 22:31:51 +02:00
|
|
|
err = config.LoadBinds(binds, baseSectionName, group)
|
2019-03-21 22:36:42 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
|
2019-03-21 22:36:42 +01:00
|
|
|
config.Bindings.Global.Globals = false
|
2021-12-10 22:27:29 +01:00
|
|
|
for _, contextBind := range config.ContextualBinds {
|
|
|
|
if contextBind.BindContext == "default" {
|
|
|
|
contextBind.Bindings.Globals = false
|
|
|
|
}
|
|
|
|
}
|
2022-07-19 21:11:33 +02:00
|
|
|
logging.Debugf("binds.conf: %#v", config.Bindings)
|
2021-12-10 22:27:29 +01:00
|
|
|
|
2018-01-10 01:18:19 +01:00
|
|
|
return config, nil
|
|
|
|
}
|
2019-05-16 23:26:08 +02:00
|
|
|
|
2021-12-10 22:27:29 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-07-19 22:31:51 +02:00
|
|
|
func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
|
2021-12-10 22:27:29 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-07-31 22:16:40 +02:00
|
|
|
contextualBind := BindingConfigContext{
|
|
|
|
Bindings: binds,
|
|
|
|
BindContext: baseName,
|
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
|
|
|
|
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":
|
2021-12-11 23:12:13 +01:00
|
|
|
acctName := sectionName[index+1:]
|
|
|
|
valid := false
|
|
|
|
for _, acctConf := range config.Accounts {
|
|
|
|
matches := contextualBind.Regex.FindString(acctConf.Name)
|
|
|
|
if matches != "" {
|
|
|
|
valid = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !valid {
|
2022-07-19 22:31:51 +02:00
|
|
|
logging.Warnf("binds.conf: unexistent account: %s", acctName)
|
2022-01-24 11:45:35 +01:00
|
|
|
continue
|
2021-12-11 23:12:13 +01:00
|
|
|
}
|
2021-12-10 22:27:29 +01:00
|
|
|
contextualBind.ContextType = BIND_CONTEXT_ACCOUNT
|
2022-06-19 23:41:15 +02:00
|
|
|
case "folder":
|
|
|
|
// No validation needed. If the folder doesn't exist, the binds
|
|
|
|
// never get used
|
|
|
|
contextualBind.ContextType = BIND_CONTEXT_FOLDER
|
2021-12-10 22:27:29 +01:00
|
|
|
default:
|
|
|
|
return fmt.Errorf("Unknown Context Bind Section: %s", sectionName)
|
|
|
|
}
|
|
|
|
config.ContextualBinds = append(config.ContextualBinds, contextualBind)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-05-16 23:26:08 +02:00
|
|
|
// 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)
|
2022-04-14 12:54:26 +02:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2019-05-22 17:35:55 +02:00
|
|
|
return nil // disregard absent files
|
2019-05-16 23:26:08 +02:00
|
|
|
}
|
2022-04-14 12:54:26 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-16 23:26:08 +02:00
|
|
|
perms := info.Mode().Perm()
|
|
|
|
// group or others have read access
|
2022-07-31 22:16:40 +02:00
|
|
|
if perms&0o44 != 0 {
|
2019-07-28 15:02:09 +02:00
|
|
|
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)
|
2019-05-16 23:26:08 +02:00
|
|
|
return errors.New("account.conf permissions too lax")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2019-07-15 20:56:44 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2020-01-23 13:56:48 +01:00
|
|
|
|
2020-07-27 10:03:55 +02:00
|
|
|
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
|
|
|
|
ui.style = NewStyleSet()
|
|
|
|
err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return fmt.Errorf("Unable to load default styleset: %w", err)
|
2020-07-27 10:03:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (config AercConfig) mergeContextualUi(baseUi UIConfig,
|
2022-07-31 22:16:40 +02:00
|
|
|
contextType ContextType, s string,
|
|
|
|
) UIConfig {
|
2020-01-23 13:56:48 +01:00
|
|
|
for _, contextualUi := range config.ContextualUis {
|
|
|
|
if contextualUi.ContextType != contextType {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if !contextualUi.Regex.Match([]byte(s)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-07-29 22:31:54 +02:00
|
|
|
err := mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
|
|
|
|
if err != nil {
|
|
|
|
logging.Warnf("merge ui failed: %v", err)
|
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
if contextualUi.UiConfig.StyleSetName != "" {
|
|
|
|
baseUi.style = contextualUi.UiConfig.style
|
|
|
|
}
|
|
|
|
return baseUi
|
2020-01-23 13:56:48 +01:00
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
|
|
|
|
return baseUi
|
2020-01-23 13:56:48 +01:00
|
|
|
}
|
|
|
|
|
2022-07-03 17:11:12 +02:00
|
|
|
func (config AercConfig) GetUiConfig(params map[ContextType]string) *UIConfig {
|
2020-01-23 13:56:48 +01:00
|
|
|
baseUi := config.Ui
|
|
|
|
|
|
|
|
for k, v := range params {
|
2020-07-27 10:03:55 +02:00
|
|
|
baseUi = config.mergeContextualUi(baseUi, k, v)
|
2020-01-23 13:56:48 +01:00
|
|
|
}
|
|
|
|
|
2022-07-03 17:11:12 +02:00
|
|
|
return &baseUi
|
2020-01-23 13:56:48 +01:00
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
|
2022-07-03 17:11:13 +02:00
|
|
|
func (config *AercConfig) GetContextualUIConfigs() []UIConfigContext {
|
|
|
|
return config.ContextualUis
|
|
|
|
}
|
|
|
|
|
2020-07-27 10:03:55 +02:00
|
|
|
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)
|
|
|
|
}
|
2020-10-27 19:56:44 +01:00
|
|
|
|
|
|
|
func (uiConfig UIConfig) GetComposedStyle(base StyleObject,
|
2022-07-31 22:16:40 +02:00
|
|
|
styles []StyleObject,
|
|
|
|
) tcell.Style {
|
2020-10-27 19:56:44 +01:00
|
|
|
return uiConfig.style.Compose(base, styles)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (uiConfig UIConfig) GetComposedStyleSelected(base StyleObject, styles []StyleObject) tcell.Style {
|
|
|
|
return uiConfig.style.ComposeSelected(base, styles)
|
|
|
|
}
|
2022-08-22 17:38:24 +02:00
|
|
|
|
|
|
|
func contains(list []string, v string) bool {
|
|
|
|
for _, item := range list {
|
|
|
|
if item == v {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|