diff --git a/config.go b/config.go index 08ef716..535acab 100644 --- a/config.go +++ b/config.go @@ -4,11 +4,27 @@ import ( "container/list" "fmt" "github.com/robfig/goconfig/config" + "net" "os" ) +// SmtpConfig houses the SMTP server configuration - not using pointers +// so that I can pass around copies of the object safely. +type SmtpConfig struct { + Ip4address net.IP + Ip4port int + Domain string +} + +var smtpConfig *SmtpConfig + var Config *config.Config +// GetSmtpConfig returns a copy of the SmtpConfig +func GetSmtpConfig() SmtpConfig { + return *smtpConfig +} + // LoadConfig loads the specified configuration file into inbucket.Config // and performs validations on it. func LoadConfig(filename string) error { @@ -49,6 +65,44 @@ func LoadConfig(filename string) error { return fmt.Errorf("Failed to validate configuration") } + err = parseSmtpConfig() + + return err +} + +// parseSmtpConfig trying to catch config errors early +func parseSmtpConfig() error { + smtpConfig = new(SmtpConfig) + + // Parse IP4 address only, error on IP6. + option := "[smtp]ip4.address" + str, err := Config.String("smtp", "ip4.address") + if err != nil { + return fmt.Errorf("Failed to parse %v: %v", option, err) + } + addr := net.ParseIP(str) + if addr == nil { + return fmt.Errorf("Failed to parse %v '%v'", option, str) + } + addr = addr.To4() + if addr == nil { + return fmt.Errorf("Failed to parse %v '%v' not IPv4!", option, str) + } + smtpConfig.Ip4address = addr + + option = "[smtp]ip4.port" + smtpConfig.Ip4port, err = Config.Int("smtp", "ip4.port") + if err != nil { + return fmt.Errorf("Failed to parse %v: %v", option, err) + } + + option = "[smtp]domain" + str, err = Config.String("smtp", "domain") + if err != nil { + return fmt.Errorf("Failed to parse %v: %v", option, err) + } + smtpConfig.Domain = str + return nil } diff --git a/datastore.go b/datastore.go index 8986932..1c7ac67 100644 --- a/datastore.go +++ b/datastore.go @@ -5,7 +5,6 @@ import ( "encoding/gob" "errors" "fmt" - "github.com/robfig/revel" "io/ioutil" "net/mail" "os" @@ -38,16 +37,20 @@ type DataStore struct { mailPath string } -// NewDataStore creates a new DataStore object. It uses the Revel Config object to +// NewDataStore creates a new DataStore object. It uses the inbucket.Config object to // construct it's path. func NewDataStore() *DataStore { - path, found := rev.Config.String("datastore.path") - if found { - mailPath := filepath.Join(path, "mail") - return &DataStore{path: path, mailPath: mailPath} + path, err := Config.String("datastore", "path") + if err != nil { + Error("Error getting datastore path: %v", err) + return nil } - rev.ERROR.Printf("No value configured for datastore.path") - return nil + if path == "" { + Error("No value configured for datastore path") + return nil + } + mailPath := filepath.Join(path, "mail") + return &DataStore{path: path, mailPath: mailPath} } // Retrieves the Mailbox object for a specified email address, if the mailbox @@ -57,7 +60,7 @@ func (ds *DataStore) MailboxFor(emailAddress string) (*Mailbox, error) { dir := HashMailboxName(name) path := filepath.Join(ds.mailPath, dir) if err := os.MkdirAll(path, 0770); err != nil { - rev.ERROR.Printf("Failed to create directory %v, %v", path, err) + Error("Failed to create directory %v, %v", path, err) return nil, err } return &Mailbox{store: ds, name: name, dirName: dir, path: path}, nil @@ -83,7 +86,7 @@ func (mb *Mailbox) GetMessages() ([]*Message, error) { if err != nil { return nil, err } - rev.TRACE.Printf("Scanning %v files for %v", len(files), mb) + Trace("Scanning %v files for %v", len(files), mb) messages := make([]*Message, 0, len(files)) for _, f := range files { @@ -100,7 +103,7 @@ func (mb *Mailbox) GetMessages() ([]*Message, error) { } file.Close() msg.mailbox = mb - rev.TRACE.Printf("Found: %v", msg) + Trace("Found: %v", msg) messages = append(messages, msg) } } @@ -121,7 +124,7 @@ func (mb *Mailbox) GetMessage(id string) (*Message, error) { } file.Close() msg.mailbox = mb - rev.TRACE.Printf("Found: %v", msg) + Trace("Found: %v", msg) return msg, nil } @@ -157,8 +160,8 @@ func (m *Message) gobPath() string { } func (m *Message) rawPath() string { - rev.TRACE.Println(m.mailbox.path) - rev.TRACE.Println(m.Id) + Trace(m.mailbox.path) + Trace(m.Id) return filepath.Join(m.mailbox.path, m.Id+".raw") } diff --git a/inbucketd/inbucketd.go b/inbucketd/inbucketd.go index 875547e..9efcf07 100644 --- a/inbucketd/inbucketd.go +++ b/inbucketd/inbucketd.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/jhillyerd/inbucket" "github.com/jhillyerd/inbucket/smtpd" + "log" "os" ) @@ -26,15 +27,20 @@ func main() { os.Exit(1) } err := inbucket.LoadConfig(flag.Arg(0)) - configError(err) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err) + os.Exit(1) + } + + log.Println("Logger test") + inbucket.Trace("trace test") + inbucket.Info("info test") + inbucket.Warn("warn test") + inbucket.Error("error test") // Startup SMTP server - domain, err := inbucket.Config.String("smtp", "domain") - configError(err) - port, err := inbucket.Config.Int("smtp", "ip4.port") - configError(err) - server := smtpd.New(domain, port) - go server.Start() + server := smtpd.New() + server.Start() } func init() { @@ -43,10 +49,3 @@ func init() { flag.PrintDefaults() } } - -func configError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing config file: %v\n", err) - os.Exit(1) - } -} diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..59bd6c9 --- /dev/null +++ b/logging.go @@ -0,0 +1,46 @@ +package inbucket + +import ( + "log" +) + +type LogLevel int + +const ( + ERROR LogLevel = iota + WARN + INFO + TRACE +) + +var MaxLogLevel LogLevel = TRACE + +// Error logs a message to the 'standard' Logger (always) +func Error(msg string, args ...interface{}) { + msg = "[ERROR] " + msg + log.Printf(msg, args...) +} + +// Warn logs a message to the 'standard' Logger if MaxLogLevel is >= WARN +func Warn(msg string, args ...interface{}) { + if MaxLogLevel >= WARN { + msg = "[WARN ] " + msg + log.Printf(msg, args...) + } +} + +// Info logs a message to the 'standard' Logger if MaxLogLevel is >= INFO +func Info(msg string, args ...interface{}) { + if MaxLogLevel >= INFO { + msg = "[INFO ] " + msg + log.Printf(msg, args...) + } +} + +// Trace logs a message to the 'standard' Logger if MaxLogLevel is >= TRACE +func Trace(msg string, args ...interface{}) { + if MaxLogLevel >= TRACE { + msg = "[TRACE] " + msg + log.Printf(msg, args...) + } +} diff --git a/smtpd/handler.go b/smtpd/handler.go index 03fafcb..e88a3a6 100644 --- a/smtpd/handler.go +++ b/smtpd/handler.go @@ -87,7 +87,7 @@ func (ss *Session) String() string { * 5. Goto 2 */ func (s *Server) startSession(id int, conn net.Conn) { - s.info("Connection from %v, starting session <%v>", conn.RemoteAddr(), id) + inbucket.Info("Connection from %v, starting session <%v>", conn.RemoteAddr(), id) defer conn.Close() ss := NewSession(s, id, conn) @@ -476,17 +476,17 @@ func (ss *Session) ooSeq(cmd string) { // Session specific logging methods func (ss *Session) trace(msg string, args ...interface{}) { - ss.server.trace("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) + inbucket.Trace("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) } func (ss *Session) info(msg string, args ...interface{}) { - ss.server.info("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) + inbucket.Info("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) } func (ss *Session) warn(msg string, args ...interface{}) { - ss.server.warn("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) + inbucket.Warn("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) } func (ss *Session) error(msg string, args ...interface{}) { - ss.server.error("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) + inbucket.Error("%v<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) } diff --git a/smtpd/listener.go b/smtpd/listener.go index a249ca6..d58d3d1 100644 --- a/smtpd/listener.go +++ b/smtpd/listener.go @@ -3,14 +3,12 @@ package smtpd import ( "fmt" "github.com/jhillyerd/inbucket" - "github.com/robfig/revel" "net" ) // Real server code starts here type Server struct { domain string - port int maxRecips int maxIdleSeconds int maxMessageBytes int @@ -18,39 +16,36 @@ type Server struct { } // Init a new Server object -func New(domain string, port int) *Server { +func New() *Server { ds := inbucket.NewDataStore() - return &Server{domain: domain, port: port, maxRecips: 100, maxIdleSeconds: 300, + // TODO Make more of these configurable + return &Server{domain: inbucket.GetSmtpConfig().Domain, maxRecips: 100, maxIdleSeconds: 300, dataStore: ds, maxMessageBytes: 2048000} } -// Loggers -func (s *Server) trace(msg string, args ...interface{}) { - rev.TRACE.Printf(msg, args...) -} - -func (s *Server) info(msg string, args ...interface{}) { - rev.INFO.Printf(msg, args...) -} - -func (s *Server) warn(msg string, args ...interface{}) { - rev.WARN.Printf(msg, args...) -} - -func (s *Server) error(msg string, args ...interface{}) { - rev.ERROR.Printf(msg, args...) -} - // Main listener loop func (s *Server) Start() { - s.trace("Server Start() called") - ln, err := net.Listen("tcp", fmt.Sprintf(":%v", s.port)) + cfg := inbucket.GetSmtpConfig() + addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v", + cfg.Ip4address, cfg.Ip4port)) if err != nil { + inbucket.Error("Failed to build tcp4 address: %v", err) + // TODO More graceful early-shutdown procedure + panic(err) + } + + inbucket.Info("Listening on TCP4 %v", addr) + ln, err := net.ListenTCP("tcp4", addr) + if err != nil { + inbucket.Error("Failed to start tcp4 listener: %v", err) + // TODO More graceful early-shutdown procedure panic(err) } for sid := 1; ; sid++ { if conn, err := ln.Accept(); err != nil { + // TODO Implement a max error counter before shutdown? + // or maybe attempt to restart smtpd panic(err) } else { go s.startSession(sid, conn)