papero/config/config.go
2021-04-01 18:09:04 +02:00

212 lines
5.4 KiB
Go

package config
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"path"
"strings"
"git.sr.ht/~blallo/papero/worker"
"github.com/mitchellh/go-homedir"
)
var ErrPortOutOfRange = errors.New("port out of range")
var ErrMissingPassword = errors.New("password has not been set")
var ErrMissingDefaultAccount = errors.New("default_account is missing from config")
// CastingErrors is a simple wrapper around map[string]error. It's endowed with a utility
// function to check if there are any errors.
type CastingErrors struct {
GeneralErrors []error
AccountErrors map[string]error
}
func initCastingErrors() *CastingErrors {
c := CastingErrors{}
c.AccountErrors = make(map[string]error)
return &c
}
func (c *CastingErrors) appendGeneralError(e error) {
c.GeneralErrors = append(c.GeneralErrors, e)
}
func (c *CastingErrors) String() string {
out := "CastingErrors{"
out += fmt.Sprintf("GeneralErrors: %s, ", c.GeneralErrors)
out += fmt.Sprintf("AccountErrors: %s", c.AccountErrors)
out += "}"
return out
}
// Check if there are any errors, i.e. if any value of the map is not nil.
func (c CastingErrors) Check() bool {
if len(c.GeneralErrors) != 0 {
return false
}
for _, err := range c.GeneralErrors {
if err != nil {
return false
}
}
return true
}
// MemConfig is the data structure that will be used program-wise. It holds all the
// necessary information to authenticate and manage each provided account.
type MemConfig struct {
Default string
Accounts map[string]*AccountData
Workers map[string]*worker.Worker
}
func initMemConfig() MemConfig {
m := MemConfig{}
m.Accounts = make(map[string]*AccountData)
m.Workers = make(map[string]*worker.Worker)
return m
}
// AccountData holds the data for the single account.
type AccountData struct {
Host string
Port int
Username string
Password string
ExcludedFolders []string
MailboxPath string
Messages int
}
func (a *AccountData) String() string {
var res string
res = "{"
res += fmt.Sprintf("Host: %s, ", a.Host)
res += fmt.Sprintf("Port: %d, ", a.Port)
res += fmt.Sprintf("Username: %s, ", a.Username)
res += fmt.Sprintf("Password: ********, ")
res += fmt.Sprintf("ExcludedFolders: %s, ", a.ExcludedFolders)
res += fmt.Sprintf("MailboxPath: %s}", a.MailboxPath)
res += fmt.Sprintf("Messages: %d}", a.Messages)
return res
}
// parseConfig translates a *fileConfig, as obtained from a file read, into a usable
// *MemConfig that could be used in computation, together with a custom *CastingErrors
// type, that holds the translation errors for each account.
func parseConfig(fileConfig *fileConfig) (*MemConfig, *CastingErrors) {
errors := initCastingErrors()
outConfig := initMemConfig()
defaultAccount := fileConfig.Default
if defaultAccount == "" {
errors.appendGeneralError(ErrMissingDefaultAccount)
}
outConfig.Default = defaultAccount
basePath, err := getOrDefaultMailbox(fileConfig.MailboxPath)
defaultMessages := getOrDefaultMessages(fileConfig.DefaultMessages)
if err != nil {
log.Fatal("Could not determine base path")
}
for _, account := range fileConfig.Accounts {
outConfig.Accounts[account.Name], errors.AccountErrors[account.Name] = parseData(&account, basePath, defaultMessages)
outConfig.Workers[account.Name] = &worker.Worker{}
}
return &outConfig, errors
}
func getOrDefaultMailbox(mailboxPath string) (string, error) {
if mailboxPath == "" {
if homePath, err := homedir.Dir(); err != nil {
return "", err
} else {
return path.Join(homePath, ".papero"), nil
}
}
return mailboxPath, nil
}
func getOrDefaultMessages(messages maybeInt) int {
if messages.empty() {
return 50
}
return messages.value()
}
func valueOrEmptySlice(value []string) []string {
if value == nil {
return []string{}
}
return value
}
func valueOrDefaultInt(value maybeInt, defaultVal int) int {
if value.empty() {
return defaultVal
}
return value.value()
}
func parseData(a *account, basePath string, defaultMessages int) (*AccountData, error) {
port, err := validatePort(a.ConnectionInfo.Port)
if err != nil {
return nil, err
}
password, err := getPassword(a.ConnectionInfo)
if err != nil {
return nil, err
}
accountData := AccountData{
Host: a.ConnectionInfo.Host,
Port: port,
Username: a.ConnectionInfo.Username,
Password: password,
ExcludedFolders: valueOrEmptySlice(a.ExcludedFolders),
MailboxPath: getMailboxPath(a.Name, a.MailboxPath, basePath),
Messages: valueOrDefaultInt(a.Messages, defaultMessages),
}
return &accountData, nil
}
func validatePort(port int) (int, error) {
if port > 0 && port < 2<<15 {
return port, nil
}
return 0, ErrPortOutOfRange
}
func getPassword(connection *connectionInfo) (string, error) {
if connection.PasswordExec.Present() {
pass, err := connection.PasswordExec.Run()
if err != nil {
return "", err
}
return strings.TrimRight(pass, "\n"), nil
}
if connection.PasswordFile != "" {
file, err := os.Open(connection.PasswordFile)
if err != nil {
return "", err
}
reader := bufio.NewReader(file)
line, _, err := reader.ReadLine()
return string(line), err
}
if connection.Password == "" {
return "", ErrMissingPassword
}
return connection.Password, nil
}
func getMailboxPath(name, configPath, basePath string) string {
if configPath != "" {
if info, err := os.Stat(configPath); err == nil || err != os.ErrNotExist && info != nil {
if info.IsDir() {
return configPath
}
}
}
return path.Join(basePath, name)
}