Adding config package
This commit is contained in:
parent
cee6d56d27
commit
483f84a443
8 changed files with 548 additions and 0 deletions
177
config/config.go
Normal file
177
config/config.go
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.lattuga.net/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")
|
||||||
|
|
||||||
|
// 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 map[string]error
|
||||||
|
|
||||||
|
// Check if there are any errors, i.e. if any value of the map is not nil.
|
||||||
|
func (c CastingErrors) Check() bool {
|
||||||
|
for _, err := range c {
|
||||||
|
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 {
|
||||||
|
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) {
|
||||||
|
outConfig := initMemConfig()
|
||||||
|
basePath, err := getOrDefaultMailbox(fileConfig.MailboxPath)
|
||||||
|
defaultMessages := getOrDefaultMessages(fileConfig.DefaultMessages)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Could not determine base path")
|
||||||
|
}
|
||||||
|
errors := make(map[string]error)
|
||||||
|
for _, account := range fileConfig.Accounts {
|
||||||
|
outConfig.Accounts[account.Name], errors[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() {
|
||||||
|
return connection.PasswordExec.Run()
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
log.Printf("name: %s,\tconfig_path: %s,\tbase_path: %s\n", name, configPath, basePath)
|
||||||
|
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)
|
||||||
|
}
|
52
config/config_test.go
Normal file
52
config/config_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.lattuga.net/blallo/papero/worker"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseConfig(t *testing.T) {
|
||||||
|
expected := &MemConfig{
|
||||||
|
Accounts: map[string]*AccountData{
|
||||||
|
"First Account": &AccountData{
|
||||||
|
Host: "mx.example.com",
|
||||||
|
Port: 993,
|
||||||
|
Username: "first@example.com",
|
||||||
|
Password: "123qweasdzxc",
|
||||||
|
ExcludedFolders: []string{"Draft", "Junk"},
|
||||||
|
MailboxPath: "/opt",
|
||||||
|
Messages: 30,
|
||||||
|
},
|
||||||
|
"Other Account": &AccountData{
|
||||||
|
Host: "mail.personal.me",
|
||||||
|
Port: 666,
|
||||||
|
Username: "h4x0R@personal.me",
|
||||||
|
Password: "mySup3r5ekre7p4ssw0rd123",
|
||||||
|
ExcludedFolders: []string{},
|
||||||
|
MailboxPath: "../base/mailbox/Other Account",
|
||||||
|
Messages: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Workers: map[string]*worker.Worker{
|
||||||
|
"First Account": &worker.Worker{},
|
||||||
|
"Other Account": &worker.Worker{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
file := writeToTempFile(t, "papero.*.toml", testConfig)
|
||||||
|
|
||||||
|
fileConfig, err := parseFile(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, errs := parseConfig(fileConfig)
|
||||||
|
if !errs.Check() {
|
||||||
|
t.Fatal(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmp.Equal(result.Accounts, expected.Accounts) {
|
||||||
|
t.Errorf("Result and expected result differ: %+v\n", cmp.Diff(result.Accounts, expected.Accounts))
|
||||||
|
}
|
||||||
|
}
|
129
config/data.go
Normal file
129
config/data.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maybeInt is an integer that may be nil
|
||||||
|
type maybeInt struct {
|
||||||
|
int
|
||||||
|
bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m maybeInt) empty() bool {
|
||||||
|
return !m.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m maybeInt) value() int {
|
||||||
|
return m.int
|
||||||
|
}
|
||||||
|
|
||||||
|
func initMaybeInt(value int) maybeInt {
|
||||||
|
return maybeInt{value, true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maybeInt) UnmarshalText(text []byte) error {
|
||||||
|
var err error
|
||||||
|
var i int64
|
||||||
|
if text != nil {
|
||||||
|
str := string(text)
|
||||||
|
i, err = strconv.ParseInt(str, 10, 64)
|
||||||
|
m.int = int(i)
|
||||||
|
m.bool = true
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileConfig struct {
|
||||||
|
MailboxPath string `toml:"mailbox_path"`
|
||||||
|
Accounts []account `toml:"account"`
|
||||||
|
DefaultMessages maybeInt `toml:"default_messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type account struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
MailboxPath string `toml:"mailbox_path"`
|
||||||
|
ConnectionInfo *connectionInfo `toml:"connection"`
|
||||||
|
ExcludedFolders []string `toml:"excluded_folders"`
|
||||||
|
Messages maybeInt `toml:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type connectionInfo struct {
|
||||||
|
Host string `toml:"hostname"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
Username string `toml:"username"`
|
||||||
|
PasswordExec *ShellScript `toml:"password_exec"`
|
||||||
|
PasswordFile string `toml:"password_file"`
|
||||||
|
Password string `toml:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectionInfo) String() string {
|
||||||
|
var res string
|
||||||
|
res += "{"
|
||||||
|
res += fmt.Sprintf("Host: %s, ", c.Host)
|
||||||
|
res += fmt.Sprintf("Port: %d, ", c.Port)
|
||||||
|
res += fmt.Sprintf("Username: %s, ", c.Username)
|
||||||
|
res += fmt.Sprintf("Password: **********, ")
|
||||||
|
res += fmt.Sprintf("PasswordFile: %s, ", c.PasswordFile)
|
||||||
|
res += fmt.Sprintf("PasswordExec: %s}", c.PasswordExec)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShellScript might be either:
|
||||||
|
// - the path to an executable
|
||||||
|
// - an executable in path
|
||||||
|
// - a shell command with its argument
|
||||||
|
// all these must return on stdout the password after successful execution.
|
||||||
|
type ShellScript struct {
|
||||||
|
Executable string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShellScript) UnmarshalText(text []byte) error {
|
||||||
|
s.Executable = string(text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShellScript) String() string {
|
||||||
|
return s.Executable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present indicates if the value has been filled in into the config
|
||||||
|
func (s *ShellScript) Present() bool {
|
||||||
|
if s != nil {
|
||||||
|
return s.Executable != ""
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tries to execute the command, first looking for a corresponding executable and
|
||||||
|
// invoking it, then invoking the string as a command followed by its parameters.
|
||||||
|
func (s *ShellScript) Run() (string, error) {
|
||||||
|
if path, err := exec.LookPath(s.Executable); err == nil {
|
||||||
|
return doExec(path)
|
||||||
|
}
|
||||||
|
parts := strings.Split(s.Executable, " ")
|
||||||
|
return doExec(parts[0], parts[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doExec(path string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(path, args...)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error running:", cmd)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFile reads the given config file and, if succeeding, returns a *FileConfig.
|
||||||
|
func parseFile(path string) (*fileConfig, error) {
|
||||||
|
newConfig := fileConfig{}
|
||||||
|
_, err := toml.DecodeFile(path, &newConfig)
|
||||||
|
return &newConfig, err
|
||||||
|
}
|
83
config/data_test.go
Normal file
83
config/data_test.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShellScriptRunInlineScript(t *testing.T) {
|
||||||
|
s := ShellScript{Executable: "echo -n hello world!"}
|
||||||
|
doTestShellScriptRun(t, &s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellScriptRunExecutable(t *testing.T) {
|
||||||
|
file := writeToTempFile(t, "papero.*.sh", "#!/usr/bin/env sh\necho -n hello world!\n")
|
||||||
|
s := ShellScript{Executable: file.Name()}
|
||||||
|
doTestShellScriptRun(t, &s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellScriptRunExecutableWithArgs(t *testing.T) {
|
||||||
|
file := writeToTempFile(t, "papero.*.sh", "#!/usr/bin/env sh\necho -n ${@}\n")
|
||||||
|
s := ShellScript{Executable: fmt.Sprintf("%s %s %s", file.Name(), "hello", "world!")}
|
||||||
|
doTestShellScriptRun(t, &s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFile(t *testing.T) {
|
||||||
|
expected := &fileConfig{
|
||||||
|
MailboxPath: "../base/mailbox",
|
||||||
|
DefaultMessages: initMaybeInt(50),
|
||||||
|
Accounts: []account{
|
||||||
|
{
|
||||||
|
Name: "First Account",
|
||||||
|
MailboxPath: "/other/path",
|
||||||
|
ExcludedFolders: []string{"Draft", "Junk"},
|
||||||
|
Messages: initMaybeInt(30),
|
||||||
|
ConnectionInfo: &connectionInfo{
|
||||||
|
Host: "mx.example.com",
|
||||||
|
Port: 993,
|
||||||
|
Username: "first@example.com",
|
||||||
|
Password: "",
|
||||||
|
PasswordFile: "",
|
||||||
|
PasswordExec: &ShellScript{Executable: "echo -n 123qweasdzxc"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Other Account",
|
||||||
|
MailboxPath: "",
|
||||||
|
ExcludedFolders: []string{},
|
||||||
|
Messages: maybeInt{},
|
||||||
|
ConnectionInfo: &connectionInfo{
|
||||||
|
Host: "mail.personal.me",
|
||||||
|
Port: 666,
|
||||||
|
Username: "h4x0R@personal.me",
|
||||||
|
Password: "mySup3r5ekre7p4ssw0rd123",
|
||||||
|
PasswordFile: "",
|
||||||
|
PasswordExec: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
file := writeToTempFile(t, "papero.*.toml", testConfig)
|
||||||
|
|
||||||
|
result, err := parseFile(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := cmp.Comparer(func(a, b maybeInt) bool {
|
||||||
|
if a.empty() {
|
||||||
|
if b.empty() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.value() == b.value()
|
||||||
|
})
|
||||||
|
|
||||||
|
if cmp.Equal(result, expected, opt) {
|
||||||
|
t.Errorf("Result and expected result differ: %+v\n", cmp.Diff(*result, *expected))
|
||||||
|
}
|
||||||
|
}
|
54
config/helper_test.go
Normal file
54
config/helper_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testConfig = `
|
||||||
|
mailbox_path = "../base/mailbox"
|
||||||
|
default_messages = 50
|
||||||
|
|
||||||
|
[[account]]
|
||||||
|
name = "First Account"
|
||||||
|
mailbox_path = "/opt"
|
||||||
|
messages = 30
|
||||||
|
excluded_folders = ["Draft", "Junk"]
|
||||||
|
[account.connection]
|
||||||
|
hostname = "mx.example.com"
|
||||||
|
port = 993
|
||||||
|
username = "first@example.com"
|
||||||
|
password_exec = "echo -n 123qweasdzxc"
|
||||||
|
|
||||||
|
[[account]]
|
||||||
|
name = "Other Account"
|
||||||
|
[account.connection]
|
||||||
|
hostname = "mail.personal.me"
|
||||||
|
port = 666
|
||||||
|
username = "h4x0R@personal.me"
|
||||||
|
password = "mySup3r5ekre7p4ssw0rd123"
|
||||||
|
`
|
||||||
|
|
||||||
|
func doTestShellScriptRun(t *testing.T, s *ShellScript) {
|
||||||
|
if res, err := s.Run(); res != "hello world!" || err != nil {
|
||||||
|
t.Errorf("Unexpected result:\n\tres -> %s (type: %T)\n\terr -> %s", res, res, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeToTempFile(t *testing.T, fileName, content string) *os.File {
|
||||||
|
dir := t.TempDir()
|
||||||
|
file, err := ioutil.TempFile(dir, fileName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = ioutil.WriteFile(file.Name(), []byte(content), 0777); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = os.Chmod(file.Name(), 0777)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
return file
|
||||||
|
}
|
21
config/parse.go
Normal file
21
config/parse.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrFailedToParseConfig = errors.New("unable to cast into usable configuration")
|
||||||
|
|
||||||
|
func Parse(filePath string) (*MemConfig, error) {
|
||||||
|
fileConfig, err := parseFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
memConfig, castingErrs := parseConfig(fileConfig)
|
||||||
|
if !castingErrs.Check() {
|
||||||
|
log.Print(castingErrs)
|
||||||
|
return nil, ErrFailedToParseConfig
|
||||||
|
}
|
||||||
|
return memConfig, nil
|
||||||
|
}
|
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module git.lattuga.net/blallo/papero
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v0.3.1
|
||||||
|
github.com/emersion/go-imap v1.0.6 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.4
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
)
|
22
go.sum
Normal file
22
go.sum
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emersion/go-imap v1.0.6 h1:N9+o5laOGuntStBo+BOgfEB5evPsPD+K5+M0T2dctIc=
|
||||||
|
github.com/emersion/go-imap v1.0.6/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||||
|
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
|
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
Loading…
Reference in a new issue