// Start begins listening for HTTP requests 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.Infof("HTTP listening on TCP4 %v", addr) var err error listener, err = net.Listen("tcp", addr) if err != nil { log.Errorf("HTTP failed to start TCP4 listener: %v", err) emergencyShutdown() return } // Listener go routine go serve() // Wait for shutdown select { case _ = <-globalShutdown: log.Tracef("HTTP server shutting down on request") } // Closing the listener will cause the serve() go routine to exit if err := listener.Close(); err != nil { log.Errorf("Failed to close HTTP listener: %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.Errorf("HTTP 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.Tracef("HTTP[%v] %v %v %q", req.RemoteAddr, req.Proto, req.Method, req.RequestURI) err = h(buf, req, ctx) if err != nil { log.Errorf("HTTP error handling %q: %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.Errorf("HTTP failed to save session: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } // Apply the buffered response to the writer if _, err = buf.Apply(w); err != nil { log.Errorf("HTTP failed to write response: %v", err) } }
// NewFileDataStore creates a new DataStore object using the specified path func NewFileDataStore(cfg config.DataStoreConfig) DataStore { path := cfg.Path if path == "" { log.Errorf("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 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} }
func retentionScanner(ds DataStore, maxAge time.Duration, sleep time.Duration) { start := time.Now() retentionLoop: for { // Prevent scanner from running more than once a minute since := time.Since(start) if since < time.Minute { dur := time.Minute - since log.Tracef("Retention scanner sleeping for %v", dur) select { case _ = <-globalShutdown: break retentionLoop case _ = <-time.After(dur): } } // Kickoff scan start = time.Now() if err := doRetentionScan(ds, maxAge, sleep); err != nil { log.Errorf("Error during retention scan: %v", err) } // Check for global shutdown select { case _ = <-globalShutdown: break retentionLoop default: } } log.Tracef("Retention scanner shut down") close(retentionShutdown) }
// Start the listener and handle incoming connections func (s *Server) Start() { cfg := config.GetSMTPConfig() addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port)) if err != nil { log.Errorf("Failed to build tcp4 address: %v", err) // serve() never called, so we do local shutdown here close(s.localShutdown) s.emergencyShutdown() return } log.Infof("SMTP listening on TCP4 %v", addr) s.listener, err = net.ListenTCP("tcp4", addr) if err != nil { log.Errorf("SMTP failed to start tcp4 listener: %v", err) // serve() never called, so we do local shutdown here close(s.localShutdown) s.emergencyShutdown() return } if !s.storeMessages { log.Infof("Load test mode active, messages will not be stored") } else if s.domainNoStore != "" { log.Infof("Messages sent to domain '%v' will be discarded", s.domainNoStore) } // Start retention scanner StartRetentionScanner(s.dataStore, s.globalShutdown) // Listener go routine go s.serve() // Wait for shutdown select { case _ = <-s.globalShutdown: log.Tracef("SMTP shutdown requested, connections will be drained") } // Closing the listener will cause the serve() go routine to exit if err := s.listener.Close(); err != nil { log.Errorf("Failed to close SMTP listener: %v", err) } }
// 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.Errorf("Failed to create directory %v, %v", mb.path, err) return err } } return nil }
// 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.Errorf("Error in template '%v': %v", name, err) return err } w.Header().Set("Expires", "-1") return t.Execute(w, data) }
// serve begins serving HTTP requests func serve() { // server.Serve blocks until we close the listener err := server.Serve(listener) select { case _ = <-globalShutdown: // Nop default: log.Errorf("HTTP server failed: %v", err) emergencyShutdown() return } }
// ReadHeader opens the .raw portion of a Message and returns a standard Go mail.Message object func (m *FileMessage) ReadHeader() (msg *mail.Message, err error) { file, err := os.Open(m.rawPath()) if err != nil { return nil, err } defer func() { if err := file.Close(); err != nil { log.Errorf("Failed to close %q: %v", m.rawPath(), err) } }() reader := bufio.NewReader(file) return mail.ReadMessage(reader) }
// Start the server and listen for connections func (s *Server) Start() { cfg := config.GetPOP3Config() addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port)) if err != nil { log.Errorf("POP3 Failed to build tcp4 address: %v", err) // serve() never called, so we do local shutdown here close(s.localShutdown) s.emergencyShutdown() return } log.Infof("POP3 listening on TCP4 %v", addr) s.listener, err = net.ListenTCP("tcp4", addr) if err != nil { log.Errorf("POP3 failed to start tcp4 listener: %v", err) // serve() never called, so we do local shutdown here close(s.localShutdown) s.emergencyShutdown() return } // Listener go routine go s.serve() // Wait for shutdown select { case _ = <-s.globalShutdown: } log.Tracef("POP3 shutdown requested, connections will be drained") // Closing the listener will cause the serve() go routine to exit if err := s.listener.Close(); err != nil { log.Errorf("Error closing POP3 listener: %v", err) } }
// ReadRaw opens the .raw portion of a Message and returns it as a string func (m *FileMessage) ReadRaw() (raw *string, err error) { reader, err := m.RawReader() if err != nil { 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)) if err != nil { return nil, err } bodyString := string(bodyBytes) return &bodyString, nil }
// 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.Tracef("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.Tracef("Purging expired message %v", msg.ID()) err = msg.Delete() if err != nil { // Log but don't abort log.Errorf("Failed to purge message %v: %v", msg.ID(), err) } else { expRetentionDeletesTotal.Add(1) } } else { retained++ } } // Check for shutdown select { case _ = <-globalShutdown: log.Tracef("Retention scan aborted due to shutdown") return nil default: } // Sleep after completing a mailbox time.Sleep(sleep) } setRetentionScanCompleted(time.Now()) expRetainedCurrent.Set(int64(retained)) return nil }
// readIndex loads the mailbox index data from disk func (mb *FileMailbox) readIndex() error { // Clear message slice, open index mb.messages = mb.messages[:0] // Lock for reading indexLock.RLock() defer indexLock.RUnlock() // Check if index exists if _, err := os.Stat(mb.indexPath); err != nil { // Does not exist, but that's not an error in our world log.Tracef("Index %v does not exist (yet)", mb.indexPath) mb.indexLoaded = true return nil } file, err := os.Open(mb.indexPath) if err != nil { return err } defer func() { if err := file.Close(); err != nil { log.Errorf("Failed to close %q: %v", mb.indexPath, err) } }() // Decode gob data dec := gob.NewDecoder(bufio.NewReader(file)) for { msg := new(FileMessage) if err = dec.Decode(msg); err != nil { if err == io.EOF { // It's OK to get an EOF here break } return fmt.Errorf("Corrupt mailbox %q: %v", mb.indexPath, err) } msg.mailbox = mb mb.messages = append(mb.messages, msg) } mb.indexLoaded = true return nil }
// writeIndex overwrites the index on disk with the current mailbox data func (mb *FileMailbox) writeIndex() error { // Lock for writing indexLock.Lock() defer indexLock.Unlock() if len(mb.messages) > 0 { // Ensure mailbox directory exists if err := mb.createDir(); err != nil { return err } // Open index for writing file, err := os.Create(mb.indexPath) if err != nil { return err } defer func() { if err := file.Close(); err != nil { log.Errorf("Failed to close %q: %v", mb.indexPath, err) } }() writer := bufio.NewWriter(file) // Write each message and then flush enc := gob.NewEncoder(writer) for _, m := range mb.messages { err = enc.Encode(m) if err != nil { return err } } if err := writer.Flush(); err != nil { return err } } else { // No messages, delete index+maildir log.Tracef("Removing mailbox %v", mb.path) return os.RemoveAll(mb.path) } return nil }
// ReadBody opens the .raw portion of a Message and returns a MIMEBody object func (m *FileMessage) ReadBody() (body *enmime.MIMEBody, err error) { file, err := os.Open(m.rawPath()) if err != nil { return nil, err } defer func() { if err := file.Close(); err != nil { log.Errorf("Failed to close %q: %v", m.rawPath(), err) } }() reader := bufio.NewReader(file) msg, err := mail.ReadMessage(reader) if err != nil { return nil, err } mime, err := enmime.ParseMIMEBody(msg) if err != nil { return nil, err } return mime, nil }
// NewMessage creates a new FileMessage 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.Infof("Mailbox %q over configured message cap", mb.name) if err := mb.messages[0].Delete(); err != nil { log.Errorf("Error deleting message: %s", err) } } } date := time.Now() id := generateID(date) return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}, nil }
// serve is the listen/accept loop func (s *Server) serve() { // Handle incoming connections var tempDelay time.Duration for sessionID := 1; ; sessionID++ { if conn, err := s.listener.Accept(); err != nil { // There was an error accepting the connection 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.Errorf("SMTP accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } else { // Permanent error select { case _ = <-s.globalShutdown: close(s.localShutdown) return default: close(s.localShutdown) s.emergencyShutdown() return } } } else { tempDelay = 0 expConnectsTotal.Add(1) s.waitgroup.Add(1) go s.startSession(sessionID, conn) } } }
func (ss *Session) logError(msg string, args ...interface{}) { // Update metrics expErrorsTotal.Add(1) log.Errorf("SMTP[%v]<%v> %v", ss.remoteHost, ss.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.Infof("SMTP Connection from %v, starting session <%v>", conn.RemoteAddr(), id) expConnectsCurrent.Add(1) defer func() { if err := conn.Close(); err != nil { log.Errorf("Error closing connection for <%v>: %v", id, err) } 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") }