1
0
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:
James Hillyerd
2016-02-20 23:20:22 -08:00
parent 83f9c6aa49
commit e6b7e335cb
34 changed files with 607 additions and 411 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, "&amp;", "&", -1) unescaped := strings.Replace(url, "&amp;", "&", -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)
} }

View File

@@ -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("&lt;html&gt;")) assert.Equal(t, textToHTML("<html>"), template.HTML("&lt;html&gt;"))
// 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&amp;n=v</a>")) template.HTML("<a href=\"http://a.com/?q=a&n=v\" target=\"_blank\">http://a.com/?q=a&amp;n=v</a>"))
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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