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:
@@ -27,83 +27,63 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// version contains the build version number, populated during linking
|
// version contains the build version number, populated during linking.
|
||||||
version = "undefined"
|
version = "undefined"
|
||||||
|
|
||||||
// date contains the build date, populated during linking
|
// date contains the build date, populated during linking.
|
||||||
date = "undefined"
|
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() {
|
func init() {
|
||||||
flag.Usage = func() {
|
// Server uptime for status page.
|
||||||
fmt.Fprintln(os.Stderr, "Usage of inbucket [options] <conf file>:")
|
|
||||||
flag.PrintDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server uptime for status page
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
expvar.Publish("uptime", expvar.Func(func() interface{} {
|
expvar.Publish("uptime", expvar.Func(func() interface{} {
|
||||||
return time.Since(startTime) / time.Second
|
return time.Since(startTime) / time.Second
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Goroutine count for status page
|
// Goroutine count for status page.
|
||||||
expvar.Publish("goroutines", expvar.Func(func() interface{} {
|
expvar.Publish("goroutines", expvar.Func(func() interface{} {
|
||||||
return runtime.NumGoroutine()
|
return runtime.NumGoroutine()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config.Version = version
|
// Command line flags.
|
||||||
config.BuildDate = date
|
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()
|
flag.Parse()
|
||||||
if *help {
|
if *help {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Process configuration.
|
||||||
// Root context
|
config.Version = version
|
||||||
rootCtx, rootCancel := context.WithCancel(context.Background())
|
config.BuildDate = date
|
||||||
|
conf, err := config.Process()
|
||||||
// Load & Parse config
|
|
||||||
if flag.NArg() != 1 {
|
|
||||||
flag.Usage()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
err := config.LoadConfig(flag.Arg(0))
|
|
||||||
if err != nil {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
// Setup signal handler.
|
||||||
// Setup signal handler
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
|
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
// Initialize logging.
|
||||||
// Initialize logging
|
log.SetLogLevel(conf.LogLevel)
|
||||||
log.SetLogLevel(config.GetLogLevel())
|
|
||||||
if err := log.Initialize(*logfile); err != nil {
|
if err := log.Initialize(*logfile); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%v", err)
|
fmt.Fprintf(os.Stderr, "%v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer log.Close()
|
defer log.Close()
|
||||||
|
|
||||||
log.Infof("Inbucket %v (%v) starting...", config.Version, config.BuildDate)
|
log.Infof("Inbucket %v (%v) starting...", config.Version, config.BuildDate)
|
||||||
|
// Write pidfile if requested.
|
||||||
// Write pidfile if requested
|
if *pidfile != "" {
|
||||||
if *pidfile != "none" {
|
|
||||||
pidf, err := os.Create(*pidfile)
|
pidf, err := os.Create(*pidfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to create %q: %v", *pidfile, err)
|
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)
|
log.Errorf("Failed to close PID file %q: %v", *pidfile, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure internal services.
|
// Configure internal services.
|
||||||
msgHub := msghub.New(rootCtx, config.GetWebConfig().MonitorHistory)
|
rootCtx, rootCancel := context.WithCancel(context.Background())
|
||||||
dscfg := config.GetDataStoreConfig()
|
shutdownChan := make(chan bool)
|
||||||
store := file.New(dscfg)
|
msgHub := msghub.New(rootCtx, conf.Web.MonitorHistory)
|
||||||
apolicy := &policy.Addressing{Config: config.GetSMTPConfig()}
|
store := file.New(conf.Storage)
|
||||||
|
addrPolicy := &policy.Addressing{Config: conf.SMTP}
|
||||||
mmanager := &message.StoreManager{Store: store, Hub: msgHub}
|
mmanager := &message.StoreManager{Store: store, Hub: msgHub}
|
||||||
// Start Retention scanner.
|
// Start Retention scanner.
|
||||||
retentionScanner := storage.NewRetentionScanner(dscfg, store, shutdownChan)
|
retentionScanner := storage.NewRetentionScanner(conf.Storage, store, shutdownChan)
|
||||||
retentionScanner.Start()
|
retentionScanner.Start()
|
||||||
// Start HTTP server.
|
// Start HTTP server.
|
||||||
web.Initialize(config.GetWebConfig(), shutdownChan, mmanager, msgHub)
|
web.Initialize(conf, shutdownChan, mmanager, msgHub)
|
||||||
webui.SetupRoutes(web.Router)
|
webui.SetupRoutes(web.Router)
|
||||||
rest.SetupRoutes(web.Router)
|
rest.SetupRoutes(web.Router)
|
||||||
go web.Start(rootCtx)
|
go web.Start(rootCtx)
|
||||||
// Start POP3 server.
|
// Start POP3 server.
|
||||||
pop3Server = pop3.New(config.GetPOP3Config(), shutdownChan, store)
|
pop3Server := pop3.New(conf.POP3, shutdownChan, store)
|
||||||
go pop3Server.Start(rootCtx)
|
go pop3Server.Start(rootCtx)
|
||||||
// Start SMTP server.
|
// Start SMTP server.
|
||||||
smtpServer = smtp.NewServer(config.GetSMTPConfig(), shutdownChan, mmanager, apolicy)
|
smtpServer := smtp.NewServer(conf.SMTP, shutdownChan, mmanager, addrPolicy)
|
||||||
go smtpServer.Start(rootCtx)
|
go smtpServer.Start(rootCtx)
|
||||||
// Loop forever waiting for signals or shutdown channel.
|
// Loop forever waiting for signals or shutdown channel.
|
||||||
signalLoop:
|
signalLoop:
|
||||||
@@ -158,30 +138,27 @@ signalLoop:
|
|||||||
break signalLoop
|
break signalLoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Wait for active connections to finish.
|
||||||
// Wait for active connections to finish
|
go timedExit(*pidfile)
|
||||||
go timedExit()
|
|
||||||
smtpServer.Drain()
|
smtpServer.Drain()
|
||||||
pop3Server.Drain()
|
pop3Server.Drain()
|
||||||
retentionScanner.Join()
|
retentionScanner.Join()
|
||||||
|
removePIDFile(*pidfile)
|
||||||
removePIDFile()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// removePIDFile removes the PID file if created
|
// removePIDFile removes the PID file if created.
|
||||||
func removePIDFile() {
|
func removePIDFile(pidfile string) {
|
||||||
if *pidfile != "none" {
|
if pidfile != "" {
|
||||||
if err := os.Remove(*pidfile); err != nil {
|
if err := os.Remove(pidfile); err != nil {
|
||||||
log.Errorf("Failed to remove %q: %v", *pidfile, err)
|
log.Errorf("Failed to remove %q: %v", pidfile, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// timedExit is called as a goroutine during shutdown, it will force an exit
|
// timedExit is called as a goroutine during shutdown, it will force an exit after 15 seconds.
|
||||||
// after 15 seconds
|
func timedExit(pidfile string) {
|
||||||
func timedExit() {
|
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
log.Errorf("Clean shutdown took too long, forcing exit")
|
log.Errorf("Clean shutdown took too long, forcing exit")
|
||||||
removePIDFile()
|
removePIDFile(pidfile)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,22 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"log"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"text/tabwriter"
|
||||||
"strings"
|
"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 (
|
const (
|
||||||
missingErrorFmt = "[%v] missing required option %q"
|
prefix = "inbucket"
|
||||||
parseErrorFmt = "[%v] option %q error: %v"
|
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 (
|
var (
|
||||||
@@ -64,207 +25,68 @@ var (
|
|||||||
|
|
||||||
// BuildDate for this build, set by main
|
// BuildDate for this build, set by main
|
||||||
BuildDate = ""
|
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
|
// Root wraps all other configurations.
|
||||||
func GetSMTPConfig() SMTPConfig {
|
type Root struct {
|
||||||
return *smtpConfig
|
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
|
// SMTP contains the SMTP server configuration.
|
||||||
func GetPOP3Config() POP3Config {
|
type SMTP struct {
|
||||||
return *pop3Config
|
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
|
// POP3 contains the POP3 server configuration.
|
||||||
func GetWebConfig() WebConfig {
|
type POP3 struct {
|
||||||
return *webConfig
|
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
|
// Web contains the HTTP server configuration.
|
||||||
func GetDataStoreConfig() DataStoreConfig {
|
type Web struct {
|
||||||
return *dataStoreConfig
|
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
|
// Storage contains the mail store configuration.
|
||||||
func GetLogLevel() string {
|
type Storage struct {
|
||||||
return logLevel
|
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
|
// Process loads and parses configuration from the environment.
|
||||||
// on it.
|
func Process() (*Root, error) {
|
||||||
func LoadConfig(filename string) error {
|
c := &Root{}
|
||||||
var err error
|
err := envconfig.Process(prefix, c)
|
||||||
Config, err = config.ReadDefault(filename)
|
return c, err
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
// Usage prints out the envconfig usage to Stderr.
|
||||||
// Validation error messages
|
func Usage() {
|
||||||
messages := make([]string, 0)
|
tabs := tabwriter.NewWriter(os.Stderr, 1, 0, 4, ' ', 0)
|
||||||
// Validate sections
|
if err := envconfig.Usagef(prefix, &Root{}, tabs, tableFormat); err != nil {
|
||||||
for _, s := range []string{"logging", "smtp", "pop3", "web", "datastore"} {
|
log.Fatalf("Unable to parse env config: %v", err)
|
||||||
if !Config.HasSection(s) {
|
}
|
||||||
messages = append(messages,
|
tabs.Flush()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func SetLogLevel(level string) (ok bool) {
|
|||||||
case "TRACE":
|
case "TRACE":
|
||||||
MaxLevel = TRACE
|
MaxLevel = TRACE
|
||||||
default:
|
default:
|
||||||
Errorf("Unknown log level requested: " + level)
|
golog.Print("Error, unknown log level requested: " + level)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -150,7 +150,6 @@ func openLogFile() error {
|
|||||||
return fmt.Errorf("failed to create %v: %v", logfname, err)
|
return fmt.Errorf("failed to create %v: %v", logfname, err)
|
||||||
}
|
}
|
||||||
golog.SetOutput(logf)
|
golog.SetOutput(logf)
|
||||||
Tracef("Opened new logfile")
|
|
||||||
// Platform specific
|
// Platform specific
|
||||||
reassignStdout()
|
reassignStdout()
|
||||||
return nil
|
return nil
|
||||||
@@ -158,7 +157,6 @@ func openLogFile() error {
|
|||||||
|
|
||||||
// closeLogFile closes the current logfile
|
// closeLogFile closes the current logfile
|
||||||
func closeLogFile() {
|
func closeLogFile() {
|
||||||
Tracef("Closing logfile")
|
|
||||||
// We are never in a situation where we can do anything about failing to close
|
// We are never in a situation where we can do anything about failing to close
|
||||||
_ = logf.Close()
|
_ = logf.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// Addressing handles email address policy.
|
// Addressing handles email address policy.
|
||||||
type Addressing struct {
|
type Addressing struct {
|
||||||
Config config.SMTPConfig
|
Config config.SMTP
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRecipient parses an address into a Recipient.
|
// NewRecipient parses an address into a Recipient.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
func TestShouldStoreDomain(t *testing.T) {
|
func TestShouldStoreDomain(t *testing.T) {
|
||||||
// Test with storage enabled.
|
// Test with storage enabled.
|
||||||
ap := &policy.Addressing{
|
ap := &policy.Addressing{
|
||||||
Config: config.SMTPConfig{
|
Config: config.SMTP{
|
||||||
DomainNoStore: "Foo.Com",
|
DomainNoStore: "Foo.Com",
|
||||||
StoreMessages: true,
|
StoreMessages: true,
|
||||||
},
|
},
|
||||||
@@ -36,7 +36,7 @@ func TestShouldStoreDomain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Test with storage disabled.
|
// Test with storage disabled.
|
||||||
ap = &policy.Addressing{
|
ap = &policy.Addressing{
|
||||||
Config: config.SMTPConfig{
|
Config: config.SMTP{
|
||||||
StoreMessages: false,
|
StoreMessages: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ func setupWebServer(mm message.Manager) *bytes.Buffer {
|
|||||||
|
|
||||||
// Have to reset default mux to prevent duplicate routes
|
// Have to reset default mux to prevent duplicate routes
|
||||||
http.DefaultServeMux = http.NewServeMux()
|
http.DefaultServeMux = http.NewServeMux()
|
||||||
cfg := config.WebConfig{
|
cfg := &config.Root{
|
||||||
|
Web: config.Web{
|
||||||
TemplateDir: "../themes/bootstrap/templates",
|
TemplateDir: "../themes/bootstrap/templates",
|
||||||
PublicDir: "../themes/bootstrap/public",
|
PublicDir: "../themes/bootstrap/public",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
shutdownChan := make(chan bool)
|
shutdownChan := make(chan bool)
|
||||||
web.Initialize(cfg, shutdownChan, mm, &msghub.Hub{})
|
web.Initialize(cfg, shutdownChan, mm, &msghub.Hub{})
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ func (ses *Session) enterState(state State) {
|
|||||||
|
|
||||||
// Calculate the next read or write deadline based on maxIdleSeconds
|
// Calculate the next read or write deadline based on maxIdleSeconds
|
||||||
func (ses *Session) nextDeadline() time.Time {
|
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
|
// Send requested message, store errors in Session.sendError
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package pop3
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,7 +15,7 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
host string
|
host string
|
||||||
domain string
|
domain string
|
||||||
maxIdleSeconds int
|
maxIdle time.Duration
|
||||||
dataStore storage.Store
|
dataStore storage.Store
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
globalShutdown chan bool
|
globalShutdown chan bool
|
||||||
@@ -24,12 +23,12 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new 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{
|
return &Server{
|
||||||
host: fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port),
|
host: cfg.Addr,
|
||||||
domain: cfg.Domain,
|
domain: cfg.Domain,
|
||||||
dataStore: ds,
|
dataStore: ds,
|
||||||
maxIdleSeconds: cfg.MaxIdleSeconds,
|
maxIdle: cfg.MaxIdle,
|
||||||
globalShutdown: shutdownChan,
|
globalShutdown: shutdownChan,
|
||||||
waitgroup: new(sync.WaitGroup),
|
waitgroup: new(sync.WaitGroup),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,9 +412,9 @@ func (ss *Session) greet() {
|
|||||||
ss.send(fmt.Sprintf("220 %v Inbucket SMTP ready", ss.server.domain))
|
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 {
|
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
|
// Send requested message, store errors in Session.sendError
|
||||||
|
|||||||
@@ -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()) {
|
func setupSMTPServer(ds storage.Store) (s *Server, buf *bytes.Buffer, teardown func()) {
|
||||||
// Test Server Config
|
// Test Server Config
|
||||||
cfg := config.SMTPConfig{
|
cfg := config.SMTP{
|
||||||
IP4address: net.IPv4(127, 0, 0, 1),
|
Addr: "127.0.0.1:2500",
|
||||||
IP4port: 2500,
|
|
||||||
Domain: "inbucket.local",
|
Domain: "inbucket.local",
|
||||||
DomainNoStore: "bitbucket.local",
|
DomainNoStore: "bitbucket.local",
|
||||||
MaxRecipients: 5,
|
MaxRecipients: 5,
|
||||||
MaxIdleSeconds: 5,
|
MaxIdle: 5,
|
||||||
MaxMessageBytes: 5000,
|
MaxMessageBytes: 5000,
|
||||||
StoreMessages: true,
|
StoreMessages: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"container/list"
|
"container/list"
|
||||||
"context"
|
"context"
|
||||||
"expvar"
|
"expvar"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -43,7 +42,7 @@ type Server struct {
|
|||||||
domain string
|
domain string
|
||||||
domainNoStore string
|
domainNoStore string
|
||||||
maxRecips int
|
maxRecips int
|
||||||
maxIdleSeconds int
|
maxIdle time.Duration
|
||||||
maxMessageBytes int
|
maxMessageBytes int
|
||||||
storeMessages bool
|
storeMessages bool
|
||||||
|
|
||||||
@@ -80,17 +79,17 @@ var (
|
|||||||
|
|
||||||
// NewServer creates a new Server instance with the specificed config
|
// NewServer creates a new Server instance with the specificed config
|
||||||
func NewServer(
|
func NewServer(
|
||||||
cfg config.SMTPConfig,
|
cfg config.SMTP,
|
||||||
globalShutdown chan bool,
|
globalShutdown chan bool,
|
||||||
manager message.Manager,
|
manager message.Manager,
|
||||||
apolicy *policy.Addressing,
|
apolicy *policy.Addressing,
|
||||||
) *Server {
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
host: fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port),
|
host: cfg.Addr,
|
||||||
domain: cfg.Domain,
|
domain: cfg.Domain,
|
||||||
domainNoStore: strings.ToLower(cfg.DomainNoStore),
|
domainNoStore: strings.ToLower(cfg.DomainNoStore),
|
||||||
maxRecips: cfg.MaxRecipients,
|
maxRecips: cfg.MaxRecipients,
|
||||||
maxIdleSeconds: cfg.MaxIdleSeconds,
|
maxIdle: cfg.MaxIdle,
|
||||||
maxMessageBytes: cfg.MaxMessageBytes,
|
maxMessageBytes: cfg.MaxMessageBytes,
|
||||||
storeMessages: cfg.StoreMessages,
|
storeMessages: cfg.StoreMessages,
|
||||||
globalShutdown: globalShutdown,
|
globalShutdown: globalShutdown,
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Context is passed into every request handler function
|
// Context is passed into every request handler function
|
||||||
|
// TODO remove redundant web config
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Vars map[string]string
|
Vars map[string]string
|
||||||
Session *sessions.Session
|
Session *sessions.Session
|
||||||
MsgHub *msghub.Hub
|
MsgHub *msghub.Hub
|
||||||
Manager message.Manager
|
Manager message.Manager
|
||||||
WebConfig config.WebConfig
|
RootConfig *config.Root
|
||||||
|
WebConfig config.Web
|
||||||
IsJSON bool
|
IsJSON bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +63,8 @@ func NewContext(req *http.Request) (*Context, error) {
|
|||||||
Session: sess,
|
Session: sess,
|
||||||
MsgHub: msgHub,
|
MsgHub: msgHub,
|
||||||
Manager: manager,
|
Manager: manager,
|
||||||
WebConfig: webConfig,
|
RootConfig: rootConfig,
|
||||||
|
WebConfig: rootConfig.Web,
|
||||||
IsJSON: headerMatch(req, "Accept", "application/json"),
|
IsJSON: headerMatch(req, "Accept", "application/json"),
|
||||||
}
|
}
|
||||||
return ctx, err
|
return ctx, err
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package web
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"expvar"
|
"expvar"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -30,7 +29,7 @@ var (
|
|||||||
// incoming requests to the correct handler function
|
// incoming requests to the correct handler function
|
||||||
Router = mux.NewRouter()
|
Router = mux.NewRouter()
|
||||||
|
|
||||||
webConfig config.WebConfig
|
rootConfig *config.Root
|
||||||
server *http.Server
|
server *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
sessionStore sessions.Store
|
sessionStore sessions.Store
|
||||||
@@ -47,12 +46,12 @@ func init() {
|
|||||||
|
|
||||||
// Initialize sets up things for unit tests or the Start() method
|
// Initialize sets up things for unit tests or the Start() method
|
||||||
func Initialize(
|
func Initialize(
|
||||||
cfg config.WebConfig,
|
conf *config.Root,
|
||||||
shutdownChan chan bool,
|
shutdownChan chan bool,
|
||||||
mm message.Manager,
|
mm message.Manager,
|
||||||
mh *msghub.Hub) {
|
mh *msghub.Hub) {
|
||||||
|
|
||||||
webConfig = cfg
|
rootConfig = conf
|
||||||
globalShutdown = shutdownChan
|
globalShutdown = shutdownChan
|
||||||
|
|
||||||
// NewContext() will use this DataStore for the web handlers
|
// NewContext() will use this DataStore for the web handlers
|
||||||
@@ -60,36 +59,35 @@ func Initialize(
|
|||||||
manager = mm
|
manager = mm
|
||||||
|
|
||||||
// Content Paths
|
// Content Paths
|
||||||
log.Infof("HTTP templates mapped to %q", cfg.TemplateDir)
|
log.Infof("HTTP templates mapped to %q", conf.Web.TemplateDir)
|
||||||
log.Infof("HTTP static content mapped to %q", cfg.PublicDir)
|
log.Infof("HTTP static content mapped to %q", conf.Web.PublicDir)
|
||||||
Router.PathPrefix("/public/").Handler(http.StripPrefix("/public/",
|
Router.PathPrefix("/public/").Handler(http.StripPrefix("/public/",
|
||||||
http.FileServer(http.Dir(cfg.PublicDir))))
|
http.FileServer(http.Dir(conf.Web.PublicDir))))
|
||||||
http.Handle("/", Router)
|
http.Handle("/", Router)
|
||||||
|
|
||||||
// Session cookie setup
|
// Session cookie setup
|
||||||
if cfg.CookieAuthKey == "" {
|
if conf.Web.CookieAuthKey == "" {
|
||||||
log.Infof("HTTP generating random cookie.auth.key")
|
log.Infof("HTTP generating random cookie.auth.key")
|
||||||
sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
|
sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
|
||||||
} else {
|
} else {
|
||||||
log.Tracef("HTTP using configured cookie.auth.key")
|
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
|
// Start begins listening for HTTP requests
|
||||||
func Start(ctx context.Context) {
|
func Start(ctx context.Context) {
|
||||||
addr := fmt.Sprintf("%v:%v", webConfig.IP4address, webConfig.IP4port)
|
|
||||||
server = &http.Server{
|
server = &http.Server{
|
||||||
Addr: addr,
|
Addr: rootConfig.Web.Addr,
|
||||||
Handler: nil,
|
Handler: nil,
|
||||||
ReadTimeout: 60 * time.Second,
|
ReadTimeout: 60 * time.Second,
|
||||||
WriteTimeout: 60 * time.Second,
|
WriteTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't use ListenAndServe because it lacks a way to close the listener
|
// 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
|
var err error
|
||||||
listener, err = net.Listen("tcp", addr)
|
listener, err = net.Listen("tcp", server.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("HTTP failed to start TCP4 listener: %v", err)
|
log.Errorf("HTTP failed to start TCP4 listener: %v", err)
|
||||||
emergencyShutdown()
|
emergencyShutdown()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tempPath := strings.Replace(name, "/", string(filepath.Separator), -1)
|
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)
|
log.Tracef("Parsing template %v", tempFile)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -62,14 +62,14 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
|||||||
t, err = t.ParseFiles(tempFile)
|
t, err = t.ParseFiles(tempFile)
|
||||||
} else {
|
} else {
|
||||||
t = template.New("_base.html").Funcs(TemplateFuncs)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows us to disable caching for theme development
|
// Allows us to disable caching for theme development
|
||||||
if webConfig.TemplateCache {
|
if rootConfig.Web.TemplateCache {
|
||||||
if partial {
|
if partial {
|
||||||
log.Tracef("Caching partial %v", name)
|
log.Tracef("Caching partial %v", name)
|
||||||
cachedTemplates[name] = t
|
cachedTemplates[name] = t
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ type Store struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new DataStore object using the specified path
|
// 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
|
path := cfg.Path
|
||||||
if path == "" {
|
if path == "" {
|
||||||
log.Errorf("No value configured for datastore path")
|
log.Errorf("No value configured for datastore path")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
// TestSuite runs storage package test suite on file store.
|
// TestSuite runs storage package test suite on file store.
|
||||||
func TestSuite(t *testing.T) {
|
func TestSuite(t *testing.T) {
|
||||||
test.StoreSuite(t, func() (storage.Store, func(), error) {
|
test.StoreSuite(t, func() (storage.Store, func(), error) {
|
||||||
ds, _ := setupDataStore(config.DataStoreConfig{})
|
ds, _ := setupDataStore(config.Storage{})
|
||||||
destroy := func() {
|
destroy := func() {
|
||||||
teardownDataStore(ds)
|
teardownDataStore(ds)
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func TestSuite(t *testing.T) {
|
|||||||
|
|
||||||
// Test directory structure created by filestore
|
// Test directory structure created by filestore
|
||||||
func TestFSDirStructure(t *testing.T) {
|
func TestFSDirStructure(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
ds, logbuf := setupDataStore(config.Storage{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
root := ds.path
|
root := ds.path
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ func TestFSDirStructure(t *testing.T) {
|
|||||||
|
|
||||||
// Test missing files
|
// Test missing files
|
||||||
func TestFSMissing(t *testing.T) {
|
func TestFSMissing(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
ds, logbuf := setupDataStore(config.Storage{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
mbName := "fred"
|
mbName := "fred"
|
||||||
@@ -147,7 +147,7 @@ func TestFSMissing(t *testing.T) {
|
|||||||
// Test delivering several messages to the same mailbox, see if message cap works
|
// Test delivering several messages to the same mailbox, see if message cap works
|
||||||
func TestFSMessageCap(t *testing.T) {
|
func TestFSMessageCap(t *testing.T) {
|
||||||
mbCap := 10
|
mbCap := 10
|
||||||
ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap})
|
ds, logbuf := setupDataStore(config.Storage{MailboxMsgCap: mbCap})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
mbName := "captain"
|
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
|
// Test delivering several messages to the same mailbox, see if no message cap works
|
||||||
func TestFSNoMessageCap(t *testing.T) {
|
func TestFSNoMessageCap(t *testing.T) {
|
||||||
mbCap := 0
|
mbCap := 0
|
||||||
ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap})
|
ds, logbuf := setupDataStore(config.Storage{MailboxMsgCap: mbCap})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
mbName := "captain"
|
mbName := "captain"
|
||||||
@@ -218,7 +218,7 @@ func TestFSNoMessageCap(t *testing.T) {
|
|||||||
|
|
||||||
// Test Get the latest message
|
// Test Get the latest message
|
||||||
func TestGetLatestMessage(t *testing.T) {
|
func TestGetLatestMessage(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
ds, logbuf := setupDataStore(config.Storage{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
// james hashes to 474ba67bdb289c6263b36dfd8a7bed6c85b04943
|
// james hashes to 474ba67bdb289c6263b36dfd8a7bed6c85b04943
|
||||||
@@ -260,7 +260,7 @@ func TestGetLatestMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setupDataStore creates a new FileDataStore in a temporary directory
|
// 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")
|
path, err := ioutil.TempDir("", "inbucket")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ type RetentionScanner struct {
|
|||||||
|
|
||||||
// NewRetentionScanner configures a new RententionScanner.
|
// NewRetentionScanner configures a new RententionScanner.
|
||||||
func NewRetentionScanner(
|
func NewRetentionScanner(
|
||||||
cfg config.DataStoreConfig,
|
cfg config.Storage,
|
||||||
ds Store,
|
ds Store,
|
||||||
shutdownChannel chan bool,
|
shutdownChannel chan bool,
|
||||||
) *RetentionScanner {
|
) *RetentionScanner {
|
||||||
@@ -62,11 +62,11 @@ func NewRetentionScanner(
|
|||||||
globalShutdown: shutdownChannel,
|
globalShutdown: shutdownChannel,
|
||||||
retentionShutdown: make(chan bool),
|
retentionShutdown: make(chan bool),
|
||||||
ds: ds,
|
ds: ds,
|
||||||
retentionPeriod: time.Duration(cfg.RetentionMinutes) * time.Minute,
|
retentionPeriod: cfg.RetentionPeriod,
|
||||||
retentionSleep: time.Duration(cfg.RetentionSleep) * time.Millisecond,
|
retentionSleep: cfg.RetentionSleep,
|
||||||
}
|
}
|
||||||
// expRetentionPeriod is displayed on the status page
|
// expRetentionPeriod is displayed on the status page
|
||||||
expRetentionPeriod.Set(int64(cfg.RetentionMinutes * 60))
|
expRetentionPeriod.Set(int64(cfg.RetentionPeriod / time.Second))
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ func TestDoRetentionScan(t *testing.T) {
|
|||||||
ds.AddMessage(new2)
|
ds.AddMessage(new2)
|
||||||
ds.AddMessage(new3)
|
ds.AddMessage(new3)
|
||||||
// Test 4 hour retention
|
// Test 4 hour retention
|
||||||
cfg := config.DataStoreConfig{
|
cfg := config.Storage{
|
||||||
RetentionMinutes: 239,
|
RetentionPeriod: 239 * time.Minute,
|
||||||
RetentionSleep: 0,
|
RetentionSleep: 0,
|
||||||
}
|
}
|
||||||
shutdownChan := make(chan bool)
|
shutdownChan := make(chan bool)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
// RootIndex serves the Inbucket landing page
|
// RootIndex serves the Inbucket landing page
|
||||||
func RootIndex(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to load greeting: %v", err)
|
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
|
// RootMonitor serves the Inbucket monitor page
|
||||||
func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
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.AddFlash("Monitor is disabled in configuration", "errors")
|
||||||
_ = ctx.Session.Save(req, w)
|
_ = ctx.Session.Save(req, w)
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
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
|
// RootMonitorMailbox serves the Inbucket monitor page for a particular mailbox
|
||||||
func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
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.AddFlash("Monitor is disabled in configuration", "errors")
|
||||||
_ = ctx.Session.Save(req, w)
|
_ = ctx.Session.Save(req, w)
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
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
|
// RootStatus serves the Inbucket status page
|
||||||
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
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
|
// Get flash messages, save session
|
||||||
errorFlash := ctx.Session.Flashes("errors")
|
errorFlash := ctx.Session.Flashes("errors")
|
||||||
if err = ctx.Session.Save(req, w); err != nil {
|
if err = ctx.Session.Save(req, w); err != nil {
|
||||||
@@ -96,10 +90,10 @@ func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err
|
|||||||
"errorFlash": errorFlash,
|
"errorFlash": errorFlash,
|
||||||
"version": config.Version,
|
"version": config.Version,
|
||||||
"buildDate": config.BuildDate,
|
"buildDate": config.BuildDate,
|
||||||
"smtpListener": smtpListener,
|
"smtpListener": ctx.RootConfig.SMTP.Addr,
|
||||||
"pop3Listener": pop3Listener,
|
"pop3Listener": ctx.RootConfig.POP3.Addr,
|
||||||
"webListener": webListener,
|
"webListener": ctx.RootConfig.Web.Addr,
|
||||||
"smtpConfig": config.GetSMTPConfig(),
|
"smtpConfig": ctx.RootConfig.SMTP,
|
||||||
"dataStoreConfig": config.GetDataStoreConfig(),
|
"storageConfig": ctx.RootConfig.Storage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ $(document).ready(
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-xs-7"><b>Message Cap:</b></div>
|
<div class="col-sm-3 col-xs-7"><b>Message Cap:</b></div>
|
||||||
<div class="col-sm-8 col-xs-5">
|
<div class="col-sm-8 col-xs-5">
|
||||||
{{with .dataStoreConfig}}
|
{{with .storageConfig}}
|
||||||
<span>{{.MailboxMsgCap}} messages per mailbox</span>
|
<span>{{.MailboxMsgCap}} messages per mailbox</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -167,14 +167,14 @@ $(document).ready(
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
<span class="glyphicon glyphicon-hdd" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-hdd" aria-hidden="true"></span>
|
||||||
Data Store Metrics</h3>
|
Storage Metrics</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<table class="metrics">
|
<table class="metrics">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-xs-7"><b>Retention Period:</b></div>
|
<div class="col-sm-3 col-xs-7"><b>Retention Period:</b></div>
|
||||||
<div class="col-sm-8 col-xs-5">
|
<div class="col-sm-8 col-xs-5">
|
||||||
{{if .dataStoreConfig.RetentionMinutes}}
|
{{if .storageConfig.RetentionPeriod}}
|
||||||
<span id="m-retentionPeriod">.</span>
|
<span id="m-retentionPeriod">.</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
Disabled
|
Disabled
|
||||||
@@ -184,7 +184,7 @@ $(document).ready(
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-xs-7"><b>Retention Scan:</b></div>
|
<div class="col-sm-3 col-xs-7"><b>Retention Scan:</b></div>
|
||||||
<div class="col-sm-8 col-xs-5">
|
<div class="col-sm-8 col-xs-5">
|
||||||
{{if .dataStoreConfig.RetentionMinutes}}
|
{{if .storageConfig.RetentionPeriod}}
|
||||||
Completed <span id="m-retentionScanCompleted">.</span> ago
|
Completed <span id="m-retentionScanCompleted">.</span> ago
|
||||||
{{else}}
|
{{else}}
|
||||||
Disabled
|
Disabled
|
||||||
|
|||||||
Reference in New Issue
Block a user