func setupRoutes(cfg config.WebConfig) { log.LogInfo("Theme templates mapped to '%v'", cfg.TemplateDir) log.LogInfo("Theme static content mapped to '%v'", cfg.PublicDir) r := mux.NewRouter() // Static content r.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir(cfg.PublicDir)))) // Root r.Path("/").Handler(handler(RootIndex)).Name("RootIndex").Methods("GET") r.Path("/status").Handler(handler(RootStatus)).Name("RootStatus").Methods("GET") r.Path("/link/{name}/{id}").Handler(handler(MailboxLink)).Name("MailboxLink").Methods("GET") r.Path("/mailbox").Handler(handler(MailboxIndex)).Name("MailboxIndex").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}/{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}/source").Handler(handler(MailboxSource)).Name("MailboxSource").Methods("GET") 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/vattach/{name}/{id}/{num}/{file}").Handler(handler(MailboxViewAttach)).Name("MailboxViewAttach").Methods("GET") // Register w/ HTTP Router = r http.Handle("/", Router) }
// signalProcessor is a goroutine that handles OS signals func signalProcessor(c <-chan os.Signal) { for { sig := <-c switch sig { case syscall.SIGHUP: // Rotate logs if configured if logf != nil { log.LogInfo("Recieved SIGHUP, cycling logfile") closeLogFile() openLogFile() } else { log.LogInfo("Ignoring SIGHUP, logfile not configured") } case syscall.SIGTERM: // Initiate shutdown log.LogInfo("Received SIGTERM, shutting down") go timedExit() web.Stop() if smtpServer != nil { smtpServer.Stop() } else { log.LogError("smtpServer was nil during shutdown") } } } }
func StartRetentionScanner(ds DataStore) { cfg := config.GetDataStoreConfig() expRetentionPeriod.Set(int64(cfg.RetentionMinutes * 60)) if cfg.RetentionMinutes > 0 { // Retention scanning enabled log.LogInfo("Retention configured for %v minutes", cfg.RetentionMinutes) go retentionScanner(ds, time.Duration(cfg.RetentionMinutes)*time.Minute, time.Duration(cfg.RetentionSleep)*time.Millisecond) } else { log.LogInfo("Retention scanner disabled") } }
// Start() the web server func Start() { addr := fmt.Sprintf("%v:%v", webConfig.Ip4address, webConfig.Ip4port) server := &http.Server{ Addr: addr, Handler: nil, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, } // We don't use ListenAndServe because it lacks a way to close the listener log.LogInfo("HTTP listening on TCP4 %v", addr) var err error listener, err = net.Listen("tcp", addr) if err != nil { log.LogError("HTTP failed to start TCP4 listener: %v", err) // TODO More graceful early-shutdown procedure panic(err) } err = server.Serve(listener) if shutdown { log.LogTrace("HTTP server shutting down on request") } else if err != nil { log.LogError("HTTP server failed: %v", err) } }
// Main listener loop func (s *Server) Start() { cfg := config.GetPop3Config() addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v", cfg.Ip4address, cfg.Ip4port)) if err != nil { log.LogError("POP3 Failed to build tcp4 address: %v", err) // TODO More graceful early-shutdown procedure panic(err) } log.LogInfo("POP3 listening on TCP4 %v", addr) s.listener, err = net.ListenTCP("tcp4", addr) if err != nil { log.LogError("POP3 failed to start tcp4 listener: %v", err) // TODO More graceful early-shutdown procedure panic(err) } // Handle incoming connections var tempDelay time.Duration for sid := 1; ; sid++ { if conn, err := s.listener.Accept(); err != nil { if nerr, ok := err.(net.Error); ok && nerr.Temporary() { // Temporary error, sleep for a bit and try again if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.LogError("POP3 accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } else { if s.shutdown { log.LogTrace("POP3 listener shutting down on request") return } // TODO Implement a max error counter before shutdown? // or maybe attempt to restart POP3 panic(err) } } else { tempDelay = 0 s.waitgroup.Add(1) go s.startSession(sid, conn) } } }
// NewMessage creates a new Message object and sets the Date and Id fields. // It will also delete messages over messageCap if configured. func (mb *FileMailbox) NewMessage() (Message, error) { // Load index if !mb.indexLoaded { if err := mb.readIndex(); err != nil { return nil, err } } // Delete old messages over messageCap if mb.store.messageCap > 0 { for len(mb.messages) >= mb.store.messageCap { log.LogInfo("Mailbox %q over configured message cap", mb.name) if err := mb.messages[0].Delete(); err != nil { return nil, err } } } date := time.Now() id := generateId(date) return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}, nil }
// Main listener loop func (s *Server) Start() { cfg := config.GetSmtpConfig() addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v", cfg.Ip4address, cfg.Ip4port)) if err != nil { log.LogError("Failed to build tcp4 address: %v", err) // TODO More graceful early-shutdown procedure panic(err) } log.LogInfo("SMTP listening on TCP4 %v", addr) s.listener, err = net.ListenTCP("tcp4", addr) if err != nil { log.LogError("SMTP failed to start tcp4 listener: %v", err) // TODO More graceful early-shutdown procedure panic(err) } if !s.storeMessages { log.LogInfo("Load test mode active, messages will not be stored") } else if s.domainNoStore != "" { log.LogInfo("Messages sent to domain '%v' will be discarded", s.domainNoStore) } // Start retention scanner StartRetentionScanner(s.dataStore) // Handle incoming connections var tempDelay time.Duration for sid := 1; ; sid++ { if conn, err := s.listener.Accept(); err != nil { if nerr, ok := err.(net.Error); ok && nerr.Temporary() { // Temporary error, sleep for a bit and try again if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.LogError("SMTP accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } else { if s.shutdown { log.LogTrace("SMTP listener shutting down on request") return } // TODO Implement a max error counter before shutdown? // or maybe attempt to restart smtpd panic(err) } } else { tempDelay = 0 expConnectsTotal.Add(1) s.waitgroup.Add(1) go s.startSession(sid, conn) } } }
/* Session flow: * 1. Send initial greeting * 2. Receive cmd * 3. If good cmd, respond, optionally change state * 4. If bad cmd, respond error * 5. Goto 2 */ func (s *Server) startSession(id int, conn net.Conn) { log.LogInfo("POP3 connection from %v, starting session <%v>", conn.RemoteAddr(), id) //expConnectsCurrent.Add(1) defer func() { conn.Close() s.waitgroup.Done() //expConnectsCurrent.Add(-1) }() ses := NewSession(s, id, conn) ses.send(fmt.Sprintf("+OK Inbucket POP3 server ready <%v.%v@%v>", os.Getpid(), time.Now().Unix(), s.domain)) // This is our command reading loop for ses.state != QUIT && ses.sendError == nil { line, err := ses.readLine() if err == nil { if cmd, arg, ok := ses.parseCmd(line); ok { // Check against valid SMTP commands if cmd == "" { ses.send("-ERR Speak up") continue } if !commands[cmd] { ses.send(fmt.Sprintf("-ERR Syntax error, %v command unrecognized", cmd)) ses.logWarn("Unrecognized command: %v", cmd) continue } // Commands we handle in any state switch cmd { case "CAPA": // List our capabilities per RFC2449 ses.send("+OK Capability list follows") ses.send("TOP") ses.send("USER") ses.send("UIDL") ses.send("IMPLEMENTATION Inbucket") ses.send(".") continue } // Send command to handler for current state switch ses.state { case AUTHORIZATION: ses.authorizationHandler(cmd, arg) continue case TRANSACTION: ses.transactionHandler(cmd, arg) continue } ses.logError("Session entered unexpected state %v", ses.state) break } else { ses.send("-ERR Syntax error, command garbled") } } else { // readLine() returned an error if err == io.EOF { switch ses.state { case AUTHORIZATION: // EOF is common here ses.logInfo("Client closed connection (state %v)", ses.state) default: ses.logWarn("Got EOF while in state %v", ses.state) } break } // not an EOF ses.logWarn("Connection error: %v", err) if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { ses.send("-ERR Idle timeout, bye bye") break } } ses.send("-ERR Connection error, sorry") break } } if ses.sendError != nil { ses.logWarn("Network send error: %v", ses.sendError) } ses.logInfo("Closing connection") }
func (ses *Session) logInfo(msg string, args ...interface{}) { log.LogInfo("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...)) }
/* Session flow: * 1. Send initial greeting * 2. Receive cmd * 3. If good cmd, respond, optionally change state * 4. If bad cmd, respond error * 5. Goto 2 */ func (s *Server) startSession(id int, conn net.Conn) { log.LogInfo("SMTP Connection from %v, starting session <%v>", conn.RemoteAddr(), id) expConnectsCurrent.Add(1) defer func() { conn.Close() s.waitgroup.Done() expConnectsCurrent.Add(-1) }() ss := NewSession(s, id, conn) ss.greet() // This is our command reading loop for ss.state != QUIT && ss.sendError == nil { if ss.state == DATA { // Special case, does not use SMTP command format ss.dataHandler() continue } line, err := ss.readLine() if err == nil { if cmd, arg, ok := ss.parseCmd(line); ok { // Check against valid SMTP commands if cmd == "" { ss.send("500 Speak up") continue } if !commands[cmd] { ss.send(fmt.Sprintf("500 Syntax error, %v command unrecognized", cmd)) ss.logWarn("Unrecognized command: %v", cmd) continue } // Commands we handle in any state switch cmd { case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN": // These commands are not implemented in any state ss.send(fmt.Sprintf("502 %v command not implemented", cmd)) ss.logWarn("Command %v not implemented by Inbucket", cmd) continue case "VRFY": ss.send("252 Cannot VRFY user, but will accept message") continue case "NOOP": ss.send("250 I have sucessfully done nothing") continue case "RSET": // Reset session ss.logTrace("Resetting session state on RSET request") ss.reset() ss.send("250 Session reset") continue case "QUIT": ss.send("221 Goodnight and good luck") ss.enterState(QUIT) continue } // Send command to handler for current state switch ss.state { case GREET: ss.greetHandler(cmd, arg) continue case READY: ss.readyHandler(cmd, arg) continue case MAIL: ss.mailHandler(cmd, arg) continue } ss.logError("Session entered unexpected state %v", ss.state) break } else { ss.send("500 Syntax error, command garbled") } } else { // readLine() returned an error if err == io.EOF { switch ss.state { case GREET, READY: // EOF is common here ss.logInfo("Client closed connection (state %v)", ss.state) default: ss.logWarn("Got EOF while in state %v", ss.state) } break } // not an EOF ss.logWarn("Connection error: %v", err) if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { ss.send("221 Idle timeout, bye bye") break } } ss.send("221 Connection error, sorry") break } } if ss.sendError != nil { ss.logWarn("Network send error: %v", ss.sendError) } ss.logInfo("Closing connection") }
func main() { config.VERSION = VERSION config.BUILD_DATE = BUILD_DATE flag.Parse() if *help { flag.Usage() return } // Load & Parse config if flag.NArg() != 1 { flag.Usage() os.Exit(1) } err := config.LoadConfig(flag.Arg(0)) if err != nil { fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err) os.Exit(1) } // Setup signal handler sigChan := make(chan os.Signal) signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM) go signalProcessor(sigChan) // Configure logging, close std* fds level, _ := config.Config.String("logging", "level") log.SetLogLevel(level) if *logfile != "stderr" { // stderr is the go logging default if *logfile == "stdout" { // set to stdout golog.SetOutput(os.Stdout) } else { err = openLogFile() if err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } defer closeLogFile() // close std* streams os.Stdout.Close() os.Stderr.Close() // Warning: this will hide panic() output os.Stdin.Close() os.Stdout = logf os.Stderr = logf } } log.LogInfo("Inbucket %v (%v) starting...", config.VERSION, config.BUILD_DATE) // Write pidfile if requested // TODO: Probably supposed to remove pidfile during shutdown if *pidfile != "none" { pidf, err := os.Create(*pidfile) if err != nil { log.LogError("Failed to create %v: %v", *pidfile, err) os.Exit(1) } defer pidf.Close() fmt.Fprintf(pidf, "%v\n", os.Getpid()) } // Grab our datastore ds := smtpd.DefaultFileDataStore() // Start HTTP server web.Initialize(config.GetWebConfig(), ds) go web.Start() // Start POP3 server pop3Server = pop3d.New() go pop3Server.Start() // Startup SMTP server, block until it exits smtpServer = smtpd.NewSmtpServer(config.GetSmtpConfig(), ds) smtpServer.Start() // Wait for active connections to finish smtpServer.Drain() pop3Server.Drain() }