From 845cbedc0dc52e63310618754afedd40e528f00a Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Wed, 21 Mar 2018 20:44:47 -0700 Subject: [PATCH] config: Replace robfig with envconfig for #86 - Initial envconfig system is working, not bulletproof. - Added sane defaults for required parameters. --- cmd/inbucket/main.go | 111 +++---- pkg/config/config.go | 304 ++++---------------- pkg/log/logging.go | 4 +- pkg/policy/address.go | 2 +- pkg/policy/address_test.go | 4 +- pkg/rest/testutils_test.go | 8 +- pkg/server/pop3/handler.go | 2 +- pkg/server/pop3/listener.go | 9 +- pkg/server/smtp/handler.go | 4 +- pkg/server/smtp/handler_test.go | 7 +- pkg/server/smtp/listener.go | 9 +- pkg/server/web/context.go | 27 +- pkg/server/web/server.go | 24 +- pkg/server/web/template.go | 6 +- pkg/storage/file/fstore.go | 2 +- pkg/storage/file/fstore_test.go | 14 +- pkg/storage/retention.go | 8 +- pkg/storage/retention_test.go | 6 +- pkg/webui/root_controller.go | 30 +- themes/bootstrap/templates/root/status.html | 8 +- 20 files changed, 190 insertions(+), 399 deletions(-) diff --git a/cmd/inbucket/main.go b/cmd/inbucket/main.go index 0de0b07..0183998 100644 --- a/cmd/inbucket/main.go +++ b/cmd/inbucket/main.go @@ -27,83 +27,63 @@ import ( ) var ( - // version contains the build version number, populated during linking + // version contains the build version number, populated during linking. version = "undefined" - // date contains the build date, populated during linking + // date contains the build date, populated during linking. date = "undefined" - - // Command line flags - help = flag.Bool("help", false, "Displays this help") - pidfile = flag.String("pidfile", "none", "Write our PID into the specified file") - logfile = flag.String("logfile", "stderr", "Write out log into the specified file") - - // shutdownChan - close it to tell Inbucket to shut down cleanly - shutdownChan = make(chan bool) - - // Server instances - smtpServer *smtp.Server - pop3Server *pop3.Server ) func init() { - flag.Usage = func() { - fmt.Fprintln(os.Stderr, "Usage of inbucket [options] :") - flag.PrintDefaults() - } - - // Server uptime for status page + // Server uptime for status page. startTime := time.Now() expvar.Publish("uptime", expvar.Func(func() interface{} { return time.Since(startTime) / time.Second })) - // Goroutine count for status page + // Goroutine count for status page. expvar.Publish("goroutines", expvar.Func(func() interface{} { return runtime.NumGoroutine() })) } func main() { - config.Version = version - config.BuildDate = date - + // Command line flags. + help := flag.Bool("help", false, "Displays this help") + pidfile := flag.String("pidfile", "", "Write our PID into the specified file") + logfile := flag.String("logfile", "stderr", "Write out log into the specified file") + flag.Usage = func() { + fmt.Fprintln(os.Stderr, "Usage: inbucket [options]") + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, "") + config.Usage() + } flag.Parse() if *help { flag.Usage() return } - - // Root context - rootCtx, rootCancel := context.WithCancel(context.Background()) - - // Load & Parse config - if flag.NArg() != 1 { - flag.Usage() - os.Exit(1) - } - err := config.LoadConfig(flag.Arg(0)) + // Process configuration. + config.Version = version + config.BuildDate = date + conf, err := config.Process() if err != nil { - fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err) + fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err) os.Exit(1) } - - // Setup signal handler + // Setup signal handler. sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT) - - // Initialize logging - log.SetLogLevel(config.GetLogLevel()) + // Initialize logging. + log.SetLogLevel(conf.LogLevel) if err := log.Initialize(*logfile); err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } defer log.Close() - log.Infof("Inbucket %v (%v) starting...", config.Version, config.BuildDate) - - // Write pidfile if requested - if *pidfile != "none" { + // Write pidfile if requested. + if *pidfile != "" { pidf, err := os.Create(*pidfile) if err != nil { log.Errorf("Failed to create %q: %v", *pidfile, err) @@ -114,26 +94,26 @@ func main() { log.Errorf("Failed to close PID file %q: %v", *pidfile, err) } } - // Configure internal services. - msgHub := msghub.New(rootCtx, config.GetWebConfig().MonitorHistory) - dscfg := config.GetDataStoreConfig() - store := file.New(dscfg) - apolicy := &policy.Addressing{Config: config.GetSMTPConfig()} + rootCtx, rootCancel := context.WithCancel(context.Background()) + shutdownChan := make(chan bool) + msgHub := msghub.New(rootCtx, conf.Web.MonitorHistory) + store := file.New(conf.Storage) + addrPolicy := &policy.Addressing{Config: conf.SMTP} mmanager := &message.StoreManager{Store: store, Hub: msgHub} // Start Retention scanner. - retentionScanner := storage.NewRetentionScanner(dscfg, store, shutdownChan) + retentionScanner := storage.NewRetentionScanner(conf.Storage, store, shutdownChan) retentionScanner.Start() // Start HTTP server. - web.Initialize(config.GetWebConfig(), shutdownChan, mmanager, msgHub) + web.Initialize(conf, shutdownChan, mmanager, msgHub) webui.SetupRoutes(web.Router) rest.SetupRoutes(web.Router) go web.Start(rootCtx) // Start POP3 server. - pop3Server = pop3.New(config.GetPOP3Config(), shutdownChan, store) + pop3Server := pop3.New(conf.POP3, shutdownChan, store) go pop3Server.Start(rootCtx) // Start SMTP server. - smtpServer = smtp.NewServer(config.GetSMTPConfig(), shutdownChan, mmanager, apolicy) + smtpServer := smtp.NewServer(conf.SMTP, shutdownChan, mmanager, addrPolicy) go smtpServer.Start(rootCtx) // Loop forever waiting for signals or shutdown channel. signalLoop: @@ -158,30 +138,27 @@ signalLoop: break signalLoop } } - - // Wait for active connections to finish - go timedExit() + // Wait for active connections to finish. + go timedExit(*pidfile) smtpServer.Drain() pop3Server.Drain() retentionScanner.Join() - - removePIDFile() + removePIDFile(*pidfile) } -// removePIDFile removes the PID file if created -func removePIDFile() { - if *pidfile != "none" { - if err := os.Remove(*pidfile); err != nil { - log.Errorf("Failed to remove %q: %v", *pidfile, err) +// removePIDFile removes the PID file if created. +func removePIDFile(pidfile string) { + if pidfile != "" { + if err := os.Remove(pidfile); err != nil { + log.Errorf("Failed to remove %q: %v", pidfile, err) } } } -// timedExit is called as a goroutine during shutdown, it will force an exit -// after 15 seconds -func timedExit() { +// timedExit is called as a goroutine during shutdown, it will force an exit after 15 seconds. +func timedExit(pidfile string) { time.Sleep(15 * time.Second) log.Errorf("Clean shutdown took too long, forcing exit") - removePIDFile() + removePIDFile(pidfile) os.Exit(0) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 01e909e..11ef51e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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() } diff --git a/pkg/log/logging.go b/pkg/log/logging.go index 7c06a1e..c2184ff 100644 --- a/pkg/log/logging.go +++ b/pkg/log/logging.go @@ -72,7 +72,7 @@ func SetLogLevel(level string) (ok bool) { case "TRACE": MaxLevel = TRACE default: - Errorf("Unknown log level requested: " + level) + golog.Print("Error, unknown log level requested: " + level) return false } return true @@ -150,7 +150,6 @@ func openLogFile() error { return fmt.Errorf("failed to create %v: %v", logfname, err) } golog.SetOutput(logf) - Tracef("Opened new logfile") // Platform specific reassignStdout() return nil @@ -158,7 +157,6 @@ func openLogFile() error { // closeLogFile closes the current logfile func closeLogFile() { - Tracef("Closing logfile") // We are never in a situation where we can do anything about failing to close _ = logf.Close() } diff --git a/pkg/policy/address.go b/pkg/policy/address.go index 34ed8f8..680a789 100644 --- a/pkg/policy/address.go +++ b/pkg/policy/address.go @@ -11,7 +11,7 @@ import ( // Addressing handles email address policy. type Addressing struct { - Config config.SMTPConfig + Config config.SMTP } // NewRecipient parses an address into a Recipient. diff --git a/pkg/policy/address_test.go b/pkg/policy/address_test.go index 2ab2c60..009033c 100644 --- a/pkg/policy/address_test.go +++ b/pkg/policy/address_test.go @@ -11,7 +11,7 @@ import ( func TestShouldStoreDomain(t *testing.T) { // Test with storage enabled. ap := &policy.Addressing{ - Config: config.SMTPConfig{ + Config: config.SMTP{ DomainNoStore: "Foo.Com", StoreMessages: true, }, @@ -36,7 +36,7 @@ func TestShouldStoreDomain(t *testing.T) { } // Test with storage disabled. ap = &policy.Addressing{ - Config: config.SMTPConfig{ + Config: config.SMTP{ StoreMessages: false, }, } diff --git a/pkg/rest/testutils_test.go b/pkg/rest/testutils_test.go index 587c3b7..e2f2e3d 100644 --- a/pkg/rest/testutils_test.go +++ b/pkg/rest/testutils_test.go @@ -34,9 +34,11 @@ func setupWebServer(mm message.Manager) *bytes.Buffer { // Have to reset default mux to prevent duplicate routes http.DefaultServeMux = http.NewServeMux() - cfg := config.WebConfig{ - TemplateDir: "../themes/bootstrap/templates", - PublicDir: "../themes/bootstrap/public", + cfg := &config.Root{ + Web: config.Web{ + TemplateDir: "../themes/bootstrap/templates", + PublicDir: "../themes/bootstrap/public", + }, } shutdownChan := make(chan bool) web.Initialize(cfg, shutdownChan, mm, &msghub.Hub{}) diff --git a/pkg/server/pop3/handler.go b/pkg/server/pop3/handler.go index f8229ca..00334fa 100644 --- a/pkg/server/pop3/handler.go +++ b/pkg/server/pop3/handler.go @@ -536,7 +536,7 @@ func (ses *Session) enterState(state State) { // Calculate the next read or write deadline based on maxIdleSeconds func (ses *Session) nextDeadline() time.Time { - return time.Now().Add(time.Duration(ses.server.maxIdleSeconds) * time.Second) + return time.Now().Add(ses.server.maxIdle) } // Send requested message, store errors in Session.sendError diff --git a/pkg/server/pop3/listener.go b/pkg/server/pop3/listener.go index c971854..ee6d2da 100644 --- a/pkg/server/pop3/listener.go +++ b/pkg/server/pop3/listener.go @@ -2,7 +2,6 @@ package pop3 import ( "context" - "fmt" "net" "sync" "time" @@ -16,7 +15,7 @@ import ( type Server struct { host string domain string - maxIdleSeconds int + maxIdle time.Duration dataStore storage.Store listener net.Listener globalShutdown chan bool @@ -24,12 +23,12 @@ type Server struct { } // New creates a new Server struct -func New(cfg config.POP3Config, shutdownChan chan bool, ds storage.Store) *Server { +func New(cfg config.POP3, shutdownChan chan bool, ds storage.Store) *Server { return &Server{ - host: fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port), + host: cfg.Addr, domain: cfg.Domain, dataStore: ds, - maxIdleSeconds: cfg.MaxIdleSeconds, + maxIdle: cfg.MaxIdle, globalShutdown: shutdownChan, waitgroup: new(sync.WaitGroup), } diff --git a/pkg/server/smtp/handler.go b/pkg/server/smtp/handler.go index 4d6ee33..12338b2 100644 --- a/pkg/server/smtp/handler.go +++ b/pkg/server/smtp/handler.go @@ -412,9 +412,9 @@ func (ss *Session) greet() { ss.send(fmt.Sprintf("220 %v Inbucket SMTP ready", ss.server.domain)) } -// Calculate the next read or write deadline based on maxIdleSeconds +// Calculate the next read or write deadline based on maxIdle func (ss *Session) nextDeadline() time.Time { - return time.Now().Add(time.Duration(ss.server.maxIdleSeconds) * time.Second) + return time.Now().Add(ss.server.maxIdle) } // Send requested message, store errors in Session.sendError diff --git a/pkg/server/smtp/handler_test.go b/pkg/server/smtp/handler_test.go index 283c6d1..e047230 100644 --- a/pkg/server/smtp/handler_test.go +++ b/pkg/server/smtp/handler_test.go @@ -361,13 +361,12 @@ func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil } func setupSMTPServer(ds storage.Store) (s *Server, buf *bytes.Buffer, teardown func()) { // Test Server Config - cfg := config.SMTPConfig{ - IP4address: net.IPv4(127, 0, 0, 1), - IP4port: 2500, + cfg := config.SMTP{ + Addr: "127.0.0.1:2500", Domain: "inbucket.local", DomainNoStore: "bitbucket.local", MaxRecipients: 5, - MaxIdleSeconds: 5, + MaxIdle: 5, MaxMessageBytes: 5000, StoreMessages: true, } diff --git a/pkg/server/smtp/listener.go b/pkg/server/smtp/listener.go index 959b6f5..e56ac93 100644 --- a/pkg/server/smtp/listener.go +++ b/pkg/server/smtp/listener.go @@ -4,7 +4,6 @@ import ( "container/list" "context" "expvar" - "fmt" "net" "strings" "sync" @@ -43,7 +42,7 @@ type Server struct { domain string domainNoStore string maxRecips int - maxIdleSeconds int + maxIdle time.Duration maxMessageBytes int storeMessages bool @@ -80,17 +79,17 @@ var ( // NewServer creates a new Server instance with the specificed config func NewServer( - cfg config.SMTPConfig, + cfg config.SMTP, globalShutdown chan bool, manager message.Manager, apolicy *policy.Addressing, ) *Server { return &Server{ - host: fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port), + host: cfg.Addr, domain: cfg.Domain, domainNoStore: strings.ToLower(cfg.DomainNoStore), maxRecips: cfg.MaxRecipients, - maxIdleSeconds: cfg.MaxIdleSeconds, + maxIdle: cfg.MaxIdle, maxMessageBytes: cfg.MaxMessageBytes, storeMessages: cfg.StoreMessages, globalShutdown: globalShutdown, diff --git a/pkg/server/web/context.go b/pkg/server/web/context.go index 9ec2ab8..65e6b3d 100644 --- a/pkg/server/web/context.go +++ b/pkg/server/web/context.go @@ -12,13 +12,15 @@ import ( ) // Context is passed into every request handler function +// TODO remove redundant web config type Context struct { - Vars map[string]string - Session *sessions.Session - MsgHub *msghub.Hub - Manager message.Manager - WebConfig config.WebConfig - IsJSON bool + Vars map[string]string + Session *sessions.Session + MsgHub *msghub.Hub + Manager message.Manager + RootConfig *config.Root + WebConfig config.Web + IsJSON bool } // Close the Context (currently does nothing) @@ -57,12 +59,13 @@ func NewContext(req *http.Request) (*Context, error) { err = nil } ctx := &Context{ - Vars: vars, - Session: sess, - MsgHub: msgHub, - Manager: manager, - WebConfig: webConfig, - IsJSON: headerMatch(req, "Accept", "application/json"), + Vars: vars, + Session: sess, + MsgHub: msgHub, + Manager: manager, + RootConfig: rootConfig, + WebConfig: rootConfig.Web, + IsJSON: headerMatch(req, "Accept", "application/json"), } return ctx, err } diff --git a/pkg/server/web/server.go b/pkg/server/web/server.go index b3e8610..96716f0 100644 --- a/pkg/server/web/server.go +++ b/pkg/server/web/server.go @@ -4,7 +4,6 @@ package web import ( "context" "expvar" - "fmt" "net" "net/http" "time" @@ -30,7 +29,7 @@ var ( // incoming requests to the correct handler function Router = mux.NewRouter() - webConfig config.WebConfig + rootConfig *config.Root server *http.Server listener net.Listener sessionStore sessions.Store @@ -47,12 +46,12 @@ func init() { // Initialize sets up things for unit tests or the Start() method func Initialize( - cfg config.WebConfig, + conf *config.Root, shutdownChan chan bool, mm message.Manager, mh *msghub.Hub) { - webConfig = cfg + rootConfig = conf globalShutdown = shutdownChan // NewContext() will use this DataStore for the web handlers @@ -60,36 +59,35 @@ func Initialize( manager = mm // Content Paths - log.Infof("HTTP templates mapped to %q", cfg.TemplateDir) - log.Infof("HTTP static content mapped to %q", cfg.PublicDir) + log.Infof("HTTP templates mapped to %q", conf.Web.TemplateDir) + log.Infof("HTTP static content mapped to %q", conf.Web.PublicDir) Router.PathPrefix("/public/").Handler(http.StripPrefix("/public/", - http.FileServer(http.Dir(cfg.PublicDir)))) + http.FileServer(http.Dir(conf.Web.PublicDir)))) http.Handle("/", Router) // Session cookie setup - if cfg.CookieAuthKey == "" { + if conf.Web.CookieAuthKey == "" { log.Infof("HTTP generating random cookie.auth.key") sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64)) } else { log.Tracef("HTTP using configured cookie.auth.key") - sessionStore = sessions.NewCookieStore([]byte(cfg.CookieAuthKey)) + sessionStore = sessions.NewCookieStore([]byte(conf.Web.CookieAuthKey)) } } // Start begins listening for HTTP requests func Start(ctx context.Context) { - addr := fmt.Sprintf("%v:%v", webConfig.IP4address, webConfig.IP4port) server = &http.Server{ - Addr: addr, + Addr: rootConfig.Web.Addr, Handler: nil, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, } // We don't use ListenAndServe because it lacks a way to close the listener - log.Infof("HTTP listening on TCP4 %v", addr) + log.Infof("HTTP listening on TCP4 %v", server.Addr) var err error - listener, err = net.Listen("tcp", addr) + listener, err = net.Listen("tcp", server.Addr) if err != nil { log.Errorf("HTTP failed to start TCP4 listener: %v", err) emergencyShutdown() diff --git a/pkg/server/web/template.go b/pkg/server/web/template.go index 216a1a4..2597ada 100644 --- a/pkg/server/web/template.go +++ b/pkg/server/web/template.go @@ -50,7 +50,7 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) { } tempPath := strings.Replace(name, "/", string(filepath.Separator), -1) - tempFile := filepath.Join(webConfig.TemplateDir, tempPath) + tempFile := filepath.Join(rootConfig.Web.TemplateDir, tempPath) log.Tracef("Parsing template %v", tempFile) var err error @@ -62,14 +62,14 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) { t, err = t.ParseFiles(tempFile) } else { t = template.New("_base.html").Funcs(TemplateFuncs) - t, err = t.ParseFiles(filepath.Join(webConfig.TemplateDir, "_base.html"), tempFile) + t, err = t.ParseFiles(filepath.Join(rootConfig.Web.TemplateDir, "_base.html"), tempFile) } if err != nil { return nil, err } // Allows us to disable caching for theme development - if webConfig.TemplateCache { + if rootConfig.Web.TemplateCache { if partial { log.Tracef("Caching partial %v", name) cachedTemplates[name] = t diff --git a/pkg/storage/file/fstore.go b/pkg/storage/file/fstore.go index 57a26bf..069a3fe 100644 --- a/pkg/storage/file/fstore.go +++ b/pkg/storage/file/fstore.go @@ -48,7 +48,7 @@ type Store struct { } // New creates a new DataStore object using the specified path -func New(cfg config.DataStoreConfig) storage.Store { +func New(cfg config.Storage) storage.Store { path := cfg.Path if path == "" { log.Errorf("No value configured for datastore path") diff --git a/pkg/storage/file/fstore_test.go b/pkg/storage/file/fstore_test.go index 589bf18..0b42e86 100644 --- a/pkg/storage/file/fstore_test.go +++ b/pkg/storage/file/fstore_test.go @@ -23,7 +23,7 @@ import ( // TestSuite runs storage package test suite on file store. func TestSuite(t *testing.T) { test.StoreSuite(t, func() (storage.Store, func(), error) { - ds, _ := setupDataStore(config.DataStoreConfig{}) + ds, _ := setupDataStore(config.Storage{}) destroy := func() { teardownDataStore(ds) } @@ -33,7 +33,7 @@ func TestSuite(t *testing.T) { // Test directory structure created by filestore func TestFSDirStructure(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) + ds, logbuf := setupDataStore(config.Storage{}) defer teardownDataStore(ds) root := ds.path @@ -111,7 +111,7 @@ func TestFSDirStructure(t *testing.T) { // Test missing files func TestFSMissing(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) + ds, logbuf := setupDataStore(config.Storage{}) defer teardownDataStore(ds) mbName := "fred" @@ -147,7 +147,7 @@ func TestFSMissing(t *testing.T) { // Test delivering several messages to the same mailbox, see if message cap works func TestFSMessageCap(t *testing.T) { mbCap := 10 - ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap}) + ds, logbuf := setupDataStore(config.Storage{MailboxMsgCap: mbCap}) defer teardownDataStore(ds) mbName := "captain" @@ -188,7 +188,7 @@ func TestFSMessageCap(t *testing.T) { // Test delivering several messages to the same mailbox, see if no message cap works func TestFSNoMessageCap(t *testing.T) { mbCap := 0 - ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap}) + ds, logbuf := setupDataStore(config.Storage{MailboxMsgCap: mbCap}) defer teardownDataStore(ds) mbName := "captain" @@ -218,7 +218,7 @@ func TestFSNoMessageCap(t *testing.T) { // Test Get the latest message func TestGetLatestMessage(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) + ds, logbuf := setupDataStore(config.Storage{}) defer teardownDataStore(ds) // james hashes to 474ba67bdb289c6263b36dfd8a7bed6c85b04943 @@ -260,7 +260,7 @@ func TestGetLatestMessage(t *testing.T) { } // setupDataStore creates a new FileDataStore in a temporary directory -func setupDataStore(cfg config.DataStoreConfig) (*Store, *bytes.Buffer) { +func setupDataStore(cfg config.Storage) (*Store, *bytes.Buffer) { path, err := ioutil.TempDir("", "inbucket") if err != nil { panic(err) diff --git a/pkg/storage/retention.go b/pkg/storage/retention.go index 6c7adb0..d083152 100644 --- a/pkg/storage/retention.go +++ b/pkg/storage/retention.go @@ -54,7 +54,7 @@ type RetentionScanner struct { // NewRetentionScanner configures a new RententionScanner. func NewRetentionScanner( - cfg config.DataStoreConfig, + cfg config.Storage, ds Store, shutdownChannel chan bool, ) *RetentionScanner { @@ -62,11 +62,11 @@ func NewRetentionScanner( globalShutdown: shutdownChannel, retentionShutdown: make(chan bool), ds: ds, - retentionPeriod: time.Duration(cfg.RetentionMinutes) * time.Minute, - retentionSleep: time.Duration(cfg.RetentionSleep) * time.Millisecond, + retentionPeriod: cfg.RetentionPeriod, + retentionSleep: cfg.RetentionSleep, } // expRetentionPeriod is displayed on the status page - expRetentionPeriod.Set(int64(cfg.RetentionMinutes * 60)) + expRetentionPeriod.Set(int64(cfg.RetentionPeriod / time.Second)) return rs } diff --git a/pkg/storage/retention_test.go b/pkg/storage/retention_test.go index 234a377..e918cc1 100644 --- a/pkg/storage/retention_test.go +++ b/pkg/storage/retention_test.go @@ -27,9 +27,9 @@ func TestDoRetentionScan(t *testing.T) { ds.AddMessage(new2) ds.AddMessage(new3) // Test 4 hour retention - cfg := config.DataStoreConfig{ - RetentionMinutes: 239, - RetentionSleep: 0, + cfg := config.Storage{ + RetentionPeriod: 239 * time.Minute, + RetentionSleep: 0, } shutdownChan := make(chan bool) rs := storage.NewRetentionScanner(cfg, ds, shutdownChan) diff --git a/pkg/webui/root_controller.go b/pkg/webui/root_controller.go index 0ea5780..352d51b 100644 --- a/pkg/webui/root_controller.go +++ b/pkg/webui/root_controller.go @@ -12,7 +12,7 @@ import ( // RootIndex serves the Inbucket landing page func RootIndex(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { - greeting, err := ioutil.ReadFile(config.GetWebConfig().GreetingFile) + greeting, err := ioutil.ReadFile(ctx.RootConfig.Web.GreetingFile) if err != nil { return fmt.Errorf("Failed to load greeting: %v", err) } @@ -31,7 +31,7 @@ func RootIndex(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err // RootMonitor serves the Inbucket monitor page func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { - if !config.GetWebConfig().MonitorVisible { + if !ctx.RootConfig.Web.MonitorVisible { ctx.Session.AddFlash("Monitor is disabled in configuration", "errors") _ = ctx.Session.Save(req, w) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) @@ -51,7 +51,7 @@ func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *web.Context) (er // RootMonitorMailbox serves the Inbucket monitor page for a particular mailbox func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { - if !config.GetWebConfig().MonitorVisible { + if !ctx.RootConfig.Web.MonitorVisible { ctx.Session.AddFlash("Monitor is disabled in configuration", "errors") _ = ctx.Session.Save(req, w) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) @@ -79,12 +79,6 @@ func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Conte // RootStatus serves the Inbucket status page func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { - smtpListener := fmt.Sprintf("%s:%d", config.GetSMTPConfig().IP4address.String(), - config.GetSMTPConfig().IP4port) - pop3Listener := fmt.Sprintf("%s:%d", config.GetPOP3Config().IP4address.String(), - config.GetPOP3Config().IP4port) - webListener := fmt.Sprintf("%s:%d", config.GetWebConfig().IP4address.String(), - config.GetWebConfig().IP4port) // Get flash messages, save session errorFlash := ctx.Session.Flashes("errors") if err = ctx.Session.Save(req, w); err != nil { @@ -92,14 +86,14 @@ func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err } // Render template return web.RenderTemplate("root/status.html", w, map[string]interface{}{ - "ctx": ctx, - "errorFlash": errorFlash, - "version": config.Version, - "buildDate": config.BuildDate, - "smtpListener": smtpListener, - "pop3Listener": pop3Listener, - "webListener": webListener, - "smtpConfig": config.GetSMTPConfig(), - "dataStoreConfig": config.GetDataStoreConfig(), + "ctx": ctx, + "errorFlash": errorFlash, + "version": config.Version, + "buildDate": config.BuildDate, + "smtpListener": ctx.RootConfig.SMTP.Addr, + "pop3Listener": ctx.RootConfig.POP3.Addr, + "webListener": ctx.RootConfig.Web.Addr, + "smtpConfig": ctx.RootConfig.SMTP, + "storageConfig": ctx.RootConfig.Storage, }) } diff --git a/themes/bootstrap/templates/root/status.html b/themes/bootstrap/templates/root/status.html index 0613e14..31fbc35 100644 --- a/themes/bootstrap/templates/root/status.html +++ b/themes/bootstrap/templates/root/status.html @@ -62,7 +62,7 @@ $(document).ready(
Message Cap:
- {{with .dataStoreConfig}} + {{with .storageConfig}} {{.MailboxMsgCap}} messages per mailbox {{end}}
@@ -167,14 +167,14 @@ $(document).ready(

- Data Store Metrics

+ Storage Metrics
Retention Period:
- {{if .dataStoreConfig.RetentionMinutes}} + {{if .storageConfig.RetentionPeriod}} . {{else}} Disabled @@ -184,7 +184,7 @@ $(document).ready(
Retention Scan:
- {{if .dataStoreConfig.RetentionMinutes}} + {{if .storageConfig.RetentionPeriod}} Completed . ago {{else}} Disabled