mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-18 18:17:03 +00:00
Follow meta-linter recommendations for all of Inbucket
- Rename BUILD_DATE to BUILDDATE in goxc - Update travis config - Follow linter recommendations for inbucket.go - Follow linter recommendations for config package - Follow linter recommendations for log package - Follow linter recommendations for pop3d package - Follow linter recommendations for smtpd package - Follow linter recommendations for web package - Fix Id -> ID in templates - Add shebang to REST demo scripts - Add or refine many comments
This commit is contained in:
@@ -8,5 +8,11 @@
|
|||||||
"ResourcesInclude": "README*,LICENSE*,inbucket.bat,etc,themes",
|
"ResourcesInclude": "README*,LICENSE*,inbucket.bat,etc,themes",
|
||||||
"PackageVersion": "1.1.0",
|
"PackageVersion": "1.1.0",
|
||||||
"PrereleaseInfo": "alpha",
|
"PrereleaseInfo": "alpha",
|
||||||
"ConfigVersion": "0.9"
|
"ConfigVersion": "0.9",
|
||||||
|
"BuildSettings": {
|
||||||
|
"LdFlagsXVars": {
|
||||||
|
"TimeNow": "main.BUILDDATE",
|
||||||
|
"Version": "main.VERSION"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.5
|
- 1.5.3
|
||||||
- tip
|
- tip
|
||||||
|
|
||||||
install:
|
|
||||||
- go get -v ./...
|
|
||||||
- go get github.com/stretchr/testify
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import (
|
|||||||
"github.com/robfig/config"
|
"github.com/robfig/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SmtpConfig houses the SMTP server configuration - not using pointers
|
// SMTPConfig contains the SMTP server configuration - not using pointers
|
||||||
// so that I can pass around copies of the object safely.
|
// so that we can pass around copies of the object safely.
|
||||||
type SmtpConfig struct {
|
type SMTPConfig struct {
|
||||||
Ip4address net.IP
|
IP4address net.IP
|
||||||
Ip4port int
|
IP4port int
|
||||||
Domain string
|
Domain string
|
||||||
DomainNoStore string
|
DomainNoStore string
|
||||||
MaxRecipients int
|
MaxRecipients int
|
||||||
@@ -23,22 +23,25 @@ type SmtpConfig struct {
|
|||||||
StoreMessages bool
|
StoreMessages bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Pop3Config struct {
|
// POP3Config contains the POP3 server configuration
|
||||||
Ip4address net.IP
|
type POP3Config struct {
|
||||||
Ip4port int
|
IP4address net.IP
|
||||||
|
IP4port int
|
||||||
Domain string
|
Domain string
|
||||||
MaxIdleSeconds int
|
MaxIdleSeconds int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebConfig contains the HTTP server configuration
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
Ip4address net.IP
|
IP4address net.IP
|
||||||
Ip4port int
|
IP4port int
|
||||||
TemplateDir string
|
TemplateDir string
|
||||||
TemplateCache bool
|
TemplateCache bool
|
||||||
PublicDir string
|
PublicDir string
|
||||||
GreetingFile string
|
GreetingFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DataStoreConfig contains the mail store configuration
|
||||||
type DataStoreConfig struct {
|
type DataStoreConfig struct {
|
||||||
Path string
|
Path string
|
||||||
RetentionMinutes int
|
RetentionMinutes int
|
||||||
@@ -47,27 +50,29 @@ type DataStoreConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Build info, set by main
|
// Version of this build, set by main
|
||||||
VERSION = ""
|
Version = ""
|
||||||
BUILD_DATE = ""
|
|
||||||
|
|
||||||
// Global goconfig object
|
// BuildDate for this build, set by main
|
||||||
|
BuildDate = ""
|
||||||
|
|
||||||
|
// Config is our global robfig/config object
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
|
||||||
// Parsed specific configs
|
// Parsed specific configs
|
||||||
smtpConfig *SmtpConfig
|
smtpConfig *SMTPConfig
|
||||||
pop3Config *Pop3Config
|
pop3Config *POP3Config
|
||||||
webConfig *WebConfig
|
webConfig *WebConfig
|
||||||
dataStoreConfig *DataStoreConfig
|
dataStoreConfig *DataStoreConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSmtpConfig returns a copy of the SmtpConfig object
|
// GetSMTPConfig returns a copy of the SmtpConfig object
|
||||||
func GetSmtpConfig() SmtpConfig {
|
func GetSMTPConfig() SMTPConfig {
|
||||||
return *smtpConfig
|
return *smtpConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPop3Config returns a copy of the Pop3Config object
|
// GetPOP3Config returns a copy of the Pop3Config object
|
||||||
func GetPop3Config() Pop3Config {
|
func GetPOP3Config() POP3Config {
|
||||||
return *pop3Config
|
return *pop3Config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +143,11 @@ func LoadConfig(filename string) error {
|
|||||||
return fmt.Errorf("Failed to validate configuration")
|
return fmt.Errorf("Failed to validate configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = parseSmtpConfig(); err != nil {
|
if err = parseSMTPConfig(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = parsePop3Config(); err != nil {
|
if err = parsePOP3Config(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +179,9 @@ func parseLoggingConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSmtpConfig trying to catch config errors early
|
// parseSMTPConfig trying to catch config errors early
|
||||||
func parseSmtpConfig() error {
|
func parseSMTPConfig() error {
|
||||||
smtpConfig = new(SmtpConfig)
|
smtpConfig = new(SMTPConfig)
|
||||||
section := "smtp"
|
section := "smtp"
|
||||||
|
|
||||||
// Parse IP4 address only, error on IP6.
|
// Parse IP4 address only, error on IP6.
|
||||||
@@ -193,10 +198,10 @@ func parseSmtpConfig() error {
|
|||||||
if addr == nil {
|
if addr == nil {
|
||||||
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, err)
|
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, err)
|
||||||
}
|
}
|
||||||
smtpConfig.Ip4address = addr
|
smtpConfig.IP4address = addr
|
||||||
|
|
||||||
option = "ip4.port"
|
option = "ip4.port"
|
||||||
smtpConfig.Ip4port, err = Config.Int(section, option)
|
smtpConfig.IP4port, err = Config.Int(section, option)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
||||||
}
|
}
|
||||||
@@ -245,9 +250,9 @@ func parseSmtpConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePop3Config trying to catch config errors early
|
// parsePOP3Config trying to catch config errors early
|
||||||
func parsePop3Config() error {
|
func parsePOP3Config() error {
|
||||||
pop3Config = new(Pop3Config)
|
pop3Config = new(POP3Config)
|
||||||
section := "pop3"
|
section := "pop3"
|
||||||
|
|
||||||
// Parse IP4 address only, error on IP6.
|
// Parse IP4 address only, error on IP6.
|
||||||
@@ -264,10 +269,10 @@ func parsePop3Config() error {
|
|||||||
if addr == nil {
|
if addr == nil {
|
||||||
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, err)
|
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, err)
|
||||||
}
|
}
|
||||||
pop3Config.Ip4address = addr
|
pop3Config.IP4address = addr
|
||||||
|
|
||||||
option = "ip4.port"
|
option = "ip4.port"
|
||||||
pop3Config.Ip4port, err = Config.Int(section, option)
|
pop3Config.IP4port, err = Config.Int(section, option)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
||||||
}
|
}
|
||||||
@@ -307,10 +312,10 @@ func parseWebConfig() error {
|
|||||||
if addr == nil {
|
if addr == nil {
|
||||||
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, err)
|
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, err)
|
||||||
}
|
}
|
||||||
webConfig.Ip4address = addr
|
webConfig.IP4address = addr
|
||||||
|
|
||||||
option = "ip4.port"
|
option = "ip4.port"
|
||||||
webConfig.Ip4port, err = Config.Int(section, option)
|
webConfig.IP4port, err = Config.Int(section, option)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
curl -i -H "Accept: application/json" --noproxy localhost "http://localhost:9000/mailbox/$1"
|
curl -i -H "Accept: application/json" --noproxy localhost "http://localhost:9000/mailbox/$1"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
curl -i -H "Accept: application/json" --noproxy localhost -X DELETE http://localhost:9000/mailbox/$1
|
curl -i -H "Accept: application/json" --noproxy localhost -X DELETE http://localhost:9000/mailbox/$1
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2
|
curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
curl -i -H "Accept: application/json" --noproxy localhost -X DELETE http://localhost:9000/mailbox/$1/$2
|
curl -i -H "Accept: application/json" --noproxy localhost -X DELETE http://localhost:9000/mailbox/$1/$2
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2/source
|
curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2/source
|
||||||
|
|||||||
64
inbucket.go
64
inbucket.go
@@ -1,6 +1,4 @@
|
|||||||
/*
|
// main is the inbucket daemon launcher
|
||||||
This is the inbucket daemon launcher
|
|
||||||
*/
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -21,9 +19,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Build info, populated during linking by goxc
|
// VERSION contains the build version number, populated during linking by goxc
|
||||||
VERSION = "1.1.0.snapshot"
|
VERSION = "1.1.0.snapshot"
|
||||||
BUILD_DATE = "undefined"
|
|
||||||
|
// BUILDDATE contains the build date, populated during linking by goxc
|
||||||
|
BUILDDATE = "undefined"
|
||||||
|
|
||||||
// Command line flags
|
// Command line flags
|
||||||
help = flag.Bool("help", false, "Displays this help")
|
help = flag.Bool("help", false, "Displays this help")
|
||||||
@@ -42,8 +42,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config.VERSION = VERSION
|
config.Version = VERSION
|
||||||
config.BUILD_DATE = BUILD_DATE
|
config.BuildDate = BUILDDATE
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *help {
|
if *help {
|
||||||
@@ -84,27 +84,38 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer closeLogFile()
|
defer closeLogFile()
|
||||||
|
|
||||||
// close std* streams
|
// Close std* streams to prevent accidental output, they will be redirected to
|
||||||
os.Stdout.Close()
|
// our logfile below
|
||||||
os.Stderr.Close() // Warning: this will hide panic() output
|
if err := os.Stdout.Close(); err != nil {
|
||||||
os.Stdin.Close()
|
log.Errorf("Failed to close os.Stdout during log setup")
|
||||||
|
}
|
||||||
|
// Warning: this will hide panic() output
|
||||||
|
// TODO Replace with syscall.Dup2 per https://github.com/golang/go/issues/325
|
||||||
|
if err := os.Stderr.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close os.Stderr during log setup")
|
||||||
|
}
|
||||||
|
if err := os.Stdin.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close os.Stdin during log setup")
|
||||||
|
}
|
||||||
os.Stdout = logf
|
os.Stdout = logf
|
||||||
os.Stderr = logf
|
os.Stderr = logf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("Inbucket %v (%v) starting...", config.VERSION, config.BUILD_DATE)
|
log.Infof("Inbucket %v (%v) starting...", config.Version, config.BuildDate)
|
||||||
|
|
||||||
// Write pidfile if requested
|
// Write pidfile if requested
|
||||||
// TODO: Probably supposed to remove pidfile during shutdown
|
// TODO: Probably supposed to remove pidfile during shutdown
|
||||||
if *pidfile != "none" {
|
if *pidfile != "none" {
|
||||||
pidf, err := os.Create(*pidfile)
|
pidf, err := os.Create(*pidfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to create %v: %v", *pidfile, err)
|
log.Errorf("Failed to create %v: %v", *pidfile, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer pidf.Close()
|
|
||||||
fmt.Fprintf(pidf, "%v\n", os.Getpid())
|
fmt.Fprintf(pidf, "%v\n", os.Getpid())
|
||||||
|
if err := pidf.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close PID file %v: %v", *pidfile, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab our datastore
|
// Grab our datastore
|
||||||
@@ -119,7 +130,7 @@ func main() {
|
|||||||
go pop3Server.Start()
|
go pop3Server.Start()
|
||||||
|
|
||||||
// Startup SMTP server, block until it exits
|
// Startup SMTP server, block until it exits
|
||||||
smtpServer = smtpd.NewSmtpServer(config.GetSmtpConfig(), ds)
|
smtpServer = smtpd.NewServer(config.GetSMTPConfig(), ds)
|
||||||
smtpServer.Start()
|
smtpServer.Start()
|
||||||
|
|
||||||
// Wait for active connections to finish
|
// Wait for active connections to finish
|
||||||
@@ -136,14 +147,15 @@ func openLogFile() error {
|
|||||||
return fmt.Errorf("Failed to create %v: %v\n", *logfile, err)
|
return fmt.Errorf("Failed to create %v: %v\n", *logfile, err)
|
||||||
}
|
}
|
||||||
golog.SetOutput(logf)
|
golog.SetOutput(logf)
|
||||||
log.LogTrace("Opened new logfile")
|
log.Tracef("Opened new logfile")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeLogFile closes the current logfile
|
// closeLogFile closes the current logfile
|
||||||
func closeLogFile() error {
|
func closeLogFile() {
|
||||||
log.LogTrace("Closing logfile")
|
log.Tracef("Closing logfile")
|
||||||
return logf.Close()
|
// We are never in a situation where we can do anything about failing to close
|
||||||
|
_ = logf.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// signalProcessor is a goroutine that handles OS signals
|
// signalProcessor is a goroutine that handles OS signals
|
||||||
@@ -154,21 +166,23 @@ func signalProcessor(c <-chan os.Signal) {
|
|||||||
case syscall.SIGHUP:
|
case syscall.SIGHUP:
|
||||||
// Rotate logs if configured
|
// Rotate logs if configured
|
||||||
if logf != nil {
|
if logf != nil {
|
||||||
log.LogInfo("Recieved SIGHUP, cycling logfile")
|
log.Infof("Recieved SIGHUP, cycling logfile")
|
||||||
closeLogFile()
|
closeLogFile()
|
||||||
openLogFile()
|
// There is nothing we can do if the log open fails
|
||||||
|
// TODO We could panic, but that would be lame?
|
||||||
|
_ = openLogFile()
|
||||||
} else {
|
} else {
|
||||||
log.LogInfo("Ignoring SIGHUP, logfile not configured")
|
log.Infof("Ignoring SIGHUP, logfile not configured")
|
||||||
}
|
}
|
||||||
case syscall.SIGTERM:
|
case syscall.SIGTERM:
|
||||||
// Initiate shutdown
|
// Initiate shutdown
|
||||||
log.LogInfo("Received SIGTERM, shutting down")
|
log.Infof("Received SIGTERM, shutting down")
|
||||||
go timedExit()
|
go timedExit()
|
||||||
web.Stop()
|
web.Stop()
|
||||||
if smtpServer != nil {
|
if smtpServer != nil {
|
||||||
smtpServer.Stop()
|
smtpServer.Stop()
|
||||||
} else {
|
} else {
|
||||||
log.LogError("smtpServer was nil during shutdown")
|
log.Errorf("smtpServer was nil during shutdown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +191,7 @@ func signalProcessor(c <-chan os.Signal) {
|
|||||||
// timedExit is called as a goroutine during shutdown, it will force an exit after 15 seconds
|
// timedExit is called as a goroutine during shutdown, it will force an exit after 15 seconds
|
||||||
func timedExit() {
|
func timedExit() {
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
log.LogError("Inbucket clean shutdown timed out, forcing exit")
|
log.Errorf("Inbucket clean shutdown timed out, forcing exit")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +1,71 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
golog "log"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogLevel int
|
// Level is used to indicate the severity of a log entry
|
||||||
|
type Level int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ERROR LogLevel = iota
|
// ERROR indicates a significant problem was encountered
|
||||||
|
ERROR Level = iota
|
||||||
|
// WARN indicates something that may be a problem
|
||||||
WARN
|
WARN
|
||||||
|
// INFO indicates a purely informational log entry
|
||||||
INFO
|
INFO
|
||||||
|
// TRACE entries are meant for development purposes only
|
||||||
TRACE
|
TRACE
|
||||||
)
|
)
|
||||||
|
|
||||||
var MaxLogLevel LogLevel = TRACE
|
// MaxLevel is the highest Level we will log (max TRACE, min ERROR)
|
||||||
|
var MaxLevel = TRACE
|
||||||
|
|
||||||
// SetLogLevel sets MaxLogLevel based on the provided string
|
// SetLogLevel sets MaxLevel based on the provided string
|
||||||
func SetLogLevel(level string) (ok bool) {
|
func SetLogLevel(level string) (ok bool) {
|
||||||
switch strings.ToUpper(level) {
|
switch strings.ToUpper(level) {
|
||||||
case "ERROR":
|
case "ERROR":
|
||||||
MaxLogLevel = ERROR
|
MaxLevel = ERROR
|
||||||
case "WARN":
|
case "WARN":
|
||||||
MaxLogLevel = WARN
|
MaxLevel = WARN
|
||||||
case "INFO":
|
case "INFO":
|
||||||
MaxLogLevel = INFO
|
MaxLevel = INFO
|
||||||
case "TRACE":
|
case "TRACE":
|
||||||
MaxLogLevel = TRACE
|
MaxLevel = TRACE
|
||||||
default:
|
default:
|
||||||
LogError("Unknown log level requested: %v", level)
|
Errorf("Unknown log level requested: " + level)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs a message to the 'standard' Logger (always)
|
// Errorf logs a message to the 'standard' Logger (always), accepts format strings
|
||||||
func LogError(msg string, args ...interface{}) {
|
func Errorf(msg string, args ...interface{}) {
|
||||||
msg = "[ERROR] " + msg
|
msg = "[ERROR] " + msg
|
||||||
log.Printf(msg, args...)
|
golog.Printf(msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a message to the 'standard' Logger if MaxLogLevel is >= WARN
|
// Warnf logs a message to the 'standard' Logger if MaxLevel is >= WARN, accepts format strings
|
||||||
func LogWarn(msg string, args ...interface{}) {
|
func Warnf(msg string, args ...interface{}) {
|
||||||
if MaxLogLevel >= WARN {
|
if MaxLevel >= WARN {
|
||||||
msg = "[WARN ] " + msg
|
msg = "[WARN ] " + msg
|
||||||
log.Printf(msg, args...)
|
golog.Printf(msg, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a message to the 'standard' Logger if MaxLogLevel is >= INFO
|
// Infof logs a message to the 'standard' Logger if MaxLevel is >= INFO, accepts format strings
|
||||||
func LogInfo(msg string, args ...interface{}) {
|
func Infof(msg string, args ...interface{}) {
|
||||||
if MaxLogLevel >= INFO {
|
if MaxLevel >= INFO {
|
||||||
msg = "[INFO ] " + msg
|
msg = "[INFO ] " + msg
|
||||||
log.Printf(msg, args...)
|
golog.Printf(msg, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace logs a message to the 'standard' Logger if MaxLogLevel is >= TRACE
|
// Tracef logs a message to the 'standard' Logger if MaxLevel is >= TRACE, accepts format strings
|
||||||
func LogTrace(msg string, args ...interface{}) {
|
func Tracef(msg string, args ...interface{}) {
|
||||||
if MaxLogLevel >= TRACE {
|
if MaxLevel >= TRACE {
|
||||||
msg = "[TRACE] " + msg
|
msg = "[TRACE] " + msg
|
||||||
log.Printf(msg, args...)
|
golog.Printf(msg, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,15 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/smtpd"
|
"github.com/jhillyerd/inbucket/smtpd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// State tracks the current mode of our POP3 state machine
|
||||||
type State int
|
type State int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AUTHORIZATION State = iota // The client must now identify and authenticate
|
// AUTHORIZATION state: the client must now identify and authenticate
|
||||||
TRANSACTION // Mailbox open, client may now issue commands
|
AUTHORIZATION State = iota
|
||||||
|
// TRANSACTION state: mailbox open, client may now issue commands
|
||||||
|
TRANSACTION
|
||||||
|
// QUIT state: client requests us to end session
|
||||||
QUIT
|
QUIT
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +55,7 @@ var commands = map[string]bool{
|
|||||||
"CAPA": true,
|
"CAPA": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session defines an active POP3 session
|
||||||
type Session struct {
|
type Session struct {
|
||||||
server *Server // Reference to the server we belong to
|
server *Server // Reference to the server we belong to
|
||||||
id int // Session ID number
|
id int // Session ID number
|
||||||
@@ -66,6 +71,7 @@ type Session struct {
|
|||||||
msgCount int // Number of undeleted messages
|
msgCount int // Number of undeleted messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSession creates a new POP3 session
|
||||||
func NewSession(server *Server, id int, conn net.Conn) *Session {
|
func NewSession(server *Server, id int, conn net.Conn) *Session {
|
||||||
reader := bufio.NewReader(conn)
|
reader := bufio.NewReader(conn)
|
||||||
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||||
@@ -85,10 +91,12 @@ func (ses *Session) String() string {
|
|||||||
* 5. Goto 2
|
* 5. Goto 2
|
||||||
*/
|
*/
|
||||||
func (s *Server) startSession(id int, conn net.Conn) {
|
func (s *Server) startSession(id int, conn net.Conn) {
|
||||||
log.LogInfo("POP3 connection from %v, starting session <%v>", conn.RemoteAddr(), id)
|
log.Infof("POP3 connection from %v, starting session <%v>", conn.RemoteAddr(), id)
|
||||||
//expConnectsCurrent.Add(1)
|
//expConnectsCurrent.Add(1)
|
||||||
defer func() {
|
defer func() {
|
||||||
conn.Close()
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf("Error closing POP3 connection for <%v>: %v", id, err)
|
||||||
|
}
|
||||||
s.waitgroup.Done()
|
s.waitgroup.Done()
|
||||||
//expConnectsCurrent.Add(-1)
|
//expConnectsCurrent.Add(-1)
|
||||||
}()
|
}()
|
||||||
@@ -235,7 +243,7 @@ func (ses *Session) transactionHandler(cmd string, args []string) {
|
|||||||
var size int64
|
var size int64
|
||||||
for i, msg := range ses.messages {
|
for i, msg := range ses.messages {
|
||||||
if ses.retain[i] {
|
if ses.retain[i] {
|
||||||
count += 1
|
count++
|
||||||
size += msg.Size()
|
size += msg.Size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,12 +314,12 @@ func (ses *Session) transactionHandler(cmd string, args []string) {
|
|||||||
ses.send(fmt.Sprintf("-ERR You deleted message %v", msgNum))
|
ses.send(fmt.Sprintf("-ERR You deleted message %v", msgNum))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ses.send(fmt.Sprintf("+OK %v %v", msgNum, ses.messages[msgNum-1].Id()))
|
ses.send(fmt.Sprintf("+OK %v %v", msgNum, ses.messages[msgNum-1].ID()))
|
||||||
} else {
|
} else {
|
||||||
ses.send(fmt.Sprintf("+OK Listing %v messages", ses.msgCount))
|
ses.send(fmt.Sprintf("+OK Listing %v messages", ses.msgCount))
|
||||||
for i, msg := range ses.messages {
|
for i, msg := range ses.messages {
|
||||||
if ses.retain[i] {
|
if ses.retain[i] {
|
||||||
ses.send(fmt.Sprintf("%v %v", i+1, msg.Id()))
|
ses.send(fmt.Sprintf("%v %v", i+1, msg.ID()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ses.send(".")
|
ses.send(".")
|
||||||
@@ -340,7 +348,7 @@ func (ses *Session) transactionHandler(cmd string, args []string) {
|
|||||||
}
|
}
|
||||||
if ses.retain[msgNum-1] {
|
if ses.retain[msgNum-1] {
|
||||||
ses.retain[msgNum-1] = false
|
ses.retain[msgNum-1] = false
|
||||||
ses.msgCount -= 1
|
ses.msgCount--
|
||||||
ses.send(fmt.Sprintf("+OK Deleted message %v", msgNum))
|
ses.send(fmt.Sprintf("+OK Deleted message %v", msgNum))
|
||||||
} else {
|
} else {
|
||||||
ses.logWarn("Client tried to DELE an already deleted message")
|
ses.logWarn("Client tried to DELE an already deleted message")
|
||||||
@@ -431,7 +439,12 @@ func (ses *Session) sendMessage(msg smtpd.Message) {
|
|||||||
ses.send("-ERR Failed to RETR that message, internal error")
|
ses.send("-ERR Failed to RETR that message, internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
ses.logError("Failed to close message: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
@@ -459,7 +472,12 @@ func (ses *Session) sendMessageTop(msg smtpd.Message, lineCount int) {
|
|||||||
ses.send("-ERR Failed to RETR that message, internal error")
|
ses.send("-ERR Failed to RETR that message, internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
ses.logError("Failed to close message: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
inBody := false
|
inBody := false
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@@ -473,7 +491,7 @@ func (ses *Session) sendMessageTop(msg smtpd.Message, lineCount int) {
|
|||||||
if lineCount < 1 {
|
if lineCount < 1 {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
lineCount -= 1
|
lineCount--
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if line == "" {
|
if line == "" {
|
||||||
@@ -522,7 +540,9 @@ func (ses *Session) processDeletes() {
|
|||||||
for i, msg := range ses.messages {
|
for i, msg := range ses.messages {
|
||||||
if !ses.retain[i] {
|
if !ses.retain[i] {
|
||||||
ses.logTrace("Deleting %v", msg)
|
ses.logTrace("Deleting %v", msg)
|
||||||
msg.Delete()
|
if err := msg.Delete(); err != nil {
|
||||||
|
ses.logWarn("Error deleting %v: %v", msg, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -562,13 +582,17 @@ func (ses *Session) readByteLine(buf *bytes.Buffer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
buf.Write(line)
|
if _, err = buf.Write(line); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// Read the next byte looking for '\n'
|
// Read the next byte looking for '\n'
|
||||||
c, err := ses.reader.ReadByte()
|
c, err := ses.reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
buf.WriteByte(c)
|
if err := buf.WriteByte(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if c == '\n' {
|
if c == '\n' {
|
||||||
// We've reached the end of the line, return
|
// We've reached the end of the line, return
|
||||||
return nil
|
return nil
|
||||||
@@ -612,21 +636,21 @@ func (ses *Session) ooSeq(cmd string) {
|
|||||||
|
|
||||||
// Session specific logging methods
|
// Session specific logging methods
|
||||||
func (ses *Session) logTrace(msg string, args ...interface{}) {
|
func (ses *Session) logTrace(msg string, args ...interface{}) {
|
||||||
log.LogTrace("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
log.Tracef("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ses *Session) logInfo(msg string, args ...interface{}) {
|
func (ses *Session) logInfo(msg string, args ...interface{}) {
|
||||||
log.LogInfo("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
log.Infof("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ses *Session) logWarn(msg string, args ...interface{}) {
|
func (ses *Session) logWarn(msg string, args ...interface{}) {
|
||||||
// Update metrics
|
// Update metrics
|
||||||
//expWarnsTotal.Add(1)
|
//expWarnsTotal.Add(1)
|
||||||
log.LogWarn("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
log.Warnf("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ses *Session) logError(msg string, args ...interface{}) {
|
func (ses *Session) logError(msg string, args ...interface{}) {
|
||||||
// Update metrics
|
// Update metrics
|
||||||
//expErrorsTotal.Add(1)
|
//expErrorsTotal.Add(1)
|
||||||
log.LogError("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
log.Errorf("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/smtpd"
|
"github.com/jhillyerd/inbucket/smtpd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Real server code starts here
|
// Server defines an instance of our POP3 server
|
||||||
type Server struct {
|
type Server struct {
|
||||||
domain string
|
domain string
|
||||||
maxIdleSeconds int
|
maxIdleSeconds int
|
||||||
@@ -21,30 +21,30 @@ type Server struct {
|
|||||||
waitgroup *sync.WaitGroup
|
waitgroup *sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init a new Server object
|
// New creates a new Server struct
|
||||||
func New() *Server {
|
func New() *Server {
|
||||||
// TODO is two filestores better/worse than sharing w/ smtpd?
|
// TODO is two filestores better/worse than sharing w/ smtpd?
|
||||||
ds := smtpd.DefaultFileDataStore()
|
ds := smtpd.DefaultFileDataStore()
|
||||||
cfg := config.GetPop3Config()
|
cfg := config.GetPOP3Config()
|
||||||
return &Server{domain: cfg.Domain, dataStore: ds, maxIdleSeconds: cfg.MaxIdleSeconds,
|
return &Server{domain: cfg.Domain, dataStore: ds, maxIdleSeconds: cfg.MaxIdleSeconds,
|
||||||
waitgroup: new(sync.WaitGroup)}
|
waitgroup: new(sync.WaitGroup)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main listener loop
|
// Start the server and listen for connections
|
||||||
func (s *Server) Start() {
|
func (s *Server) Start() {
|
||||||
cfg := config.GetPop3Config()
|
cfg := config.GetPOP3Config()
|
||||||
addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v",
|
addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v",
|
||||||
cfg.Ip4address, cfg.Ip4port))
|
cfg.IP4address, cfg.IP4port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("POP3 Failed to build tcp4 address: %v", err)
|
log.Errorf("POP3 Failed to build tcp4 address: %v", err)
|
||||||
// TODO More graceful early-shutdown procedure
|
// TODO More graceful early-shutdown procedure
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("POP3 listening on TCP4 %v", addr)
|
log.Infof("POP3 listening on TCP4 %v", addr)
|
||||||
s.listener, err = net.ListenTCP("tcp4", addr)
|
s.listener, err = net.ListenTCP("tcp4", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("POP3 failed to start tcp4 listener: %v", err)
|
log.Errorf("POP3 failed to start tcp4 listener: %v", err)
|
||||||
// TODO More graceful early-shutdown procedure
|
// TODO More graceful early-shutdown procedure
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -63,12 +63,12 @@ func (s *Server) Start() {
|
|||||||
if max := 1 * time.Second; tempDelay > max {
|
if max := 1 * time.Second; tempDelay > max {
|
||||||
tempDelay = max
|
tempDelay = max
|
||||||
}
|
}
|
||||||
log.LogError("POP3 accept error: %v; retrying in %v", err, tempDelay)
|
log.Errorf("POP3 accept error: %v; retrying in %v", err, tempDelay)
|
||||||
time.Sleep(tempDelay)
|
time.Sleep(tempDelay)
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
if s.shutdown {
|
if s.shutdown {
|
||||||
log.LogTrace("POP3 listener shutting down on request")
|
log.Tracef("POP3 listener shutting down on request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO Implement a max error counter before shutdown?
|
// TODO Implement a max error counter before shutdown?
|
||||||
@@ -85,13 +85,15 @@ func (s *Server) Start() {
|
|||||||
|
|
||||||
// Stop requests the POP3 server closes it's listener
|
// Stop requests the POP3 server closes it's listener
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
log.LogTrace("POP3 shutdown requested, connections will be drained")
|
log.Tracef("POP3 shutdown requested, connections will be drained")
|
||||||
s.shutdown = true
|
s.shutdown = true
|
||||||
s.listener.Close()
|
if err := s.listener.Close(); err != nil {
|
||||||
|
log.Errorf("Error closing POP3 listener: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain causes the caller to block until all active POP3 sessions have finished
|
// Drain causes the caller to block until all active POP3 sessions have finished
|
||||||
func (s *Server) Drain() {
|
func (s *Server) Drain() {
|
||||||
s.waitgroup.Wait()
|
s.waitgroup.Wait()
|
||||||
log.LogTrace("POP3 connections drained")
|
log.Tracef("POP3 connections drained")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import (
|
|||||||
"github.com/jhillyerd/go.enmime"
|
"github.com/jhillyerd/go.enmime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrNotExist indicates the requested message does not exist
|
||||||
var ErrNotExist = errors.New("Message does not exist")
|
var ErrNotExist = errors.New("Message does not exist")
|
||||||
|
|
||||||
|
// DataStore is an interface to get Mailboxes stored in Inbucket
|
||||||
type DataStore interface {
|
type DataStore interface {
|
||||||
MailboxFor(emailAddress string) (Mailbox, error)
|
MailboxFor(emailAddress string) (Mailbox, error)
|
||||||
AllMailboxes() ([]Mailbox, error)
|
AllMailboxes() ([]Mailbox, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mailbox is an interface to get and manipulate messages in a DataStore
|
||||||
type Mailbox interface {
|
type Mailbox interface {
|
||||||
GetMessages() ([]Message, error)
|
GetMessages() ([]Message, error)
|
||||||
GetMessage(id string) (Message, error)
|
GetMessage(id string) (Message, error)
|
||||||
@@ -24,8 +27,9 @@ type Mailbox interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Message is an interface for a single message in a Mailbox
|
||||||
type Message interface {
|
type Message interface {
|
||||||
Id() string
|
ID() string
|
||||||
From() string
|
From() string
|
||||||
Date() time.Time
|
Date() time.Time
|
||||||
Subject() string
|
Subject() string
|
||||||
|
|||||||
@@ -19,16 +19,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Name of index file in each mailbox
|
// Name of index file in each mailbox
|
||||||
const INDEX_FILE = "index.gob"
|
const indexFileName = "index.gob"
|
||||||
|
|
||||||
// We lock this when reading/writing an index file, this is a bottleneck because
|
var (
|
||||||
// it's a single lock even if we have a million index files
|
// indexLock is locked while reading/writing an index file
|
||||||
var indexLock = new(sync.RWMutex)
|
//
|
||||||
|
// NOTE: This is a bottleneck because it's a single lock even if we have a
|
||||||
|
// million index files
|
||||||
|
indexLock = new(sync.RWMutex)
|
||||||
|
|
||||||
var ErrNotWritable = errors.New("Message not writable")
|
// TODO Consider moving this to the Message interface
|
||||||
|
errNotWritable = errors.New("Message not writable")
|
||||||
|
|
||||||
// Global because we only want one regardless of the number of DataStore objects
|
// countChannel is filled with a sequential numbers (0000..9999), which are
|
||||||
var countChannel = make(chan int, 10)
|
// used by generateID() to generate unique message IDs. It's global
|
||||||
|
// because we only want one regardless of the number of DataStore objects
|
||||||
|
countChannel = make(chan int, 10)
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Start generator
|
// Start generator
|
||||||
@@ -42,8 +49,8 @@ func countGenerator(c chan int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A DataStore is the root of the mail storage hiearchy. It provides access to
|
// FileDataStore implements DataStore aand is the root of the mail storage
|
||||||
// Mailbox objects
|
// hiearchy. It provides access to Mailbox objects
|
||||||
type FileDataStore struct {
|
type FileDataStore struct {
|
||||||
path string
|
path string
|
||||||
mailPath string
|
mailPath string
|
||||||
@@ -54,13 +61,15 @@ type FileDataStore struct {
|
|||||||
func NewFileDataStore(cfg config.DataStoreConfig) DataStore {
|
func NewFileDataStore(cfg config.DataStoreConfig) DataStore {
|
||||||
path := cfg.Path
|
path := cfg.Path
|
||||||
if path == "" {
|
if path == "" {
|
||||||
log.LogError("No value configured for datastore path")
|
log.Errorf("No value configured for datastore path")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
mailPath := filepath.Join(path, "mail")
|
mailPath := filepath.Join(path, "mail")
|
||||||
if _, err := os.Stat(mailPath); err != nil {
|
if _, err := os.Stat(mailPath); err != nil {
|
||||||
// Mail datastore does not yet exist
|
// Mail datastore does not yet exist
|
||||||
os.MkdirAll(mailPath, 0770)
|
if err = os.MkdirAll(mailPath, 0770); err != nil {
|
||||||
|
log.Errorf("Error creating dir %q: %v", mailPath, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &FileDataStore{path: path, mailPath: mailPath, messageCap: cfg.MailboxMsgCap}
|
return &FileDataStore{path: path, mailPath: mailPath, messageCap: cfg.MailboxMsgCap}
|
||||||
}
|
}
|
||||||
@@ -72,7 +81,7 @@ func DefaultFileDataStore() DataStore {
|
|||||||
return NewFileDataStore(cfg)
|
return NewFileDataStore(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieves the Mailbox object for a specified email address, if the mailbox
|
// MailboxFor retrieves the Mailbox object for a specified email address, if the mailbox
|
||||||
// does not exist, it will attempt to create it.
|
// does not exist, it will attempt to create it.
|
||||||
func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) {
|
func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) {
|
||||||
name, err := ParseMailboxName(emailAddress)
|
name, err := ParseMailboxName(emailAddress)
|
||||||
@@ -83,7 +92,7 @@ func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) {
|
|||||||
s1 := dir[0:3]
|
s1 := dir[0:3]
|
||||||
s2 := dir[0:6]
|
s2 := dir[0:6]
|
||||||
path := filepath.Join(ds.mailPath, s1, s2, dir)
|
path := filepath.Join(ds.mailPath, s1, s2, dir)
|
||||||
indexPath := filepath.Join(path, INDEX_FILE)
|
indexPath := filepath.Join(path, indexFileName)
|
||||||
|
|
||||||
return &FileMailbox{store: ds, name: name, dirName: dir, path: path,
|
return &FileMailbox{store: ds, name: name, dirName: dir, path: path,
|
||||||
indexPath: indexPath}, nil
|
indexPath: indexPath}, nil
|
||||||
@@ -117,7 +126,7 @@ func (ds *FileDataStore) AllMailboxes() ([]Mailbox, error) {
|
|||||||
if inf3.IsDir() {
|
if inf3.IsDir() {
|
||||||
mbdir := inf3.Name()
|
mbdir := inf3.Name()
|
||||||
mbpath := filepath.Join(ds.mailPath, l1, l2, mbdir)
|
mbpath := filepath.Join(ds.mailPath, l1, l2, mbdir)
|
||||||
idx := filepath.Join(mbpath, INDEX_FILE)
|
idx := filepath.Join(mbpath, indexFileName)
|
||||||
mb := &FileMailbox{store: ds, dirName: mbdir, path: mbpath,
|
mb := &FileMailbox{store: ds, dirName: mbdir, path: mbpath,
|
||||||
indexPath: idx}
|
indexPath: idx}
|
||||||
mailboxes = append(mailboxes, mb)
|
mailboxes = append(mailboxes, mb)
|
||||||
@@ -131,8 +140,8 @@ func (ds *FileDataStore) AllMailboxes() ([]Mailbox, error) {
|
|||||||
return mailboxes, nil
|
return mailboxes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Mailbox manages the mail for a specific user and correlates to a particular
|
// FileMailbox implements Mailbox, manages the mail for a specific user and
|
||||||
// directory on disk.
|
// correlates to a particular directory on disk.
|
||||||
type FileMailbox struct {
|
type FileMailbox struct {
|
||||||
store *FileDataStore
|
store *FileDataStore
|
||||||
name string
|
name string
|
||||||
@@ -180,7 +189,7 @@ func (mb *FileMailbox) GetMessage(id string) (Message, error) {
|
|||||||
return nil, ErrNotExist
|
return nil, ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all messages in this mailbox
|
// Purge deletes all messages in this mailbox
|
||||||
func (mb *FileMailbox) Purge() error {
|
func (mb *FileMailbox) Purge() error {
|
||||||
mb.messages = mb.messages[:0]
|
mb.messages = mb.messages[:0]
|
||||||
return mb.writeIndex()
|
return mb.writeIndex()
|
||||||
@@ -196,7 +205,7 @@ func (mb *FileMailbox) readIndex() error {
|
|||||||
// Check if index exists
|
// Check if index exists
|
||||||
if _, err := os.Stat(mb.indexPath); err != nil {
|
if _, err := os.Stat(mb.indexPath); err != nil {
|
||||||
// Does not exist, but that's not an error in our world
|
// Does not exist, but that's not an error in our world
|
||||||
log.LogTrace("Index %v does not exist (yet)", mb.indexPath)
|
log.Tracef("Index %v does not exist (yet)", mb.indexPath)
|
||||||
mb.indexLoaded = true
|
mb.indexLoaded = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -204,7 +213,11 @@ func (mb *FileMailbox) readIndex() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close %q: %v", mb.indexPath, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Decode gob data
|
// Decode gob data
|
||||||
dec := gob.NewDecoder(bufio.NewReader(file))
|
dec := gob.NewDecoder(bufio.NewReader(file))
|
||||||
@@ -219,7 +232,7 @@ func (mb *FileMailbox) readIndex() error {
|
|||||||
return fmt.Errorf("While decoding message: %v", err)
|
return fmt.Errorf("While decoding message: %v", err)
|
||||||
}
|
}
|
||||||
msg.mailbox = mb
|
msg.mailbox = mb
|
||||||
log.LogTrace("Found: %v", msg)
|
log.Tracef("Found: %v", msg)
|
||||||
mb.messages = append(mb.messages, msg)
|
mb.messages = append(mb.messages, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +244,7 @@ func (mb *FileMailbox) readIndex() error {
|
|||||||
func (mb *FileMailbox) createDir() error {
|
func (mb *FileMailbox) createDir() error {
|
||||||
if _, err := os.Stat(mb.path); err != nil {
|
if _, err := os.Stat(mb.path); err != nil {
|
||||||
if err := os.MkdirAll(mb.path, 0770); err != nil {
|
if err := os.MkdirAll(mb.path, 0770); err != nil {
|
||||||
log.LogError("Failed to create directory %v, %v", mb.path, err)
|
log.Errorf("Failed to create directory %v, %v", mb.path, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,7 +266,11 @@ func (mb *FileMailbox) writeIndex() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close %q: %v", mb.indexPath, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
writer := bufio.NewWriter(file)
|
writer := bufio.NewWriter(file)
|
||||||
|
|
||||||
// Write each message and then flush
|
// Write each message and then flush
|
||||||
@@ -264,18 +281,20 @@ func (mb *FileMailbox) writeIndex() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writer.Flush()
|
if err := writer.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No messages, delete index+maildir
|
// No messages, delete index+maildir
|
||||||
log.LogTrace("Removing mailbox %v", mb.path)
|
log.Tracef("Removing mailbox %v", mb.path)
|
||||||
return os.RemoveAll(mb.path)
|
return os.RemoveAll(mb.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message contains a little bit of data about a particular email message, and
|
// FileMessage implements Message and contains a little bit of data about a
|
||||||
// methods to retrieve the rest of it from disk.
|
// particular email message, and methods to retrieve the rest of it from disk.
|
||||||
type FileMessage struct {
|
type FileMessage struct {
|
||||||
mailbox *FileMailbox
|
mailbox *FileMailbox
|
||||||
// Stored in GOB
|
// Stored in GOB
|
||||||
@@ -290,7 +309,7 @@ type FileMessage struct {
|
|||||||
writer *bufio.Writer
|
writer *bufio.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessage creates a new Message object and sets the Date and Id fields.
|
// NewMessage creates a new FileMessage object and sets the Date and Id fields.
|
||||||
// It will also delete messages over messageCap if configured.
|
// It will also delete messages over messageCap if configured.
|
||||||
func (mb *FileMailbox) NewMessage() (Message, error) {
|
func (mb *FileMailbox) NewMessage() (Message, error) {
|
||||||
// Load index
|
// Load index
|
||||||
@@ -303,7 +322,7 @@ func (mb *FileMailbox) NewMessage() (Message, error) {
|
|||||||
// Delete old messages over messageCap
|
// Delete old messages over messageCap
|
||||||
if mb.store.messageCap > 0 {
|
if mb.store.messageCap > 0 {
|
||||||
for len(mb.messages) >= mb.store.messageCap {
|
for len(mb.messages) >= mb.store.messageCap {
|
||||||
log.LogInfo("Mailbox %q over configured message cap", mb.name)
|
log.Infof("Mailbox %q over configured message cap", mb.name)
|
||||||
if err := mb.messages[0].Delete(); err != nil {
|
if err := mb.messages[0].Delete(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -311,30 +330,37 @@ func (mb *FileMailbox) NewMessage() (Message, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
date := time.Now()
|
date := time.Now()
|
||||||
id := generateId(date)
|
id := generateID(date)
|
||||||
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}, nil
|
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FileMessage) Id() string {
|
// ID gets the ID of the Message
|
||||||
|
func (m *FileMessage) ID() string {
|
||||||
return m.Fid
|
return m.Fid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Date returns the date of the Message
|
||||||
|
// TODO Is this the create timestamp, or from the Date header?
|
||||||
func (m *FileMessage) Date() time.Time {
|
func (m *FileMessage) Date() time.Time {
|
||||||
return m.Fdate
|
return m.Fdate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From returns the value of the Message From header
|
||||||
func (m *FileMessage) From() string {
|
func (m *FileMessage) From() string {
|
||||||
return m.Ffrom
|
return m.Ffrom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subject returns the value of the Message Subject header
|
||||||
func (m *FileMessage) Subject() string {
|
func (m *FileMessage) Subject() string {
|
||||||
return m.Fsubject
|
return m.Fsubject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns a string in the form: "Subject()" from From()
|
||||||
func (m *FileMessage) String() string {
|
func (m *FileMessage) String() string {
|
||||||
return fmt.Sprintf("\"%v\" from %v", m.Fsubject, m.Ffrom)
|
return fmt.Sprintf("\"%v\" from %v", m.Fsubject, m.Ffrom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Size returns the size of the Message on disk in bytes
|
||||||
func (m *FileMessage) Size() int64 {
|
func (m *FileMessage) Size() int64 {
|
||||||
return m.Fsize
|
return m.Fsize
|
||||||
}
|
}
|
||||||
@@ -349,10 +375,14 @@ func (m *FileMessage) ReadHeader() (msg *mail.Message, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close %q: %v", m.rawPath(), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
reader := bufio.NewReader(file)
|
reader := bufio.NewReader(file)
|
||||||
msg, err = mail.ReadMessage(reader)
|
return mail.ReadMessage(reader)
|
||||||
return msg, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadBody opens the .raw portion of a Message and returns a MIMEBody object
|
// ReadBody opens the .raw portion of a Message and returns a MIMEBody object
|
||||||
@@ -361,7 +391,12 @@ func (m *FileMessage) ReadBody() (body *enmime.MIMEBody, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close %q: %v", m.rawPath(), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
reader := bufio.NewReader(file)
|
reader := bufio.NewReader(file)
|
||||||
msg, err := mail.ReadMessage(reader)
|
msg, err := mail.ReadMessage(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -371,7 +406,7 @@ func (m *FileMessage) ReadBody() (body *enmime.MIMEBody, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return mime, err
|
return mime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawReader opens the .raw portion of a Message as an io.ReadCloser
|
// RawReader opens the .raw portion of a Message as an io.ReadCloser
|
||||||
@@ -386,10 +421,15 @@ func (m *FileMessage) RawReader() (reader io.ReadCloser, err error) {
|
|||||||
// ReadRaw opens the .raw portion of a Message and returns it as a string
|
// ReadRaw opens the .raw portion of a Message and returns it as a string
|
||||||
func (m *FileMessage) ReadRaw() (raw *string, err error) {
|
func (m *FileMessage) ReadRaw() (raw *string, err error) {
|
||||||
reader, err := m.RawReader()
|
reader, err := m.RawReader()
|
||||||
defer reader.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close %q: %v", m.rawPath(), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
bodyBytes, err := ioutil.ReadAll(bufio.NewReader(reader))
|
bodyBytes, err := ioutil.ReadAll(bufio.NewReader(reader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -403,7 +443,7 @@ func (m *FileMessage) ReadRaw() (raw *string, err error) {
|
|||||||
func (m *FileMessage) Append(data []byte) error {
|
func (m *FileMessage) Append(data []byte) error {
|
||||||
// Prevent Appending to a pre-existing Message
|
// Prevent Appending to a pre-existing Message
|
||||||
if !m.writable {
|
if !m.writable {
|
||||||
return ErrNotWritable
|
return errNotWritable
|
||||||
}
|
}
|
||||||
// Open file for writing if we haven't yet
|
// Open file for writing if we haven't yet
|
||||||
if m.writer == nil {
|
if m.writer == nil {
|
||||||
@@ -466,7 +506,8 @@ func (m *FileMessage) Close() error {
|
|||||||
return m.mailbox.writeIndex()
|
return m.mailbox.writeIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete this Message from disk by removing both the gob and raw files
|
// Delete this Message from disk by removing it from the index and deleting the
|
||||||
|
// raw files.
|
||||||
func (m *FileMessage) Delete() error {
|
func (m *FileMessage) Delete() error {
|
||||||
messages := m.mailbox.messages
|
messages := m.mailbox.messages
|
||||||
for i, mm := range messages {
|
for i, mm := range messages {
|
||||||
@@ -476,16 +517,18 @@ func (m *FileMessage) Delete() error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.mailbox.writeIndex()
|
if err := m.mailbox.writeIndex(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if len(m.mailbox.messages) == 0 {
|
if len(m.mailbox.messages) == 0 {
|
||||||
// This was the last message, writeIndex() has removed the entire
|
// This was the last message, thus writeIndex() has removed the entire
|
||||||
// directory
|
// directory; we don't need to delete the raw file.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are still messages in the index
|
// There are still messages in the index
|
||||||
log.LogTrace("Deleting %v", m.rawPath())
|
log.Tracef("Deleting %v", m.rawPath())
|
||||||
return os.Remove(m.rawPath())
|
return os.Remove(m.rawPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +541,6 @@ func generatePrefix(date time.Time) string {
|
|||||||
|
|
||||||
// generateId adds a 4-digit unique number onto the end of the string
|
// generateId adds a 4-digit unique number onto the end of the string
|
||||||
// returned by generatePrefix()
|
// returned by generatePrefix()
|
||||||
func generateId(date time.Time) string {
|
func generateID(date time.Time) string {
|
||||||
return generatePrefix(date) + "-" + fmt.Sprintf("%04d", <-countChannel)
|
return generatePrefix(date) + "-" + fmt.Sprintf("%04d", <-countChannel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func TestFSDirStructure(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ func TestFSAllMailboxes(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ func TestFSDeliverMany(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +201,8 @@ func TestFSDelete(t *testing.T) {
|
|||||||
len(subjects), len(msgs))
|
len(subjects), len(msgs))
|
||||||
|
|
||||||
// Delete a couple messages
|
// Delete a couple messages
|
||||||
msgs[1].Delete()
|
_ = msgs[1].Delete()
|
||||||
msgs[3].Delete()
|
_ = msgs[3].Delete()
|
||||||
|
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
mb, err = ds.MailboxFor(mbName)
|
mb, err = ds.MailboxFor(mbName)
|
||||||
@@ -246,7 +246,7 @@ func TestFSDelete(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ func TestFSPurge(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ func TestFSSize(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ func TestFSMissing(t *testing.T) {
|
|||||||
msg, err := mb.GetMessage(sentIds[1])
|
msg, err := mb.GetMessage(sentIds[1])
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
fmsg := msg.(*FileMessage)
|
fmsg := msg.(*FileMessage)
|
||||||
os.Remove(fmsg.rawPath())
|
_ = os.Remove(fmsg.rawPath())
|
||||||
msg, err = mb.GetMessage(sentIds[1])
|
msg, err = mb.GetMessage(sentIds[1])
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ func TestFSMissing(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ func TestFSMessageCap(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ func TestFSNoMessageCap(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +490,7 @@ func deliverMessage(ds *FileDataStore, mbName string, subject string,
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
// Create message object
|
// Create message object
|
||||||
id = generateId(date)
|
id = generateID(date)
|
||||||
msg, err := mb.NewMessage()
|
msg, err := mb.NewMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -498,7 +498,9 @@ func deliverMessage(ds *FileDataStore, mbName string, subject string,
|
|||||||
fmsg := msg.(*FileMessage)
|
fmsg := msg.(*FileMessage)
|
||||||
fmsg.Fdate = date
|
fmsg.Fdate = date
|
||||||
fmsg.Fid = id
|
fmsg.Fid = id
|
||||||
msg.Append(testMsg)
|
if err = msg.Append(testMsg); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
if err = msg.Close(); err != nil {
|
if err = msg.Close(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,23 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/log"
|
"github.com/jhillyerd/inbucket/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// State tracks the current mode of our SMTP state machine
|
||||||
type State int
|
type State int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GREET State = iota // Waiting for HELO
|
// GREET State: Waiting for HELO
|
||||||
READY // Got HELO, waiting for MAIL
|
GREET State = iota
|
||||||
MAIL // Got MAIL, accepting RCPTs
|
// READY State: Got HELO, waiting for MAIL
|
||||||
DATA // Got DATA, waiting for "."
|
READY
|
||||||
QUIT // Close session
|
// MAIL State: Got MAIL, accepting RCPTs
|
||||||
|
MAIL
|
||||||
|
// DATA State: Got DATA, waiting for "."
|
||||||
|
DATA
|
||||||
|
// QUIT State: Client requested end of session
|
||||||
|
QUIT
|
||||||
)
|
)
|
||||||
|
|
||||||
const STAMP_FMT = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
|
const timeStampFormat = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
|
||||||
|
|
||||||
func (s State) String() string {
|
func (s State) String() string {
|
||||||
switch s {
|
switch s {
|
||||||
@@ -61,6 +67,7 @@ var commands = map[string]bool{
|
|||||||
"TURN": true,
|
"TURN": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session holds the state of an SMTP session
|
||||||
type Session struct {
|
type Session struct {
|
||||||
server *Server
|
server *Server
|
||||||
id int
|
id int
|
||||||
@@ -74,6 +81,7 @@ type Session struct {
|
|||||||
recipients *list.List
|
recipients *list.List
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSession creates a new Session for the given connection
|
||||||
func NewSession(server *Server, id int, conn net.Conn) *Session {
|
func NewSession(server *Server, id int, conn net.Conn) *Session {
|
||||||
reader := bufio.NewReader(conn)
|
reader := bufio.NewReader(conn)
|
||||||
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||||
@@ -92,10 +100,12 @@ func (ss *Session) String() string {
|
|||||||
* 5. Goto 2
|
* 5. Goto 2
|
||||||
*/
|
*/
|
||||||
func (s *Server) startSession(id int, conn net.Conn) {
|
func (s *Server) startSession(id int, conn net.Conn) {
|
||||||
log.LogInfo("SMTP Connection from %v, starting session <%v>", conn.RemoteAddr(), id)
|
log.Infof("SMTP Connection from %v, starting session <%v>", conn.RemoteAddr(), id)
|
||||||
expConnectsCurrent.Add(1)
|
expConnectsCurrent.Add(1)
|
||||||
defer func() {
|
defer func() {
|
||||||
conn.Close()
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf("Error closing connection for <%v>: %v", id, err)
|
||||||
|
}
|
||||||
s.waitgroup.Done()
|
s.waitgroup.Done()
|
||||||
expConnectsCurrent.Add(-1)
|
expConnectsCurrent.Add(-1)
|
||||||
}()
|
}()
|
||||||
@@ -321,19 +331,18 @@ func (ss *Session) mailHandler(cmd string, arg string) {
|
|||||||
// We have recipients, go to accept data
|
// We have recipients, go to accept data
|
||||||
ss.enterState(DATA)
|
ss.enterState(DATA)
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
// DATA out of sequence
|
// DATA out of sequence
|
||||||
ss.ooSeq(cmd)
|
ss.ooSeq(cmd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ss.ooSeq(cmd)
|
ss.ooSeq(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DATA
|
// DATA
|
||||||
func (ss *Session) dataHandler() {
|
func (ss *Session) dataHandler() {
|
||||||
// Timestamp for Received header
|
// Timestamp for Received header
|
||||||
stamp := time.Now().Format(STAMP_FMT)
|
stamp := time.Now().Format(timeStampFormat)
|
||||||
// Get a Mailbox and a new Message for each recipient
|
// Get a Mailbox and a new Message for each recipient
|
||||||
mailboxes := make([]Mailbox, ss.recipients.Len())
|
mailboxes := make([]Mailbox, ss.recipients.Len())
|
||||||
messages := make([]Message, ss.recipients.Len())
|
messages := make([]Message, ss.recipients.Len())
|
||||||
@@ -369,9 +378,14 @@ func (ss *Session) dataHandler() {
|
|||||||
// Generate Received header
|
// Generate Received header
|
||||||
recd := fmt.Sprintf("Received: from %s ([%s]) by %s\r\n for <%s>; %s\r\n",
|
recd := fmt.Sprintf("Received: from %s ([%s]) by %s\r\n for <%s>; %s\r\n",
|
||||||
ss.remoteDomain, ss.remoteHost, ss.server.domain, recip, stamp)
|
ss.remoteDomain, ss.remoteHost, ss.server.domain, recip, stamp)
|
||||||
messages[i].Append([]byte(recd))
|
if err := messages[i].Append([]byte(recd)); err != nil {
|
||||||
|
ss.logError("Failed to write received header for %q: %s", local, err)
|
||||||
|
ss.send(fmt.Sprintf("451 Failed to create message for %v", local))
|
||||||
|
ss.reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.LogTrace("Not storing message for %q", recip)
|
log.Tracef("Not storing message for %q", recip)
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
@@ -482,13 +496,17 @@ func (ss *Session) readByteLine(buf *bytes.Buffer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
buf.Write(line)
|
if _, err = buf.Write(line); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// Read the next byte looking for '\n'
|
// Read the next byte looking for '\n'
|
||||||
c, err := ss.reader.ReadByte()
|
c, err := ss.reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
buf.WriteByte(c)
|
if err = buf.WriteByte(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if c == '\n' {
|
if c == '\n' {
|
||||||
// We've reached the end of the line, return
|
// We've reached the end of the line, return
|
||||||
return nil
|
return nil
|
||||||
@@ -570,21 +588,21 @@ func (ss *Session) ooSeq(cmd string) {
|
|||||||
|
|
||||||
// Session specific logging methods
|
// Session specific logging methods
|
||||||
func (ss *Session) logTrace(msg string, args ...interface{}) {
|
func (ss *Session) logTrace(msg string, args ...interface{}) {
|
||||||
log.LogTrace("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
log.Tracef("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Session) logInfo(msg string, args ...interface{}) {
|
func (ss *Session) logInfo(msg string, args ...interface{}) {
|
||||||
log.LogInfo("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
log.Infof("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Session) logWarn(msg string, args ...interface{}) {
|
func (ss *Session) logWarn(msg string, args ...interface{}) {
|
||||||
// Update metrics
|
// Update metrics
|
||||||
expWarnsTotal.Add(1)
|
expWarnsTotal.Add(1)
|
||||||
log.LogWarn("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
log.Warnf("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Session) logError(msg string, args ...interface{}) {
|
func (ss *Session) logError(msg string, args ...interface{}) {
|
||||||
// Update metrics
|
// Update metrics
|
||||||
expErrorsTotal.Add(1)
|
expErrorsTotal.Add(1)
|
||||||
log.LogError("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
log.Errorf("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/jhillyerd/inbucket/config"
|
"github.com/jhillyerd/inbucket/config"
|
||||||
//"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
@@ -27,8 +26,8 @@ func TestGreetState(t *testing.T) {
|
|||||||
mb1 := &MockMailbox{}
|
mb1 := &MockMailbox{}
|
||||||
mds.On("MailboxFor").Return(mb1, nil)
|
mds.On("MailboxFor").Return(mb1, nil)
|
||||||
|
|
||||||
server, logbuf := setupSmtpServer(mds)
|
server, logbuf := setupSMTPServer(mds)
|
||||||
defer teardownSmtpServer(server)
|
defer teardownSMTPServer(server)
|
||||||
|
|
||||||
var script []scriptStep
|
var script []scriptStep
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ func TestGreetState(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +87,8 @@ func TestReadyState(t *testing.T) {
|
|||||||
mb1 := &MockMailbox{}
|
mb1 := &MockMailbox{}
|
||||||
mds.On("MailboxFor").Return(mb1, nil)
|
mds.On("MailboxFor").Return(mb1, nil)
|
||||||
|
|
||||||
server, logbuf := setupSmtpServer(mds)
|
server, logbuf := setupSMTPServer(mds)
|
||||||
defer teardownSmtpServer(server)
|
defer teardownSMTPServer(server)
|
||||||
|
|
||||||
var script []scriptStep
|
var script []scriptStep
|
||||||
|
|
||||||
@@ -142,7 +141,7 @@ func TestReadyState(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +155,8 @@ func TestMailState(t *testing.T) {
|
|||||||
mb1.On("NewMessage").Return(msg1, nil)
|
mb1.On("NewMessage").Return(msg1, nil)
|
||||||
msg1.On("Close").Return(nil)
|
msg1.On("Close").Return(nil)
|
||||||
|
|
||||||
server, logbuf := setupSmtpServer(mds)
|
server, logbuf := setupSMTPServer(mds)
|
||||||
defer teardownSmtpServer(server)
|
defer teardownSMTPServer(server)
|
||||||
|
|
||||||
var script []scriptStep
|
var script []scriptStep
|
||||||
|
|
||||||
@@ -252,7 +251,7 @@ func TestMailState(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,11 +265,11 @@ func TestDataState(t *testing.T) {
|
|||||||
mb1.On("NewMessage").Return(msg1, nil)
|
mb1.On("NewMessage").Return(msg1, nil)
|
||||||
msg1.On("Close").Return(nil)
|
msg1.On("Close").Return(nil)
|
||||||
|
|
||||||
server, logbuf := setupSmtpServer(mds)
|
server, logbuf := setupSMTPServer(mds)
|
||||||
defer teardownSmtpServer(server)
|
defer teardownSMTPServer(server)
|
||||||
|
|
||||||
var script []scriptStep
|
var script []scriptStep
|
||||||
pipe := setupSmtpSession(server)
|
pipe := setupSMTPSession(server)
|
||||||
c := textproto.NewConn(pipe)
|
c := textproto.NewConn(pipe)
|
||||||
|
|
||||||
// Get us into DATA state
|
// Get us into DATA state
|
||||||
@@ -294,8 +293,8 @@ Subject: test
|
|||||||
Hi!
|
Hi!
|
||||||
`
|
`
|
||||||
dw := c.DotWriter()
|
dw := c.DotWriter()
|
||||||
io.WriteString(dw, body)
|
_, _ = io.WriteString(dw, body)
|
||||||
dw.Close()
|
_ = dw.Close()
|
||||||
if code, _, err := c.ReadCodeLine(250); err != nil {
|
if code, _, err := c.ReadCodeLine(250); err != nil {
|
||||||
t.Errorf("Expected a 250 greeting, got %v", code)
|
t.Errorf("Expected a 250 greeting, got %v", code)
|
||||||
}
|
}
|
||||||
@@ -304,13 +303,13 @@ Hi!
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// playSession creates a new session, reads the greeting and then plays the script
|
// playSession creates a new session, reads the greeting and then plays the script
|
||||||
func playSession(t *testing.T, server *Server, script []scriptStep) error {
|
func playSession(t *testing.T, server *Server, script []scriptStep) error {
|
||||||
pipe := setupSmtpSession(server)
|
pipe := setupSMTPSession(server)
|
||||||
c := textproto.NewConn(pipe)
|
c := textproto.NewConn(pipe)
|
||||||
|
|
||||||
if code, _, err := c.ReadCodeLine(220); err != nil {
|
if code, _, err := c.ReadCodeLine(220); err != nil {
|
||||||
@@ -319,8 +318,10 @@ func playSession(t *testing.T, server *Server, script []scriptStep) error {
|
|||||||
|
|
||||||
err := playScriptAgainst(t, c, script)
|
err := playScriptAgainst(t, c, script)
|
||||||
|
|
||||||
c.Cmd("QUIT")
|
// Not all tests leave the session in a clean state, so the following two
|
||||||
c.ReadCodeLine(221)
|
// calls can fail
|
||||||
|
_, _ = c.Cmd("QUIT")
|
||||||
|
_, _, _ = c.ReadCodeLine(221)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -358,11 +359,11 @@ func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
|||||||
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
func setupSmtpServer(ds DataStore) (*Server, *bytes.Buffer) {
|
func setupSMTPServer(ds DataStore) (*Server, *bytes.Buffer) {
|
||||||
// Test Server Config
|
// Test Server Config
|
||||||
cfg := config.SmtpConfig{
|
cfg := config.SMTPConfig{
|
||||||
Ip4address: net.IPv4(127, 0, 0, 1),
|
IP4address: net.IPv4(127, 0, 0, 1),
|
||||||
Ip4port: 2500,
|
IP4port: 2500,
|
||||||
Domain: "inbucket.local",
|
Domain: "inbucket.local",
|
||||||
DomainNoStore: "bitbucket.local",
|
DomainNoStore: "bitbucket.local",
|
||||||
MaxRecipients: 5,
|
MaxRecipients: 5,
|
||||||
@@ -376,12 +377,12 @@ func setupSmtpServer(ds DataStore) (*Server, *bytes.Buffer) {
|
|||||||
log.SetOutput(buf)
|
log.SetOutput(buf)
|
||||||
|
|
||||||
// Create a server, don't start it
|
// Create a server, don't start it
|
||||||
return NewSmtpServer(cfg, ds), buf
|
return NewServer(cfg, ds), buf
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionNum int
|
var sessionNum int
|
||||||
|
|
||||||
func setupSmtpSession(server *Server) net.Conn {
|
func setupSMTPSession(server *Server) net.Conn {
|
||||||
// Pair of pipes to communicate
|
// Pair of pipes to communicate
|
||||||
serverConn, clientConn := net.Pipe()
|
serverConn, clientConn := net.Pipe()
|
||||||
// Start the session
|
// Start the session
|
||||||
@@ -392,6 +393,6 @@ func setupSmtpSession(server *Server) net.Conn {
|
|||||||
return clientConn
|
return clientConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func teardownSmtpServer(server *Server) {
|
func teardownSMTPServer(server *Server) {
|
||||||
//log.SetOutput(os.Stderr)
|
//log.SetOutput(os.Stderr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/log"
|
"github.com/jhillyerd/inbucket/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Real server code starts here
|
// Server holds the configuration and state of our SMTP server
|
||||||
type Server struct {
|
type Server struct {
|
||||||
domain string
|
domain string
|
||||||
domainNoStore string
|
domainNoStore string
|
||||||
@@ -46,37 +46,37 @@ var expConnectsHist = new(expvar.String)
|
|||||||
var expErrorsHist = new(expvar.String)
|
var expErrorsHist = new(expvar.String)
|
||||||
var expWarnsHist = new(expvar.String)
|
var expWarnsHist = new(expvar.String)
|
||||||
|
|
||||||
// Init a new Server object
|
// NewServer creates a new Server instance with the specificed config
|
||||||
func NewSmtpServer(cfg config.SmtpConfig, ds DataStore) *Server {
|
func NewServer(cfg config.SMTPConfig, ds DataStore) *Server {
|
||||||
return &Server{dataStore: ds, domain: cfg.Domain, maxRecips: cfg.MaxRecipients,
|
return &Server{dataStore: ds, domain: cfg.Domain, maxRecips: cfg.MaxRecipients,
|
||||||
maxIdleSeconds: cfg.MaxIdleSeconds, maxMessageBytes: cfg.MaxMessageBytes,
|
maxIdleSeconds: cfg.MaxIdleSeconds, maxMessageBytes: cfg.MaxMessageBytes,
|
||||||
storeMessages: cfg.StoreMessages, domainNoStore: strings.ToLower(cfg.DomainNoStore),
|
storeMessages: cfg.StoreMessages, domainNoStore: strings.ToLower(cfg.DomainNoStore),
|
||||||
waitgroup: new(sync.WaitGroup)}
|
waitgroup: new(sync.WaitGroup)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main listener loop
|
// Start the listener and handle incoming connections
|
||||||
func (s *Server) Start() {
|
func (s *Server) Start() {
|
||||||
cfg := config.GetSmtpConfig()
|
cfg := config.GetSMTPConfig()
|
||||||
addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v",
|
addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v",
|
||||||
cfg.Ip4address, cfg.Ip4port))
|
cfg.IP4address, cfg.IP4port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to build tcp4 address: %v", err)
|
log.Errorf("Failed to build tcp4 address: %v", err)
|
||||||
// TODO More graceful early-shutdown procedure
|
// TODO More graceful early-shutdown procedure
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("SMTP listening on TCP4 %v", addr)
|
log.Infof("SMTP listening on TCP4 %v", addr)
|
||||||
s.listener, err = net.ListenTCP("tcp4", addr)
|
s.listener, err = net.ListenTCP("tcp4", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("SMTP failed to start tcp4 listener: %v", err)
|
log.Errorf("SMTP failed to start tcp4 listener: %v", err)
|
||||||
// TODO More graceful early-shutdown procedure
|
// TODO More graceful early-shutdown procedure
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.storeMessages {
|
if !s.storeMessages {
|
||||||
log.LogInfo("Load test mode active, messages will not be stored")
|
log.Infof("Load test mode active, messages will not be stored")
|
||||||
} else if s.domainNoStore != "" {
|
} else if s.domainNoStore != "" {
|
||||||
log.LogInfo("Messages sent to domain '%v' will be discarded", s.domainNoStore)
|
log.Infof("Messages sent to domain '%v' will be discarded", s.domainNoStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start retention scanner
|
// Start retention scanner
|
||||||
@@ -96,12 +96,12 @@ func (s *Server) Start() {
|
|||||||
if max := 1 * time.Second; tempDelay > max {
|
if max := 1 * time.Second; tempDelay > max {
|
||||||
tempDelay = max
|
tempDelay = max
|
||||||
}
|
}
|
||||||
log.LogError("SMTP accept error: %v; retrying in %v", err, tempDelay)
|
log.Errorf("SMTP accept error: %v; retrying in %v", err, tempDelay)
|
||||||
time.Sleep(tempDelay)
|
time.Sleep(tempDelay)
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
if s.shutdown {
|
if s.shutdown {
|
||||||
log.LogTrace("SMTP listener shutting down on request")
|
log.Tracef("SMTP listener shutting down on request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO Implement a max error counter before shutdown?
|
// TODO Implement a max error counter before shutdown?
|
||||||
@@ -119,15 +119,17 @@ func (s *Server) Start() {
|
|||||||
|
|
||||||
// Stop requests the SMTP server closes it's listener
|
// Stop requests the SMTP server closes it's listener
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
log.LogTrace("SMTP shutdown requested, connections will be drained")
|
log.Tracef("SMTP shutdown requested, connections will be drained")
|
||||||
s.shutdown = true
|
s.shutdown = true
|
||||||
s.listener.Close()
|
if err := s.listener.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close SMTP listener: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain causes the caller to block until all active SMTP sessions have finished
|
// Drain causes the caller to block until all active SMTP sessions have finished
|
||||||
func (s *Server) Drain() {
|
func (s *Server) Drain() {
|
||||||
s.waitgroup.Wait()
|
s.waitgroup.Wait()
|
||||||
log.LogTrace("SMTP connections drained")
|
log.Tracef("SMTP connections drained")
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the provided Ticker ticks, we update our metrics history
|
// When the provided Ticker ticks, we update our metrics history
|
||||||
|
|||||||
@@ -21,20 +21,22 @@ var expRetainedCurrent = new(expvar.Int)
|
|||||||
var retentionDeletesHist = list.New()
|
var retentionDeletesHist = list.New()
|
||||||
var retainedHist = list.New()
|
var retainedHist = list.New()
|
||||||
|
|
||||||
// History rendered as comma delim string
|
// History rendered as comma delimited string
|
||||||
var expRetentionDeletesHist = new(expvar.String)
|
var expRetentionDeletesHist = new(expvar.String)
|
||||||
var expRetainedHist = new(expvar.String)
|
var expRetainedHist = new(expvar.String)
|
||||||
|
|
||||||
|
// StartRetentionScanner launches a go-routine that scans for expired
|
||||||
|
// messages, following the configured interval
|
||||||
func StartRetentionScanner(ds DataStore) {
|
func StartRetentionScanner(ds DataStore) {
|
||||||
cfg := config.GetDataStoreConfig()
|
cfg := config.GetDataStoreConfig()
|
||||||
expRetentionPeriod.Set(int64(cfg.RetentionMinutes * 60))
|
expRetentionPeriod.Set(int64(cfg.RetentionMinutes * 60))
|
||||||
if cfg.RetentionMinutes > 0 {
|
if cfg.RetentionMinutes > 0 {
|
||||||
// Retention scanning enabled
|
// Retention scanning enabled
|
||||||
log.LogInfo("Retention configured for %v minutes", cfg.RetentionMinutes)
|
log.Infof("Retention configured for %v minutes", cfg.RetentionMinutes)
|
||||||
go retentionScanner(ds, time.Duration(cfg.RetentionMinutes)*time.Minute,
|
go retentionScanner(ds, time.Duration(cfg.RetentionMinutes)*time.Minute,
|
||||||
time.Duration(cfg.RetentionSleep)*time.Millisecond)
|
time.Duration(cfg.RetentionSleep)*time.Millisecond)
|
||||||
} else {
|
} else {
|
||||||
log.LogInfo("Retention scanner disabled")
|
log.Infof("Retention scanner disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,21 +47,21 @@ func retentionScanner(ds DataStore, maxAge time.Duration, sleep time.Duration) {
|
|||||||
since := time.Since(start)
|
since := time.Since(start)
|
||||||
if since < time.Minute {
|
if since < time.Minute {
|
||||||
dur := time.Minute - since
|
dur := time.Minute - since
|
||||||
log.LogTrace("Retention scanner sleeping for %v", dur)
|
log.Tracef("Retention scanner sleeping for %v", dur)
|
||||||
time.Sleep(dur)
|
time.Sleep(dur)
|
||||||
}
|
}
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
|
|
||||||
// Kickoff scan
|
// Kickoff scan
|
||||||
if err := doRetentionScan(ds, maxAge, sleep); err != nil {
|
if err := doRetentionScan(ds, maxAge, sleep); err != nil {
|
||||||
log.LogError("Error during retention scan: %v", err)
|
log.Errorf("Error during retention scan: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doRetentionScan does a single pass of all mailboxes looking for messages that can be purged
|
// doRetentionScan does a single pass of all mailboxes looking for messages that can be purged
|
||||||
func doRetentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) error {
|
func doRetentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) error {
|
||||||
log.LogTrace("Starting retention scan")
|
log.Tracef("Starting retention scan")
|
||||||
cutoff := time.Now().Add(-1 * maxAge)
|
cutoff := time.Now().Add(-1 * maxAge)
|
||||||
mboxes, err := ds.AllMailboxes()
|
mboxes, err := ds.AllMailboxes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,11 +76,11 @@ func doRetentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) er
|
|||||||
}
|
}
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
if msg.Date().Before(cutoff) {
|
if msg.Date().Before(cutoff) {
|
||||||
log.LogTrace("Purging expired message %v", msg.Id())
|
log.Tracef("Purging expired message %v", msg.ID())
|
||||||
err = msg.Delete()
|
err = msg.Delete()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log but don't abort
|
// Log but don't abort
|
||||||
log.LogError("Failed to purge message %v: %v", msg.Id(), err)
|
log.Errorf("Failed to purge message %v: %v", msg.ID(), err)
|
||||||
} else {
|
} else {
|
||||||
expRetentionDeletesTotal.Add(1)
|
expRetentionDeletesTotal.Add(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ func TestDoRetentionScan(t *testing.T) {
|
|||||||
mb3.On("GetMessages").Return([]Message{new3}, nil)
|
mb3.On("GetMessages").Return([]Message{new3}, nil)
|
||||||
|
|
||||||
// Test 4 hour retention
|
// Test 4 hour retention
|
||||||
doRetentionScan(mds, 4*time.Hour, 0)
|
if err := doRetentionScan(mds, 4*time.Hour, 0); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Check our assertions
|
// Check our assertions
|
||||||
mds.AssertExpectations(t)
|
mds.AssertExpectations(t)
|
||||||
@@ -58,7 +60,7 @@ func TestDoRetentionScan(t *testing.T) {
|
|||||||
// Make a MockMessage of a specific age
|
// Make a MockMessage of a specific age
|
||||||
func mockMessage(ageHours int) *MockMessage {
|
func mockMessage(ageHours int) *MockMessage {
|
||||||
msg := &MockMessage{}
|
msg := &MockMessage{}
|
||||||
msg.On("Id").Return(fmt.Sprintf("MSG[age=%vh]", ageHours))
|
msg.On("ID").Return(fmt.Sprintf("MSG[age=%vh]", ageHours))
|
||||||
msg.On("Date").Return(time.Now().Add(time.Duration(ageHours*-1) * time.Hour))
|
msg.On("Date").Return(time.Now().Add(time.Duration(ageHours*-1) * time.Hour))
|
||||||
msg.On("Delete").Return(nil)
|
msg.On("Delete").Return(nil)
|
||||||
return msg
|
return msg
|
||||||
@@ -114,7 +116,7 @@ type MockMessage struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMessage) Id() string {
|
func (m *MockMessage) ID() string {
|
||||||
args := m.Called()
|
args := m.Called()
|
||||||
return args.String(0)
|
return args.String(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Take "user+ext" and return "user", aka the mailbox we'll store it in
|
// ParseMailboxName takes a localPart string (ex: "user+ext" without "@domain")
|
||||||
// Return error if it contains invalid characters, we don't accept anything
|
// and returns just the mailbox name (ex: "user"). Returns an error if
|
||||||
// that must be quoted according to RFC3696.
|
// localPart contains invalid characters; it won't accept any that must be
|
||||||
|
// quoted according to RFC3696.
|
||||||
func ParseMailboxName(localPart string) (result string, err error) {
|
func ParseMailboxName(localPart string) (result string, err error) {
|
||||||
if localPart == "" {
|
if localPart == "" {
|
||||||
return "", fmt.Errorf("Mailbox name cannot be empty")
|
return "", fmt.Errorf("Mailbox name cannot be empty")
|
||||||
@@ -41,10 +42,14 @@ func ParseMailboxName(localPart string) (result string, err error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take a mailbox name and hash it into the directory we'll store it in
|
// HashMailboxName accepts a mailbox name and hashes it. Inbucket uses this as
|
||||||
|
// the directory to house the mailbox
|
||||||
func HashMailboxName(mailbox string) string {
|
func HashMailboxName(mailbox string) string {
|
||||||
h := sha1.New()
|
h := sha1.New()
|
||||||
io.WriteString(h, mailbox)
|
if _, err := io.WriteString(h, mailbox); err != nil {
|
||||||
|
// This shouldn't ever happen
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,15 +143,15 @@ LOOP:
|
|||||||
switch {
|
switch {
|
||||||
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
|
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
|
||||||
// Letters are OK
|
// Letters are OK
|
||||||
buf.WriteByte(c)
|
_ = buf.WriteByte(c)
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
case '0' <= c && c <= '9':
|
case '0' <= c && c <= '9':
|
||||||
// Numbers are OK
|
// Numbers are OK
|
||||||
buf.WriteByte(c)
|
_ = buf.WriteByte(c)
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0:
|
case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0:
|
||||||
// These specials can be used unquoted
|
// These specials can be used unquoted
|
||||||
buf.WriteByte(c)
|
_ = buf.WriteByte(c)
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
case c == '.':
|
case c == '.':
|
||||||
// A single period is OK
|
// A single period is OK
|
||||||
@@ -154,13 +159,13 @@ LOOP:
|
|||||||
// Sequence of periods is not permitted
|
// Sequence of periods is not permitted
|
||||||
return "", "", fmt.Errorf("Sequence of periods is not permitted")
|
return "", "", fmt.Errorf("Sequence of periods is not permitted")
|
||||||
}
|
}
|
||||||
buf.WriteByte(c)
|
_ = buf.WriteByte(c)
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
case c == '\\':
|
case c == '\\':
|
||||||
inCharQuote = true
|
inCharQuote = true
|
||||||
case c == '"':
|
case c == '"':
|
||||||
if inCharQuote {
|
if inCharQuote {
|
||||||
buf.WriteByte(c)
|
_ = buf.WriteByte(c)
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
} else if inStringQuote {
|
} else if inStringQuote {
|
||||||
inStringQuote = false
|
inStringQuote = false
|
||||||
@@ -173,7 +178,7 @@ LOOP:
|
|||||||
}
|
}
|
||||||
case c == '@':
|
case c == '@':
|
||||||
if inCharQuote || inStringQuote {
|
if inCharQuote || inStringQuote {
|
||||||
buf.WriteByte(c)
|
_ = buf.WriteByte(c)
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
} else {
|
} else {
|
||||||
// End of local-part
|
// End of local-part
|
||||||
@@ -190,7 +195,7 @@ LOOP:
|
|||||||
return "", "", fmt.Errorf("Characters outside of US-ASCII range not permitted")
|
return "", "", fmt.Errorf("Characters outside of US-ASCII range not permitted")
|
||||||
default:
|
default:
|
||||||
if inCharQuote || inStringQuote {
|
if inCharQuote || inStringQuote {
|
||||||
buf.WriteByte(c)
|
_ = buf.WriteByte(c)
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
} else {
|
} else {
|
||||||
return "", "", fmt.Errorf("Character %q must be quoted", c)
|
return "", "", fmt.Errorf("Character %q must be quoted", c)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{$name := .name}}
|
{{$name := .name}}
|
||||||
{{range .messages}}
|
{{range .messages}}
|
||||||
<button id="{{.Id}}" type="button" class="listEntry list-group-item">
|
<button id="{{.ID}}" type="button" class="listEntry list-group-item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-4 col-md-12 text-primary">{{.Subject}}</div>
|
<div class="col-sm-4 col-md-12 text-primary">{{.Subject}}</div>
|
||||||
<div class="col-sm-4 col-md-12 small">{{.From}}</div>
|
<div class="col-sm-4 col-md-12 small">{{.From}}</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{$name := .name}}
|
{{$name := .name}}
|
||||||
{{$id := .message.Id}}
|
{{$id := .message.ID}}
|
||||||
<div class="btn-group btn-group-sm message-controls" role="group" aria-label="Message Controls">
|
<div class="btn-group btn-group-sm message-controls" role="group" aria-label="Message Controls">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
@@ -9,20 +9,20 @@
|
|||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
onClick="deleteMessage('{{.message.Id}}');">
|
onClick="deleteMessage('{{.message.ID}}');">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
onClick="messageSource('{{.message.Id}}');">
|
onClick="messageSource('{{.message.ID}}');">
|
||||||
<span class="glyphicon glyphicon-education" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-education" aria-hidden="true"></span>
|
||||||
Source
|
Source
|
||||||
</button>
|
</button>
|
||||||
{{if .htmlAvailable}}
|
{{if .htmlAvailable}}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
onClick="htmlView('{{.message.Id}}');">
|
onClick="htmlView('{{.message.ID}}');">
|
||||||
<span class="glyphicon glyphicon-new-window" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-new-window" aria-hidden="true"></span>
|
||||||
HTML
|
HTML
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{$name := .name}}
|
{{$name := .name}}
|
||||||
{{range .messages}}
|
{{range .messages}}
|
||||||
<div class="box listEntry" id="{{.Id}}">
|
<div class="box listEntry" id="{{.ID}}">
|
||||||
<div class="subject">{{.Subject}}</div>
|
<div class="subject">{{.Subject}}</div>
|
||||||
<div class="from">{{.From}}</div>
|
<div class="from">{{.From}}</div>
|
||||||
<div class="date">{{friendlyTime .Date}}</div>
|
<div class="date">{{friendlyTime .Date}}</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{{$name := .name}}
|
{{$name := .name}}
|
||||||
{{$id := .message.Id}}
|
{{$id := .message.ID}}
|
||||||
<div id="emailActions">
|
<div id="emailActions">
|
||||||
<a href="/link/{{$name}}/{{$id}}">Link</a>
|
<a href="/link/{{$name}}/{{$id}}">Link</a>
|
||||||
<a href="javascript:deleteMessage('{{.message.Id}}');">Delete</a>
|
<a href="javascript:deleteMessage('{{.message.ID}}');">Delete</a>
|
||||||
<a href="javascript:messageSource('{{.message.Id}}');">Source</a>
|
<a href="javascript:messageSource('{{.message.ID}}');">Source</a>
|
||||||
{{if .htmlAvailable}}
|
{{if .htmlAvailable}}
|
||||||
<a href="javascript:htmlView('{{.message.Id}}');">HTML</a>
|
<a href="javascript:htmlView('{{.message.ID}}');">HTML</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<table id="emailHeader">
|
<table id="emailHeader">
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/smtpd"
|
"github.com/jhillyerd/inbucket/smtpd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Context is passed into every request handler function
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Vars map[string]string
|
Vars map[string]string
|
||||||
Session *sessions.Session
|
Session *sessions.Session
|
||||||
DataStore smtpd.DataStore
|
DataStore smtpd.DataStore
|
||||||
IsJson bool
|
IsJSON bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the Context (currently does nothing)
|
||||||
func (c *Context) Close() {
|
func (c *Context) Close() {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,7 @@ func headerMatch(req *http.Request, name string, value string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewContext returns a Context for the given HTTP Request
|
||||||
func NewContext(req *http.Request) (*Context, error) {
|
func NewContext(req *http.Request) (*Context, error) {
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
sess, err := sessionStore.Get(req, "inbucket")
|
sess, err := sessionStore.Get(req, "inbucket")
|
||||||
@@ -44,7 +47,7 @@ func NewContext(req *http.Request) (*Context, error) {
|
|||||||
Vars: vars,
|
Vars: vars,
|
||||||
Session: sess,
|
Session: sess,
|
||||||
DataStore: DataStore,
|
DataStore: DataStore,
|
||||||
IsJson: headerMatch(req, "Accept", "application/json"),
|
IsJSON: headerMatch(req, "Accept", "application/json"),
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, err
|
return ctx, err
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/log"
|
"github.com/jhillyerd/inbucket/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TemplateFuncs declares functions made available to all templates (including partials)
|
||||||
var TemplateFuncs = template.FuncMap{
|
var TemplateFuncs = template.FuncMap{
|
||||||
"friendlyTime": friendlyTime,
|
"friendlyTime": friendlyTime,
|
||||||
"reverse": reverse,
|
"reverse": reverse,
|
||||||
"textToHtml": textToHtml,
|
"textToHtml": textToHTML,
|
||||||
}
|
}
|
||||||
|
|
||||||
// From http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
// From http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
||||||
@@ -40,7 +41,7 @@ func reverse(name string, things ...interface{}) string {
|
|||||||
// Grab the route
|
// Grab the route
|
||||||
u, err := Router.Get(name).URL(strs...)
|
u, err := Router.Get(name).URL(strs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to reverse route: %v", err)
|
log.Errorf("Failed to reverse route: %v", err)
|
||||||
return "/ROUTE-ERROR"
|
return "/ROUTE-ERROR"
|
||||||
}
|
}
|
||||||
return u.Path
|
return u.Path
|
||||||
@@ -48,15 +49,15 @@ func reverse(name string, things ...interface{}) string {
|
|||||||
|
|
||||||
// textToHtml takes plain text, escapes it and tries to pretty it up for
|
// textToHtml takes plain text, escapes it and tries to pretty it up for
|
||||||
// HTML display
|
// HTML display
|
||||||
func textToHtml(text string) template.HTML {
|
func textToHTML(text string) template.HTML {
|
||||||
text = html.EscapeString(text)
|
text = html.EscapeString(text)
|
||||||
text = urlRE.ReplaceAllStringFunc(text, wrapUrl)
|
text = urlRE.ReplaceAllStringFunc(text, wrapURL)
|
||||||
replacer := strings.NewReplacer("\r\n", "<br/>\n", "\r", "<br/>\n", "\n", "<br/>\n")
|
replacer := strings.NewReplacer("\r\n", "<br/>\n", "\r", "<br/>\n", "\n", "<br/>\n")
|
||||||
return template.HTML(replacer.Replace(text))
|
return template.HTML(replacer.Replace(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapUrl wraps a <a href> tag around the provided URL
|
// wrapUrl wraps a <a href> tag around the provided URL
|
||||||
func wrapUrl(url string) string {
|
func wrapURL(url string) string {
|
||||||
unescaped := strings.Replace(url, "&", "&", -1)
|
unescaped := strings.Replace(url, "&", "&", -1)
|
||||||
return fmt.Sprintf("<a href=\"%s\" target=\"_blank\">%s</a>", unescaped, url)
|
return fmt.Sprintf("<a href=\"%s\" target=\"_blank\">%s</a>", unescaped, url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,22 +9,22 @@ import (
|
|||||||
|
|
||||||
func TestTextToHtml(t *testing.T) {
|
func TestTextToHtml(t *testing.T) {
|
||||||
// Identity
|
// Identity
|
||||||
assert.Equal(t, textToHtml("html"), template.HTML("html"))
|
assert.Equal(t, textToHTML("html"), template.HTML("html"))
|
||||||
|
|
||||||
// Check it escapes
|
// Check it escapes
|
||||||
assert.Equal(t, textToHtml("<html>"), template.HTML("<html>"))
|
assert.Equal(t, textToHTML("<html>"), template.HTML("<html>"))
|
||||||
|
|
||||||
// Check for linebreaks
|
// Check for linebreaks
|
||||||
assert.Equal(t, textToHtml("line\nbreak"), template.HTML("line<br/>\nbreak"))
|
assert.Equal(t, textToHTML("line\nbreak"), template.HTML("line<br/>\nbreak"))
|
||||||
assert.Equal(t, textToHtml("line\r\nbreak"), template.HTML("line<br/>\nbreak"))
|
assert.Equal(t, textToHTML("line\r\nbreak"), template.HTML("line<br/>\nbreak"))
|
||||||
assert.Equal(t, textToHtml("line\rbreak"), template.HTML("line<br/>\nbreak"))
|
assert.Equal(t, textToHTML("line\rbreak"), template.HTML("line<br/>\nbreak"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestURLDetection(t *testing.T) {
|
func TestURLDetection(t *testing.T) {
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
textToHtml("http://google.com/"),
|
textToHTML("http://google.com/"),
|
||||||
template.HTML("<a href=\"http://google.com/\" target=\"_blank\">http://google.com/</a>"))
|
template.HTML("<a href=\"http://google.com/\" target=\"_blank\">http://google.com/</a>"))
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
textToHtml("http://a.com/?q=a&n=v"),
|
textToHTML("http://a.com/?q=a&n=v"),
|
||||||
template.HTML("<a href=\"http://a.com/?q=a&n=v\" target=\"_blank\">http://a.com/?q=a&n=v</a>"))
|
template.HTML("<a href=\"http://a.com/?q=a&n=v\" target=\"_blank\">http://a.com/?q=a&n=v</a>"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,24 +13,35 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/smtpd"
|
"github.com/jhillyerd/inbucket/smtpd"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JsonMessageHeader struct {
|
// JSONMessageHeader contains the basic header data for a message
|
||||||
Mailbox, Id, From, Subject string
|
type JSONMessageHeader struct {
|
||||||
|
Mailbox string
|
||||||
|
ID string `json:"Id"`
|
||||||
|
From string
|
||||||
|
Subject string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Size int64
|
Size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type JsonMessage struct {
|
// JSONMessage contains the same data as the header plus a JSONMessageBody
|
||||||
Mailbox, Id, From, Subject string
|
type JSONMessage struct {
|
||||||
|
Mailbox string
|
||||||
|
ID string `json:"Id"`
|
||||||
|
From string
|
||||||
|
Subject string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Size int64
|
Size int64
|
||||||
Body *JsonMessageBody
|
Body *JSONMessageBody
|
||||||
Header mail.Header
|
Header mail.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
type JsonMessageBody struct {
|
// JSONMessageBody contains the Text and HTML versions of the message body
|
||||||
Text, Html string
|
type JSONMessageBody struct {
|
||||||
|
Text string
|
||||||
|
HTML string `json:"Html"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxIndex renders the index page for a particular mailbox
|
||||||
func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Form values must be validated manually
|
// Form values must be validated manually
|
||||||
name := req.FormValue("name")
|
name := req.FormValue("name")
|
||||||
@@ -59,6 +70,7 @@ func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err e
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxLink handles pretty links to a particular message
|
||||||
func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
id := ctx.Vars["id"]
|
id := ctx.Vars["id"]
|
||||||
@@ -74,6 +86,7 @@ func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxList renders a list of messages in a mailbox
|
||||||
func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||||
@@ -88,21 +101,21 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to get messages for %v: %v", name, err)
|
return fmt.Errorf("Failed to get messages for %v: %v", name, err)
|
||||||
}
|
}
|
||||||
log.LogTrace("Got %v messsages", len(messages))
|
log.Tracef("Got %v messsages", len(messages))
|
||||||
|
|
||||||
if ctx.IsJson {
|
if ctx.IsJSON {
|
||||||
jmessages := make([]*JsonMessageHeader, len(messages))
|
jmessages := make([]*JSONMessageHeader, len(messages))
|
||||||
for i, msg := range messages {
|
for i, msg := range messages {
|
||||||
jmessages[i] = &JsonMessageHeader{
|
jmessages[i] = &JSONMessageHeader{
|
||||||
Mailbox: name,
|
Mailbox: name,
|
||||||
Id: msg.Id(),
|
ID: msg.ID(),
|
||||||
From: msg.From(),
|
From: msg.From(),
|
||||||
Subject: msg.Subject(),
|
Subject: msg.Subject(),
|
||||||
Date: msg.Date(),
|
Date: msg.Date(),
|
||||||
Size: msg.Size(),
|
Size: msg.Size(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RenderJson(w, jmessages)
|
return RenderJSON(w, jmessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
return RenderPartial("mailbox/_list.html", w, map[string]interface{}{
|
return RenderPartial("mailbox/_list.html", w, map[string]interface{}{
|
||||||
@@ -112,6 +125,7 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxShow renders a particular message from a mailbox
|
||||||
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
id := ctx.Vars["id"]
|
id := ctx.Vars["id"]
|
||||||
@@ -140,24 +154,24 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
|||||||
return fmt.Errorf("ReadBody() failed: %v", err)
|
return fmt.Errorf("ReadBody() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsJson {
|
if ctx.IsJSON {
|
||||||
return RenderJson(w,
|
return RenderJSON(w,
|
||||||
&JsonMessage{
|
&JSONMessage{
|
||||||
Mailbox: name,
|
Mailbox: name,
|
||||||
Id: msg.Id(),
|
ID: msg.ID(),
|
||||||
From: msg.From(),
|
From: msg.From(),
|
||||||
Subject: msg.Subject(),
|
Subject: msg.Subject(),
|
||||||
Date: msg.Date(),
|
Date: msg.Date(),
|
||||||
Size: msg.Size(),
|
Size: msg.Size(),
|
||||||
Header: header.Header,
|
Header: header.Header,
|
||||||
Body: &JsonMessageBody{
|
Body: &JSONMessageBody{
|
||||||
Text: mime.Text,
|
Text: mime.Text,
|
||||||
Html: mime.Html,
|
HTML: mime.Html,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
body := template.HTML(textToHtml(mime.Text))
|
body := template.HTML(textToHTML(mime.Text))
|
||||||
htmlAvailable := mime.Html != ""
|
htmlAvailable := mime.Html != ""
|
||||||
|
|
||||||
return RenderPartial("mailbox/_show.html", w, map[string]interface{}{
|
return RenderPartial("mailbox/_show.html", w, map[string]interface{}{
|
||||||
@@ -170,6 +184,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxPurge deletes all messages from a mailbox
|
||||||
func MailboxPurge(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxPurge(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||||
@@ -183,18 +198,21 @@ func MailboxPurge(w http.ResponseWriter, req *http.Request, ctx *Context) (err e
|
|||||||
if err := mb.Purge(); err != nil {
|
if err := mb.Purge(); err != nil {
|
||||||
return fmt.Errorf("Mailbox(%q) Purge: %v", name, err)
|
return fmt.Errorf("Mailbox(%q) Purge: %v", name, err)
|
||||||
}
|
}
|
||||||
log.LogTrace("Purged mailbox for %q", name)
|
log.Tracef("Purged mailbox for %q", name)
|
||||||
|
|
||||||
if ctx.IsJson {
|
if ctx.IsJSON {
|
||||||
return RenderJson(w, "OK")
|
return RenderJSON(w, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
io.WriteString(w, "OK")
|
if _, err := io.WriteString(w, "OK"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
// MailboxHTML displays the HTML content of a message
|
||||||
|
func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
id := ctx.Vars["id"]
|
id := ctx.Vars["id"]
|
||||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||||
@@ -224,6 +242,7 @@ func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxSource displays the raw source of a message, including headers
|
||||||
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
id := ctx.Vars["id"]
|
id := ctx.Vars["id"]
|
||||||
@@ -245,10 +264,14 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
io.WriteString(w, *raw)
|
if _, err := io.WriteString(w, *raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxDownloadAttach sends the attachment to the client; disposition:
|
||||||
|
// attachment, type: application/octet-stream
|
||||||
func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
id := ctx.Vars["id"]
|
id := ctx.Vars["id"]
|
||||||
@@ -287,10 +310,13 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *Contex
|
|||||||
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
w.Header().Set("Content-Disposition", "attachment")
|
w.Header().Set("Content-Disposition", "attachment")
|
||||||
w.Write(part.Content())
|
if _, err := w.Write(part.Content()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxViewAttach sends the attachment to the client for online viewing
|
||||||
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||||
@@ -328,10 +354,13 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (
|
|||||||
part := body.Attachments[num]
|
part := body.Attachments[num]
|
||||||
|
|
||||||
w.Header().Set("Content-Type", part.ContentType())
|
w.Header().Set("Content-Type", part.ContentType())
|
||||||
w.Write(part.Content())
|
if _, err := w.Write(part.Content()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxDelete removes a particular message from a mailbox
|
||||||
func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
id := ctx.Vars["id"]
|
id := ctx.Vars["id"]
|
||||||
@@ -352,11 +381,13 @@ func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsJson {
|
if ctx.IsJSON {
|
||||||
return RenderJson(w, "OK")
|
return RenderJSON(w, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
io.WriteString(w, "OK")
|
if _, err := io.WriteString(w, "OK"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderJson(w http.ResponseWriter, data interface{}) error {
|
// RenderJSON sets the correct HTTP headers for JSON, then writes the specified
|
||||||
|
// data (typically a struct) encoded in JSON
|
||||||
|
func RenderJSON(w http.ResponseWriter, data interface{}) error {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.Header().Set("Expires", "-1")
|
w.Header().Set("Expires", "-1")
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
|
|||||||
@@ -19,31 +19,42 @@ import (
|
|||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OutputJsonHeader struct {
|
// OutputJSONHeader holds the received Header
|
||||||
Mailbox, Id, From, Subject, Date string
|
type OutputJSONHeader struct {
|
||||||
|
Mailbox string
|
||||||
|
ID string `json:"Id"`
|
||||||
|
From, Subject, Date string
|
||||||
Size int
|
Size int
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutputJsonMessage struct {
|
// OutputJSONMessage holds the received Message
|
||||||
Mailbox, Id, From, Subject, Date string
|
type OutputJSONMessage struct {
|
||||||
|
Mailbox string
|
||||||
|
ID string `json:"Id"`
|
||||||
|
From, Subject, Date string
|
||||||
Size int
|
Size int
|
||||||
Header map[string][]string
|
Header map[string][]string
|
||||||
Body struct {
|
Body struct {
|
||||||
Text, Html string
|
Text string
|
||||||
|
HTML string `json:"Html"`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InputMessageData holds the message we want to test
|
||||||
type InputMessageData struct {
|
type InputMessageData struct {
|
||||||
Mailbox, Id, From, Subject string
|
Mailbox string
|
||||||
|
ID string `json:"Id"`
|
||||||
|
From, Subject string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Size int
|
Size int
|
||||||
Header mail.Header
|
Header mail.Header
|
||||||
Html, Text string
|
HTML string `json:"Html"`
|
||||||
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *InputMessageData) MockMessage() *MockMessage {
|
func (d *InputMessageData) MockMessage() *MockMessage {
|
||||||
msg := &MockMessage{}
|
msg := &MockMessage{}
|
||||||
msg.On("Id").Return(d.Id)
|
msg.On("ID").Return(d.ID)
|
||||||
msg.On("From").Return(d.From)
|
msg.On("From").Return(d.From)
|
||||||
msg.On("Subject").Return(d.Subject)
|
msg.On("Subject").Return(d.Subject)
|
||||||
msg.On("Date").Return(d.Date)
|
msg.On("Date").Return(d.Date)
|
||||||
@@ -54,20 +65,20 @@ func (d *InputMessageData) MockMessage() *MockMessage {
|
|||||||
msg.On("ReadHeader").Return(gomsg, nil)
|
msg.On("ReadHeader").Return(gomsg, nil)
|
||||||
body := &enmime.MIMEBody{
|
body := &enmime.MIMEBody{
|
||||||
Text: d.Text,
|
Text: d.Text,
|
||||||
Html: d.Html,
|
Html: d.HTML,
|
||||||
}
|
}
|
||||||
msg.On("ReadBody").Return(body, nil)
|
msg.On("ReadBody").Return(body, nil)
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *InputMessageData) CompareToJsonHeader(j *OutputJsonHeader) (errors []string) {
|
func (d *InputMessageData) CompareToJSONHeader(j *OutputJSONHeader) (errors []string) {
|
||||||
if d.Mailbox != j.Mailbox {
|
if d.Mailbox != j.Mailbox {
|
||||||
errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox,
|
errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox,
|
||||||
j.Mailbox))
|
j.Mailbox))
|
||||||
}
|
}
|
||||||
if d.Id != j.Id {
|
if d.ID != j.ID {
|
||||||
errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.Id,
|
errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.ID,
|
||||||
j.Id))
|
j.ID))
|
||||||
}
|
}
|
||||||
if d.From != j.From {
|
if d.From != j.From {
|
||||||
errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From,
|
errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From,
|
||||||
@@ -90,14 +101,14 @@ func (d *InputMessageData) CompareToJsonHeader(j *OutputJsonHeader) (errors []st
|
|||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *InputMessageData) CompareToJsonMessage(j *OutputJsonMessage) (errors []string) {
|
func (d *InputMessageData) CompareToJSONMessage(j *OutputJSONMessage) (errors []string) {
|
||||||
if d.Mailbox != j.Mailbox {
|
if d.Mailbox != j.Mailbox {
|
||||||
errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox,
|
errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox,
|
||||||
j.Mailbox))
|
j.Mailbox))
|
||||||
}
|
}
|
||||||
if d.Id != j.Id {
|
if d.ID != j.ID {
|
||||||
errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.Id,
|
errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.ID,
|
||||||
j.Id))
|
j.ID))
|
||||||
}
|
}
|
||||||
if d.From != j.From {
|
if d.From != j.From {
|
||||||
errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From,
|
errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From,
|
||||||
@@ -120,9 +131,9 @@ func (d *InputMessageData) CompareToJsonMessage(j *OutputJsonMessage) (errors []
|
|||||||
errors = append(errors, fmt.Sprintf("Expected JSON.Text=%q, got %q", d.Text,
|
errors = append(errors, fmt.Sprintf("Expected JSON.Text=%q, got %q", d.Text,
|
||||||
j.Body.Text))
|
j.Body.Text))
|
||||||
}
|
}
|
||||||
if d.Html != j.Body.Html {
|
if d.HTML != j.Body.HTML {
|
||||||
errors = append(errors, fmt.Sprintf("Expected JSON.Html=%q, got %q", d.Html,
|
errors = append(errors, fmt.Sprintf("Expected JSON.Html=%q, got %q", d.HTML,
|
||||||
j.Body.Html))
|
j.Body.HTML))
|
||||||
}
|
}
|
||||||
for k, vals := range d.Header {
|
for k, vals := range d.Header {
|
||||||
jvals, ok := j.Header[k]
|
jvals, ok := j.Header[k]
|
||||||
@@ -191,7 +202,7 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test MailboxFor error
|
// Test MailboxFor error
|
||||||
@@ -211,14 +222,14 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
// Test JSON message headers
|
// Test JSON message headers
|
||||||
data1 := &InputMessageData{
|
data1 := &InputMessageData{
|
||||||
Mailbox: "good",
|
Mailbox: "good",
|
||||||
Id: "0001",
|
ID: "0001",
|
||||||
From: "from1",
|
From: "from1",
|
||||||
Subject: "subject 1",
|
Subject: "subject 1",
|
||||||
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)),
|
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)),
|
||||||
}
|
}
|
||||||
data2 := &InputMessageData{
|
data2 := &InputMessageData{
|
||||||
Mailbox: "good",
|
Mailbox: "good",
|
||||||
Id: "0002",
|
ID: "0002",
|
||||||
From: "from2",
|
From: "from2",
|
||||||
Subject: "subject 2",
|
Subject: "subject 2",
|
||||||
Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)),
|
Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)),
|
||||||
@@ -241,19 +252,19 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
|
|
||||||
// Check JSON
|
// Check JSON
|
||||||
dec := json.NewDecoder(w.Body)
|
dec := json.NewDecoder(w.Body)
|
||||||
var result []OutputJsonHeader
|
var result []OutputJSONHeader
|
||||||
if err := dec.Decode(&result); err != nil {
|
if err := dec.Decode(&result); err != nil {
|
||||||
t.Errorf("Failed to decode JSON: %v", err)
|
t.Errorf("Failed to decode JSON: %v", err)
|
||||||
}
|
}
|
||||||
if len(result) != 2 {
|
if len(result) != 2 {
|
||||||
t.Errorf("Expected 2 results, got %v", len(result))
|
t.Errorf("Expected 2 results, got %v", len(result))
|
||||||
}
|
}
|
||||||
if errors := data1.CompareToJsonHeader(&result[0]); len(errors) > 0 {
|
if errors := data1.CompareToJSONHeader(&result[0]); len(errors) > 0 {
|
||||||
for _, e := range errors {
|
for _, e := range errors {
|
||||||
t.Error(e)
|
t.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors := data2.CompareToJsonHeader(&result[1]); len(errors) > 0 {
|
if errors := data2.CompareToJSONHeader(&result[1]); len(errors) > 0 {
|
||||||
for _, e := range errors {
|
for _, e := range errors {
|
||||||
t.Error(e)
|
t.Error(e)
|
||||||
}
|
}
|
||||||
@@ -263,7 +274,7 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +322,7 @@ func TestRestMessage(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test GetMessage error
|
// Test GetMessage error
|
||||||
@@ -331,7 +342,7 @@ func TestRestMessage(t *testing.T) {
|
|||||||
// Test JSON message headers
|
// Test JSON message headers
|
||||||
data1 := &InputMessageData{
|
data1 := &InputMessageData{
|
||||||
Mailbox: "good",
|
Mailbox: "good",
|
||||||
Id: "0001",
|
ID: "0001",
|
||||||
From: "from1",
|
From: "from1",
|
||||||
Subject: "subject 1",
|
Subject: "subject 1",
|
||||||
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)),
|
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)),
|
||||||
@@ -339,7 +350,7 @@ func TestRestMessage(t *testing.T) {
|
|||||||
"To": []string{"fred@fish.com", "keyword@nsa.gov"},
|
"To": []string{"fred@fish.com", "keyword@nsa.gov"},
|
||||||
},
|
},
|
||||||
Text: "This is some text",
|
Text: "This is some text",
|
||||||
Html: "This is some HTML",
|
HTML: "This is some HTML",
|
||||||
}
|
}
|
||||||
goodbox := &MockMailbox{}
|
goodbox := &MockMailbox{}
|
||||||
ds.On("MailboxFor", "good").Return(goodbox, nil)
|
ds.On("MailboxFor", "good").Return(goodbox, nil)
|
||||||
@@ -358,11 +369,11 @@ func TestRestMessage(t *testing.T) {
|
|||||||
|
|
||||||
// Check JSON
|
// Check JSON
|
||||||
dec := json.NewDecoder(w.Body)
|
dec := json.NewDecoder(w.Body)
|
||||||
var result OutputJsonMessage
|
var result OutputJSONMessage
|
||||||
if err := dec.Decode(&result); err != nil {
|
if err := dec.Decode(&result); err != nil {
|
||||||
t.Errorf("Failed to decode JSON: %v", err)
|
t.Errorf("Failed to decode JSON: %v", err)
|
||||||
}
|
}
|
||||||
if errors := data1.CompareToJsonMessage(&result); len(errors) > 0 {
|
if errors := data1.CompareToJSONMessage(&result); len(errors) > 0 {
|
||||||
for _, e := range errors {
|
for _, e := range errors {
|
||||||
t.Error(e)
|
t.Error(e)
|
||||||
}
|
}
|
||||||
@@ -372,7 +383,7 @@ func TestRestMessage(t *testing.T) {
|
|||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// Dump buffered log data if there was a failure
|
// Dump buffered log data if there was a failure
|
||||||
io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +415,7 @@ func setupWebServer(ds smtpd.DataStore) *bytes.Buffer {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock DataStore object
|
// MockDataStore used to mock DataStore interface
|
||||||
type MockDataStore struct {
|
type MockDataStore struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
@@ -419,7 +430,7 @@ func (m *MockDataStore) AllMailboxes() ([]smtpd.Mailbox, error) {
|
|||||||
return args.Get(0).([]smtpd.Mailbox), args.Error(1)
|
return args.Get(0).([]smtpd.Mailbox), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock Mailbox object
|
// MockMailbox used to mock Mailbox interface
|
||||||
type MockMailbox struct {
|
type MockMailbox struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
@@ -454,7 +465,7 @@ type MockMessage struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMessage) Id() string {
|
func (m *MockMessage) ID() string {
|
||||||
args := m.Called()
|
args := m.Called()
|
||||||
return args.String(0)
|
return args.String(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/config"
|
"github.com/jhillyerd/inbucket/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RootIndex serves the Inbucket landing page
|
||||||
func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
greeting, err := ioutil.ReadFile(config.GetWebConfig().GreetingFile)
|
greeting, err := ioutil.ReadFile(config.GetWebConfig().GreetingFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -21,18 +22,19 @@ func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err erro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RootStatus serves the Inbucket status page
|
||||||
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||||
retentionMinutes := config.GetDataStoreConfig().RetentionMinutes
|
retentionMinutes := config.GetDataStoreConfig().RetentionMinutes
|
||||||
smtpListener := fmt.Sprintf("%s:%d", config.GetSmtpConfig().Ip4address.String(),
|
smtpListener := fmt.Sprintf("%s:%d", config.GetSMTPConfig().IP4address.String(),
|
||||||
config.GetSmtpConfig().Ip4port)
|
config.GetSMTPConfig().IP4port)
|
||||||
pop3Listener := fmt.Sprintf("%s:%d", config.GetPop3Config().Ip4address.String(),
|
pop3Listener := fmt.Sprintf("%s:%d", config.GetPOP3Config().IP4address.String(),
|
||||||
config.GetPop3Config().Ip4port)
|
config.GetPOP3Config().IP4port)
|
||||||
webListener := fmt.Sprintf("%s:%d", config.GetWebConfig().Ip4address.String(),
|
webListener := fmt.Sprintf("%s:%d", config.GetWebConfig().IP4address.String(),
|
||||||
config.GetWebConfig().Ip4port)
|
config.GetWebConfig().IP4port)
|
||||||
return RenderTemplate("root/status.html", w, map[string]interface{}{
|
return RenderTemplate("root/status.html", w, map[string]interface{}{
|
||||||
"ctx": ctx,
|
"ctx": ctx,
|
||||||
"version": config.VERSION,
|
"version": config.Version,
|
||||||
"buildDate": config.BUILD_DATE,
|
"buildDate": config.BuildDate,
|
||||||
"retentionMinutes": retentionMinutes,
|
"retentionMinutes": retentionMinutes,
|
||||||
"smtpListener": smtpListener,
|
"smtpListener": smtpListener,
|
||||||
"pop3Listener": pop3Listener,
|
"pop3Listener": pop3Listener,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
/*
|
// Package web provides Inbucket's web GUI and RESTful API
|
||||||
The web package contains all the code to provide Inbucket's web GUI
|
|
||||||
*/
|
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -19,12 +17,18 @@ import (
|
|||||||
|
|
||||||
type handler func(http.ResponseWriter, *http.Request, *Context) error
|
type handler func(http.ResponseWriter, *http.Request, *Context) error
|
||||||
|
|
||||||
var webConfig config.WebConfig
|
var (
|
||||||
var DataStore smtpd.DataStore
|
// DataStore is where all the mailboxes and messages live
|
||||||
var Router *mux.Router
|
DataStore smtpd.DataStore
|
||||||
var listener net.Listener
|
|
||||||
var sessionStore sessions.Store
|
// Router sends incoming requests to the correct handler function
|
||||||
var shutdown bool
|
Router *mux.Router
|
||||||
|
|
||||||
|
webConfig config.WebConfig
|
||||||
|
listener net.Listener
|
||||||
|
sessionStore sessions.Store
|
||||||
|
shutdown bool
|
||||||
|
)
|
||||||
|
|
||||||
// Initialize sets up things for unit tests or the Start() method
|
// Initialize sets up things for unit tests or the Start() method
|
||||||
func Initialize(cfg config.WebConfig, ds smtpd.DataStore) {
|
func Initialize(cfg config.WebConfig, ds smtpd.DataStore) {
|
||||||
@@ -39,8 +43,8 @@ func Initialize(cfg config.WebConfig, ds smtpd.DataStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupRoutes(cfg config.WebConfig) {
|
func setupRoutes(cfg config.WebConfig) {
|
||||||
log.LogInfo("Theme templates mapped to '%v'", cfg.TemplateDir)
|
log.Infof("Theme templates mapped to '%v'", cfg.TemplateDir)
|
||||||
log.LogInfo("Theme static content mapped to '%v'", cfg.PublicDir)
|
log.Infof("Theme static content mapped to '%v'", cfg.PublicDir)
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
// Static content
|
// Static content
|
||||||
@@ -55,7 +59,7 @@ func setupRoutes(cfg config.WebConfig) {
|
|||||||
r.Path("/mailbox/{name}").Handler(handler(MailboxList)).Name("MailboxList").Methods("GET")
|
r.Path("/mailbox/{name}").Handler(handler(MailboxList)).Name("MailboxList").Methods("GET")
|
||||||
r.Path("/mailbox/{name}").Handler(handler(MailboxPurge)).Name("MailboxPurge").Methods("DELETE")
|
r.Path("/mailbox/{name}").Handler(handler(MailboxPurge)).Name("MailboxPurge").Methods("DELETE")
|
||||||
r.Path("/mailbox/{name}/{id}").Handler(handler(MailboxShow)).Name("MailboxShow").Methods("GET")
|
r.Path("/mailbox/{name}/{id}").Handler(handler(MailboxShow)).Name("MailboxShow").Methods("GET")
|
||||||
r.Path("/mailbox/{name}/{id}/html").Handler(handler(MailboxHtml)).Name("MailboxHtml").Methods("GET")
|
r.Path("/mailbox/{name}/{id}/html").Handler(handler(MailboxHTML)).Name("MailboxHtml").Methods("GET")
|
||||||
r.Path("/mailbox/{name}/{id}/source").Handler(handler(MailboxSource)).Name("MailboxSource").Methods("GET")
|
r.Path("/mailbox/{name}/{id}/source").Handler(handler(MailboxSource)).Name("MailboxSource").Methods("GET")
|
||||||
r.Path("/mailbox/{name}/{id}").Handler(handler(MailboxDelete)).Name("MailboxDelete").Methods("DELETE")
|
r.Path("/mailbox/{name}/{id}").Handler(handler(MailboxDelete)).Name("MailboxDelete").Methods("DELETE")
|
||||||
r.Path("/mailbox/dattach/{name}/{id}/{num}/{file}").Handler(handler(MailboxDownloadAttach)).Name("MailboxDownloadAttach").Methods("GET")
|
r.Path("/mailbox/dattach/{name}/{id}/{num}/{file}").Handler(handler(MailboxDownloadAttach)).Name("MailboxDownloadAttach").Methods("GET")
|
||||||
@@ -66,9 +70,9 @@ func setupRoutes(cfg config.WebConfig) {
|
|||||||
http.Handle("/", Router)
|
http.Handle("/", Router)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start() the web server
|
// Start begins listening for HTTP requests
|
||||||
func Start() {
|
func Start() {
|
||||||
addr := fmt.Sprintf("%v:%v", webConfig.Ip4address, webConfig.Ip4port)
|
addr := fmt.Sprintf("%v:%v", webConfig.IP4address, webConfig.IP4port)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: nil,
|
Handler: nil,
|
||||||
@@ -77,30 +81,33 @@ func Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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.LogInfo("HTTP listening on TCP4 %v", addr)
|
log.Infof("HTTP listening on TCP4 %v", addr)
|
||||||
var err error
|
var err error
|
||||||
listener, err = net.Listen("tcp", addr)
|
listener, err = net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("HTTP failed to start TCP4 listener: %v", err)
|
log.Errorf("HTTP failed to start TCP4 listener: %v", err)
|
||||||
// TODO More graceful early-shutdown procedure
|
// TODO More graceful early-shutdown procedure
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = server.Serve(listener)
|
err = server.Serve(listener)
|
||||||
if shutdown {
|
if shutdown {
|
||||||
log.LogTrace("HTTP server shutting down on request")
|
log.Tracef("HTTP server shutting down on request")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.LogError("HTTP server failed: %v", err)
|
log.Errorf("HTTP server failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop shuts down the HTTP server
|
||||||
func Stop() {
|
func Stop() {
|
||||||
log.LogTrace("HTTP shutdown requested")
|
log.Tracef("HTTP shutdown requested")
|
||||||
shutdown = true
|
shutdown = true
|
||||||
if listener != nil {
|
if listener != nil {
|
||||||
listener.Close()
|
if err := listener.Close(); err != nil {
|
||||||
|
log.Errorf("Error closing HTTP listener: %v", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.LogError("HTTP listener was nil during shutdown")
|
log.Errorf("HTTP listener was nil during shutdown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +116,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
// Create the context
|
// Create the context
|
||||||
ctx, err := NewContext(req)
|
ctx, err := NewContext(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to create context: %v", err)
|
log.Errorf("Failed to create context: %v", err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,21 +124,23 @@ func (h handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
// Run the handler, grab the error, and report it
|
// Run the handler, grab the error, and report it
|
||||||
buf := new(httpbuf.Buffer)
|
buf := new(httpbuf.Buffer)
|
||||||
log.LogTrace("Web: %v %v %v %v", req.RemoteAddr, req.Proto, req.Method, req.RequestURI)
|
log.Tracef("Web: %v %v %v %v", req.RemoteAddr, req.Proto, req.Method, req.RequestURI)
|
||||||
err = h(buf, req, ctx)
|
err = h(buf, req, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Error handling %v: %v", req.RequestURI, err)
|
log.Errorf("Error handling %v: %v", req.RequestURI, err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the session
|
// Save the session
|
||||||
if err = ctx.Session.Save(req, buf); err != nil {
|
if err = ctx.Session.Save(req, buf); err != nil {
|
||||||
log.LogError("Failed to save session: %v", err)
|
log.Errorf("Failed to save session: %v", err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the buffered response to the writer
|
// Apply the buffered response to the writer
|
||||||
buf.Apply(w)
|
if _, err = buf.Apply(w); err != nil {
|
||||||
|
log.Errorf("Failed to write response: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ var cachedPartials = map[string]*template.Template{}
|
|||||||
func RenderTemplate(name string, w http.ResponseWriter, data interface{}) error {
|
func RenderTemplate(name string, w http.ResponseWriter, data interface{}) error {
|
||||||
t, err := ParseTemplate(name, false)
|
t, err := ParseTemplate(name, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Error in template '%v': %v", name, err)
|
log.Errorf("Error in template '%v': %v", name, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.Header().Set("Expires", "-1")
|
w.Header().Set("Expires", "-1")
|
||||||
@@ -32,7 +32,7 @@ func RenderTemplate(name string, w http.ResponseWriter, data interface{}) error
|
|||||||
func RenderPartial(name string, w http.ResponseWriter, data interface{}) error {
|
func RenderPartial(name string, w http.ResponseWriter, data interface{}) error {
|
||||||
t, err := ParseTemplate(name, true)
|
t, err := ParseTemplate(name, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Error in template '%v': %v", name, err)
|
log.Errorf("Error in template '%v': %v", name, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.Header().Set("Expires", "-1")
|
w.Header().Set("Expires", "-1")
|
||||||
@@ -51,7 +51,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(webConfig.TemplateDir, tempPath)
|
||||||
log.LogTrace("Parsing template %v", tempFile)
|
log.Tracef("Parsing template %v", tempFile)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var t *template.Template
|
var t *template.Template
|
||||||
@@ -71,10 +71,10 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
|||||||
// Allows us to disable caching for theme development
|
// Allows us to disable caching for theme development
|
||||||
if webConfig.TemplateCache {
|
if webConfig.TemplateCache {
|
||||||
if partial {
|
if partial {
|
||||||
log.LogTrace("Caching partial %v", name)
|
log.Tracef("Caching partial %v", name)
|
||||||
cachedTemplates[name] = t
|
cachedTemplates[name] = t
|
||||||
} else {
|
} else {
|
||||||
log.LogTrace("Caching template %v", name)
|
log.Tracef("Caching template %v", name)
|
||||||
cachedTemplates[name] = t
|
cachedTemplates[name] = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user