// 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 file.Close() 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 } } writer.Flush() } else { // No messages, delete index+maildir log.LogTrace("Removing mailbox %v", mb.path) return os.RemoveAll(mb.path) } return nil }
// 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) }
// 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) } }
func Stop() { log.LogTrace("HTTP shutdown requested") shutdown = true if listener != nil { listener.Close() } else { log.LogError("HTTP listener was nil during shutdown") } }
// openLogFile creates or appends to the logfile passed on commandline func openLogFile() error { // use specified log file var err error logf, err = os.OpenFile(*logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) if err != nil { return fmt.Errorf("Failed to create %v: %v\n", *logfile, err) } golog.SetOutput(logf) log.LogTrace("Opened new logfile") 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.LogTrace("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.LogTrace("Caching partial %v", name) cachedTemplates[name] = t } else { log.LogTrace("Caching template %v", name) cachedTemplates[name] = t } } return t, 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.LogTrace("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 file.Close() // Decode gob data dec := gob.NewDecoder(bufio.NewReader(file)) for { // TODO Detect EOF 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("While decoding message: %v", err) } msg.mailbox = mb log.LogTrace("Found: %v", msg) mb.messages = append(mb.messages, msg) } mb.indexLoaded = true return nil }
// 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) } } }
// 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 }
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) } } }
// Delete this Message from disk by removing both the gob and 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 } } m.mailbox.writeIndex() if len(m.mailbox.messages) == 0 { // This was the last message, writeIndex() has removed the entire // directory return nil } // There are still messages in the index log.LogTrace("Deleting %v", m.rawPath()) return os.Remove(m.rawPath()) }
func MailboxPurge(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { // Don't have to validate these aren't empty, Gorilla returns 404 name, err := smtpd.ParseMailboxName(ctx.Vars["name"]) if err != nil { return err } mb, err := ctx.DataStore.MailboxFor(name) if err != nil { return fmt.Errorf("MailboxFor('%v'): %v", name, err) } if err := mb.Purge(); err != nil { return fmt.Errorf("Mailbox(%q) Purge: %v", name, err) } log.LogTrace("Purged mailbox for %q", name) if ctx.IsJson { return RenderJson(w, "OK") } w.Header().Set("Content-Type", "text/plain") io.WriteString(w, "OK") return nil }
func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { // Don't have to validate these aren't empty, Gorilla returns 404 name, err := smtpd.ParseMailboxName(ctx.Vars["name"]) if err != nil { return err } mb, err := ctx.DataStore.MailboxFor(name) if err != nil { return fmt.Errorf("Failed to get mailbox for %v: %v", name, err) } messages, err := mb.GetMessages() if err != nil { return fmt.Errorf("Failed to get messages for %v: %v", name, err) } log.LogTrace("Got %v messsages", len(messages)) if ctx.IsJson { jmessages := make([]*JsonMessageHeader, len(messages)) for i, msg := range messages { jmessages[i] = &JsonMessageHeader{ Mailbox: name, Id: msg.Id(), From: msg.From(), Subject: msg.Subject(), Date: msg.Date(), Size: msg.Size(), } } return RenderJson(w, jmessages) } return RenderPartial("mailbox/_list.html", w, map[string]interface{}{ "ctx": ctx, "name": name, "messages": messages, }) }
// closeLogFile closes the current logfile func closeLogFile() error { log.LogTrace("Closing logfile") return logf.Close() }
// Drain causes the caller to block until all active POP3 sessions have finished func (s *Server) Drain() { s.waitgroup.Wait() log.LogTrace("POP3 connections drained") }
// Stop requests the POP3 server closes it's listener func (s *Server) Stop() { log.LogTrace("POP3 shutdown requested, connections will be drained") s.shutdown = true s.listener.Close() }
// 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 specific logging methods func (ses *Session) logTrace(msg string, args ...interface{}) { log.LogTrace("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...)) }
// DATA func (ss *Session) dataHandler() { // Timestamp for Received header stamp := time.Now().Format(STAMP_FMT) // 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) messages[i].Append([]byte(recd)) } else { log.LogTrace("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 { ss.logError("Error: %v while writing message", err) // TODO Report to client? } 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() // TODO: Should really cleanup the crap on filesystem... 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() // TODO: Should really cleanup the crap on filesystem... return } } } } } }