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 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) } }
// Drain causes the caller to block until all active POP3 sessions have finished func (s *Server) Drain() { // Wait for listener to exit select { case _ = <-s.localShutdown: } // Wait for sessions to close s.waitgroup.Wait() log.Tracef("POP3 connections have drained") }
// 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 }
// ParseTemplate loads the requested template along with _base.html, caching // the result (if configured to do so) func ParseTemplate(name string, partial bool) (*template.Template, error) { cachedMutex.Lock() defer cachedMutex.Unlock() if t, ok := cachedTemplates[name]; ok { return t, nil } tempPath := strings.Replace(name, "/", string(filepath.Separator), -1) tempFile := filepath.Join(webConfig.TemplateDir, tempPath) log.Tracef("Parsing template %v", tempFile) var err error var t *template.Template if partial { // Need to get basename of file to make it root template w/ funcs base := path.Base(name) t = template.New(base).Funcs(TemplateFuncs) t, err = t.ParseFiles(tempFile) } else { t = template.New("_base.html").Funcs(TemplateFuncs) t, err = t.ParseFiles(filepath.Join(webConfig.TemplateDir, "_base.html"), tempFile) } if err != nil { return nil, err } // Allows us to disable caching for theme development if webConfig.TemplateCache { if partial { log.Tracef("Caching partial %v", name) cachedTemplates[name] = t } else { log.Tracef("Caching template %v", name) cachedTemplates[name] = t } } return t, nil }
// 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) } }
// 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 }
// Delete this Message from disk by removing it from the index and deleting the // raw files. func (m *FileMessage) Delete() error { messages := m.mailbox.messages for i, mm := range messages { if m == mm { // Slice around message we are deleting m.mailbox.messages = append(messages[:i], messages[i+1:]...) break } } if err := m.mailbox.writeIndex(); err != nil { return err } if len(m.mailbox.messages) == 0 { // This was the last message, thus writeIndex() has removed the entire // directory; we don't need to delete the raw file. return nil } // There are still messages in the index log.Tracef("Deleting %v", m.rawPath()) return os.Remove(m.rawPath()) }
// Initialize sets up things for unit tests or the Start() method func Initialize(cfg config.WebConfig, ds smtpd.DataStore, shutdownChan chan bool) { webConfig = cfg globalShutdown = shutdownChan // NewContext() will use this DataStore for the web handlers DataStore = ds // Content Paths log.Infof("HTTP templates mapped to %q", cfg.TemplateDir) log.Infof("HTTP static content mapped to %q", cfg.PublicDir) Router.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir(cfg.PublicDir)))) http.Handle("/", Router) // Session cookie setup if cfg.CookieAuthKey == "" { log.Infof("HTTP generating random cookie.auth.key") sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64)) } else { log.Tracef("HTTP using configured cookie.auth.key") sessionStore = sessions.NewCookieStore([]byte(cfg.CookieAuthKey)) } }
// 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) } }
// Session specific logging methods func (ss *Session) logTrace(msg string, args ...interface{}) { log.Tracef("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...)) }
// DATA func (ss *Session) dataHandler() { // Timestamp for Received header stamp := time.Now().Format(timeStampFormat) // Get a Mailbox and a new Message for each recipient mailboxes := make([]Mailbox, ss.recipients.Len()) messages := make([]Message, ss.recipients.Len()) msgSize := 0 if ss.server.storeMessages { i := 0 for e := ss.recipients.Front(); e != nil; e = e.Next() { recip := e.Value.(string) local, domain, err := ParseEmailAddress(recip) if err != nil { ss.logError("Failed to parse address for %q", recip) ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", recip)) ss.reset() return } if strings.ToLower(domain) != ss.server.domainNoStore { // Not our "no store" domain, so store the message mb, err := ss.server.dataStore.MailboxFor(local) if err != nil { ss.logError("Failed to open mailbox for %q: %s", local, err) ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", local)) ss.reset() return } mailboxes[i] = mb if messages[i], err = mb.NewMessage(); err != nil { ss.logError("Failed to create message for %q: %s", local, err) ss.send(fmt.Sprintf("451 Failed to create message for %v", local)) ss.reset() return } // Generate Received header 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) 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 { log.Tracef("Not storing message for %q", recip) } i++ } } ss.send("354 Start mail input; end with <CRLF>.<CRLF>") var buf bytes.Buffer for { buf.Reset() err := ss.readByteLine(&buf) if err != nil { if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { ss.send("221 Idle timeout, bye bye") } } ss.logWarn("Error: %v while reading", err) ss.enterState(QUIT) return } line := buf.Bytes() if string(line) == ".\r\n" { // Mail data complete if ss.server.storeMessages { for _, m := range messages { if m != nil { if err := m.Close(); err != nil { // This logic should be updated to report failures // writing the initial message file to the client // after we implement a single-store system (issue // #23) ss.logError("Error: %v while writing message", err) } expReceivedTotal.Add(1) } } } else { expReceivedTotal.Add(1) } ss.send("250 Mail accepted for delivery") ss.logInfo("Message size %v bytes", msgSize) ss.reset() return } // SMTP RFC says remove leading periods from input if len(line) > 0 && line[0] == '.' { line = line[1:] } msgSize += len(line) if msgSize > ss.server.maxMessageBytes { // Max message size exceeded ss.send("552 Maximum message size exceeded") ss.logWarn("Max message size exceeded while in DATA") ss.reset() // Should really cleanup the crap on filesystem (after issue #23) return } // Append to message objects if ss.server.storeMessages { for i, m := range messages { if m != nil { if err := m.Append(line); err != nil { ss.logError("Failed to append to mailbox %v: %v", mailboxes[i], err) ss.send("554 Something went wrong") ss.reset() // Should really cleanup the crap on filesystem (after issue #23) return } } } } } }