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