// 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) } }
// ServeHTTP builds the context and passes onto the real handler func (h handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Create the context ctx, err := NewContext(req) if err != nil { log.LogError("Failed to create context: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } defer ctx.Close() // Run the handler, grab the error, and report it buf := new(httpbuf.Buffer) log.LogTrace("Web: %v %v %v %v", req.RemoteAddr, req.Proto, req.Method, req.RequestURI) err = h(buf, req, ctx) if err != nil { log.LogError("Error handling %v: %v", req.RequestURI, err) http.Error(w, err.Error(), http.StatusInternalServerError) return } // Save the session if err = ctx.Session.Save(req, buf); err != nil { log.LogError("Failed to save session: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } // Apply the buffered response to the writer buf.Apply(w) }
// 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) } } }
// 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 Stop() { log.LogTrace("HTTP shutdown requested") shutdown = true if listener != nil { listener.Close() } else { log.LogError("HTTP listener was nil during shutdown") } }
// RenderPartial fetches the named template and renders it to the provided // ResponseWriter. func RenderPartial(name string, w http.ResponseWriter, data interface{}) error { t, err := ParseTemplate(name, true) if err != nil { log.LogError("Error in template '%v': %v", name, err) return err } w.Header().Set("Expires", "-1") return t.Execute(w, data) }
// createDir checks for the presence of the path for this mailbox, creates it if needed func (mb *FileMailbox) createDir() error { if _, err := os.Stat(mb.path); err != nil { if err := os.MkdirAll(mb.path, 0770); err != nil { log.LogError("Failed to create directory %v, %v", mb.path, err) return err } } return nil }
// NewFileDataStore creates a new DataStore object using the specified path func NewFileDataStore(cfg config.DataStoreConfig) DataStore { path := cfg.Path if path == "" { log.LogError("No value configured for datastore path") return nil } mailPath := filepath.Join(path, "mail") if _, err := os.Stat(mailPath); err != nil { // Mail datastore does not yet exist os.MkdirAll(mailPath, 0770) } return &FileDataStore{path: path, mailPath: mailPath, messageCap: cfg.MailboxMsgCap} }
// Reversable routing function (shared with templates) func reverse(name string, things ...interface{}) string { // Convert the things to strings strs := make([]string, len(things)) for i, th := range things { strs[i] = fmt.Sprint(th) } // Grab the route u, err := Router.Get(name).URL(strs...) if err != nil { log.LogError("Failed to reverse route: %v", err) return "/ROUTE-ERROR" } return u.Path }
func retentionScanner(ds DataStore, maxAge time.Duration, sleep time.Duration) { start := time.Now() for { // Prevent scanner from running more than once a minute since := time.Since(start) if since < time.Minute { dur := time.Minute - since log.LogTrace("Retention scanner sleeping for %v", dur) time.Sleep(dur) } start = time.Now() // Kickoff scan if err := doRetentionScan(ds, maxAge, sleep); err != nil { log.LogError("Error during retention scan: %v", err) } } }
// 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 { log.LogTrace("Starting retention scan") cutoff := time.Now().Add(-1 * maxAge) mboxes, err := ds.AllMailboxes() if err != nil { return err } retained := 0 for _, mb := range mboxes { messages, err := mb.GetMessages() if err != nil { return err } for _, msg := range messages { if msg.Date().Before(cutoff) { log.LogTrace("Purging expired message %v", msg.Id()) err = msg.Delete() if err != nil { // Log but don't abort log.LogError("Failed to purge message %v: %v", msg.Id(), err) } else { expRetentionDeletesTotal.Add(1) } } else { retained++ } } // Sleep after completing a mailbox time.Sleep(sleep) } setRetentionScanCompleted(time.Now()) expRetainedCurrent.Set(int64(retained)) return 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) } } }
func (ses *Session) logError(msg string, args ...interface{}) { // Update metrics //expErrorsTotal.Add(1) log.LogError("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...)) }
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() }
// timedExit is called as a goroutine during shutdown, it will force an exit after 15 seconds func timedExit() { time.Sleep(15 * time.Second) log.LogError("Inbucket clean shutdown timed out, forcing exit") os.Exit(0) }