1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 09:37:02 +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:
James Hillyerd
2018-03-21 20:44:47 -07:00
parent be940dd2bc
commit 845cbedc0d
20 changed files with 190 additions and 399 deletions

View File

@@ -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] <conf file>:")
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)
}

View File

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

View File

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

View File

@@ -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.

View File

@@ -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,
},
}

View File

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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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,

View File

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

View File

@@ -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()

View File

@@ -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

View File

@@ -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")

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,7 @@ $(document).ready(
<div class="row">
<div class="col-sm-3 col-xs-7"><b>Message Cap:</b></div>
<div class="col-sm-8 col-xs-5">
{{with .dataStoreConfig}}
{{with .storageConfig}}
<span>{{.MailboxMsgCap}} messages per mailbox</span>
{{end}}
</div>
@@ -167,14 +167,14 @@ $(document).ready(
<div class="panel-heading">
<h3 class="panel-title">
<span class="glyphicon glyphicon-hdd" aria-hidden="true"></span>
Data Store Metrics</h3>
Storage Metrics</h3>
</div>
<div class="panel-body">
<table class="metrics">
<div class="row">
<div class="col-sm-3 col-xs-7"><b>Retention Period:</b></div>
<div class="col-sm-8 col-xs-5">
{{if .dataStoreConfig.RetentionMinutes}}
{{if .storageConfig.RetentionPeriod}}
<span id="m-retentionPeriod">.</span>
{{else}}
Disabled
@@ -184,7 +184,7 @@ $(document).ready(
<div class="row">
<div class="col-sm-3 col-xs-7"><b>Retention Scan:</b></div>
<div class="col-sm-8 col-xs-5">
{{if .dataStoreConfig.RetentionMinutes}}
{{if .storageConfig.RetentionPeriod}}
Completed <span id="m-retentionScanCompleted">.</span> ago
{{else}}
Disabled