mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
config: Replace robfig with envconfig for #86
- Initial envconfig system is working, not bulletproof. - Added sane defaults for required parameters.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user