mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
config: Replace robfig with envconfig for #86
- Initial envconfig system is working, not bulletproof. - Added sane defaults for required parameters.
This commit is contained in:
@@ -1,61 +1,22 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/config"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
// SMTPConfig contains the SMTP server configuration - not using pointers so that we can pass around
|
||||
// copies of the object safely.
|
||||
type SMTPConfig struct {
|
||||
IP4address net.IP
|
||||
IP4port int
|
||||
Domain string
|
||||
DomainNoStore string
|
||||
MaxRecipients int
|
||||
MaxIdleSeconds int
|
||||
MaxMessageBytes int
|
||||
StoreMessages bool
|
||||
}
|
||||
|
||||
// POP3Config contains the POP3 server configuration
|
||||
type POP3Config struct {
|
||||
IP4address net.IP
|
||||
IP4port int
|
||||
Domain string
|
||||
MaxIdleSeconds int
|
||||
}
|
||||
|
||||
// WebConfig contains the HTTP server configuration
|
||||
type WebConfig struct {
|
||||
IP4address net.IP
|
||||
IP4port int
|
||||
TemplateDir string
|
||||
TemplateCache bool
|
||||
PublicDir string
|
||||
GreetingFile string
|
||||
MailboxPrompt string
|
||||
CookieAuthKey string
|
||||
MonitorVisible bool
|
||||
MonitorHistory int
|
||||
}
|
||||
|
||||
// DataStoreConfig contains the mail store configuration
|
||||
type DataStoreConfig struct {
|
||||
Path string
|
||||
RetentionMinutes int
|
||||
RetentionSleep int
|
||||
MailboxMsgCap int
|
||||
}
|
||||
|
||||
const (
|
||||
missingErrorFmt = "[%v] missing required option %q"
|
||||
parseErrorFmt = "[%v] option %q error: %v"
|
||||
prefix = "inbucket"
|
||||
tableFormat = `Inbucket is configured via the environment. The following environment
|
||||
variables can be used:
|
||||
|
||||
KEY DEFAULT REQUIRED DESCRIPTION
|
||||
{{range .}}{{usage_key .}} {{usage_default .}} {{usage_required .}} {{usage_description .}}
|
||||
{{end}}`
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -64,207 +25,68 @@ var (
|
||||
|
||||
// BuildDate for this build, set by main
|
||||
BuildDate = ""
|
||||
|
||||
// Config is our global robfig/config object
|
||||
Config *config.Config
|
||||
logLevel string
|
||||
|
||||
// Parsed specific configs
|
||||
smtpConfig = &SMTPConfig{}
|
||||
pop3Config = &POP3Config{}
|
||||
webConfig = &WebConfig{}
|
||||
dataStoreConfig = &DataStoreConfig{}
|
||||
)
|
||||
|
||||
// GetSMTPConfig returns a copy of the SmtpConfig object
|
||||
func GetSMTPConfig() SMTPConfig {
|
||||
return *smtpConfig
|
||||
// Root wraps all other configurations.
|
||||
type Root struct {
|
||||
LogLevel string `required:"true" default:"INFO" desc:"TRACE, INFO, WARN, or ERROR"`
|
||||
SMTP SMTP
|
||||
POP3 POP3
|
||||
Web Web
|
||||
Storage Storage
|
||||
}
|
||||
|
||||
// GetPOP3Config returns a copy of the Pop3Config object
|
||||
func GetPOP3Config() POP3Config {
|
||||
return *pop3Config
|
||||
// SMTP contains the SMTP server configuration.
|
||||
type SMTP struct {
|
||||
Addr string `required:"true" default:"0.0.0.0:2500" desc:"SMTP server IP4 host:port"`
|
||||
Domain string `required:"true" default:"inbucket" desc:"HELO domain"`
|
||||
DomainNoStore string `desc:"Load testing domain"`
|
||||
MaxRecipients int `required:"true" default:"200" desc:"Maximum RCPT TO per message"`
|
||||
MaxIdle time.Duration `required:"true" default:"300s" desc:"Idle network timeout"`
|
||||
MaxMessageBytes int `required:"true" default:"2048000" desc:"Maximum message size"`
|
||||
StoreMessages bool `required:"true" default:"true" desc:"Store incoming mail?"`
|
||||
}
|
||||
|
||||
// GetWebConfig returns a copy of the WebConfig object
|
||||
func GetWebConfig() WebConfig {
|
||||
return *webConfig
|
||||
// POP3 contains the POP3 server configuration.
|
||||
type POP3 struct {
|
||||
Addr string `required:"true" default:"0.0.0.0:1100" desc:"POP3 server IP4 host:port"`
|
||||
Domain string `required:"true" default:"inbucket" desc:"HELLO domain"`
|
||||
MaxIdle time.Duration `required:"true" default:"600s" desc:"Idle network timeout"`
|
||||
}
|
||||
|
||||
// GetDataStoreConfig returns a copy of the DataStoreConfig object
|
||||
func GetDataStoreConfig() DataStoreConfig {
|
||||
return *dataStoreConfig
|
||||
// Web contains the HTTP server configuration.
|
||||
type Web struct {
|
||||
Addr string `required:"true" default:"0.0.0.0:9000" desc:"Web server IP4 host:port"`
|
||||
TemplateDir string `required:"true" default:"themes/bootstrap/templates" desc:"Theme template dir"`
|
||||
TemplateCache bool `required:"true" default:"true" desc:"Cache templates after first use?"`
|
||||
PublicDir string `required:"true" default:"themes/bootstrap/public" desc:"Theme public dir"`
|
||||
GreetingFile string `required:"true" default:"themes/greeting.html" desc:"Home page greeting HTML"`
|
||||
MailboxPrompt string `required:"true" default:"@inbucket" desc:"Prompt next to mailbox input"`
|
||||
CookieAuthKey string `desc:"Session cipher key (text)"`
|
||||
MonitorVisible bool `required:"true" default:"true" desc:"Show monitor tab in UI?"`
|
||||
MonitorHistory int `required:"true" default:"30" desc:"Monitor remembered messages"`
|
||||
}
|
||||
|
||||
// GetLogLevel returns the configured log level
|
||||
func GetLogLevel() string {
|
||||
return logLevel
|
||||
// Storage contains the mail store configuration.
|
||||
type Storage struct {
|
||||
Path string `required:"true" default:"/tmp/inbucket" desc:"Mail store path"`
|
||||
RetentionPeriod time.Duration `required:"true" default:"24h" desc:"Duration to retain messages"`
|
||||
RetentionSleep time.Duration `required:"true" default:"100ms" desc:"Duration to sleep between deletes"`
|
||||
MailboxMsgCap int `required:"true" default:"500" desc:"Maximum messages per mailbox"`
|
||||
}
|
||||
|
||||
// LoadConfig loads the specified configuration file into inbucket.Config and performs validations
|
||||
// on it.
|
||||
func LoadConfig(filename string) error {
|
||||
var err error
|
||||
Config, err = config.ReadDefault(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Validation error messages
|
||||
messages := make([]string, 0)
|
||||
// Validate sections
|
||||
for _, s := range []string{"logging", "smtp", "pop3", "web", "datastore"} {
|
||||
if !Config.HasSection(s) {
|
||||
messages = append(messages,
|
||||
fmt.Sprintf("Config section [%v] is required", s))
|
||||
}
|
||||
}
|
||||
// Return immediately if config is missing entire sections
|
||||
if len(messages) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
|
||||
for _, m := range messages {
|
||||
fmt.Fprintln(os.Stderr, " -", m)
|
||||
}
|
||||
return fmt.Errorf("Failed to validate configuration")
|
||||
}
|
||||
// Load string config options
|
||||
stringOptions := []struct {
|
||||
section string
|
||||
name string
|
||||
target *string
|
||||
required bool
|
||||
}{
|
||||
{"logging", "level", &logLevel, true},
|
||||
{"smtp", "domain", &smtpConfig.Domain, true},
|
||||
{"smtp", "domain.nostore", &smtpConfig.DomainNoStore, false},
|
||||
{"pop3", "domain", &pop3Config.Domain, true},
|
||||
{"web", "template.dir", &webConfig.TemplateDir, true},
|
||||
{"web", "public.dir", &webConfig.PublicDir, true},
|
||||
{"web", "greeting.file", &webConfig.GreetingFile, true},
|
||||
{"web", "mailbox.prompt", &webConfig.MailboxPrompt, false},
|
||||
{"web", "cookie.auth.key", &webConfig.CookieAuthKey, false},
|
||||
{"datastore", "path", &dataStoreConfig.Path, true},
|
||||
}
|
||||
for _, opt := range stringOptions {
|
||||
str, err := Config.String(opt.section, opt.name)
|
||||
if Config.HasOption(opt.section, opt.name) && err != nil {
|
||||
messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
|
||||
continue
|
||||
}
|
||||
if str == "" && opt.required {
|
||||
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
|
||||
}
|
||||
*opt.target = str
|
||||
}
|
||||
// Load boolean config options
|
||||
boolOptions := []struct {
|
||||
section string
|
||||
name string
|
||||
target *bool
|
||||
required bool
|
||||
}{
|
||||
{"smtp", "store.messages", &smtpConfig.StoreMessages, true},
|
||||
{"web", "template.cache", &webConfig.TemplateCache, true},
|
||||
{"web", "monitor.visible", &webConfig.MonitorVisible, true},
|
||||
}
|
||||
for _, opt := range boolOptions {
|
||||
if Config.HasOption(opt.section, opt.name) {
|
||||
flag, err := Config.Bool(opt.section, opt.name)
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
|
||||
}
|
||||
*opt.target = flag
|
||||
} else {
|
||||
if opt.required {
|
||||
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load integer config options
|
||||
intOptions := []struct {
|
||||
section string
|
||||
name string
|
||||
target *int
|
||||
required bool
|
||||
}{
|
||||
{"smtp", "ip4.port", &smtpConfig.IP4port, true},
|
||||
{"smtp", "max.recipients", &smtpConfig.MaxRecipients, true},
|
||||
{"smtp", "max.idle.seconds", &smtpConfig.MaxIdleSeconds, true},
|
||||
{"smtp", "max.message.bytes", &smtpConfig.MaxMessageBytes, true},
|
||||
{"pop3", "ip4.port", &pop3Config.IP4port, true},
|
||||
{"pop3", "max.idle.seconds", &pop3Config.MaxIdleSeconds, true},
|
||||
{"web", "ip4.port", &webConfig.IP4port, true},
|
||||
{"web", "monitor.history", &webConfig.MonitorHistory, true},
|
||||
{"datastore", "retention.minutes", &dataStoreConfig.RetentionMinutes, true},
|
||||
{"datastore", "retention.sleep.millis", &dataStoreConfig.RetentionSleep, true},
|
||||
{"datastore", "mailbox.message.cap", &dataStoreConfig.MailboxMsgCap, true},
|
||||
}
|
||||
for _, opt := range intOptions {
|
||||
if Config.HasOption(opt.section, opt.name) {
|
||||
num, err := Config.Int(opt.section, opt.name)
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
|
||||
}
|
||||
*opt.target = num
|
||||
} else {
|
||||
if opt.required {
|
||||
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load IP address config options
|
||||
ipOptions := []struct {
|
||||
section string
|
||||
name string
|
||||
target *net.IP
|
||||
required bool
|
||||
}{
|
||||
{"smtp", "ip4.address", &smtpConfig.IP4address, true},
|
||||
{"pop3", "ip4.address", &pop3Config.IP4address, true},
|
||||
{"web", "ip4.address", &webConfig.IP4address, true},
|
||||
}
|
||||
for _, opt := range ipOptions {
|
||||
if Config.HasOption(opt.section, opt.name) {
|
||||
str, err := Config.String(opt.section, opt.name)
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
|
||||
continue
|
||||
}
|
||||
addr := net.ParseIP(str)
|
||||
if addr == nil {
|
||||
messages = append(messages,
|
||||
fmt.Sprintf("Failed to parse IP [%v]%v: %q", opt.section, opt.name, str))
|
||||
continue
|
||||
}
|
||||
addr = addr.To4()
|
||||
if addr == nil {
|
||||
messages = append(messages,
|
||||
fmt.Sprintf("Failed to parse IP [%v]%v: %q not IPv4!",
|
||||
opt.section, opt.name, str))
|
||||
}
|
||||
*opt.target = addr
|
||||
} else {
|
||||
if opt.required {
|
||||
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate log level
|
||||
switch strings.ToUpper(logLevel) {
|
||||
case "":
|
||||
// Missing was already reported
|
||||
case "TRACE", "INFO", "WARN", "ERROR":
|
||||
default:
|
||||
messages = append(messages,
|
||||
fmt.Sprintf("Invalid value provided for [logging]level: %q", logLevel))
|
||||
}
|
||||
// Print messages and return error if any validations failed
|
||||
if len(messages) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
|
||||
sort.Strings(messages)
|
||||
for _, m := range messages {
|
||||
fmt.Fprintln(os.Stderr, " -", m)
|
||||
}
|
||||
return fmt.Errorf("Failed to validate configuration")
|
||||
}
|
||||
return nil
|
||||
// Process loads and parses configuration from the environment.
|
||||
func Process() (*Root, error) {
|
||||
c := &Root{}
|
||||
err := envconfig.Process(prefix, c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Usage prints out the envconfig usage to Stderr.
|
||||
func Usage() {
|
||||
tabs := tabwriter.NewWriter(os.Stderr, 1, 0, 4, ' ', 0)
|
||||
if err := envconfig.Usagef(prefix, &Root{}, tabs, tableFormat); err != nil {
|
||||
log.Fatalf("Unable to parse env config: %v", err)
|
||||
}
|
||||
tabs.Flush()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user