コード例 #1
0
ファイル: retention.go プロジェクト: jhillyerd/inbucket
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)
}
コード例 #2
0
ファイル: server.go プロジェクト: jhillyerd/inbucket
// 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)
	}
}
コード例 #3
0
ファイル: server.go プロジェクト: jhillyerd/inbucket
// 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)
	}
}
コード例 #4
0
ファイル: listener.go プロジェクト: jhillyerd/inbucket
// 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")
}
コード例 #5
0
ファイル: retention.go プロジェクト: jhillyerd/inbucket
// 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
}
コード例 #6
0
ファイル: template.go プロジェクト: jhillyerd/inbucket
// 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
}
コード例 #7
0
ファイル: listener.go プロジェクト: jhillyerd/inbucket
// 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)
	}
}
コード例 #8
0
ファイル: filestore.go プロジェクト: jhillyerd/inbucket
// 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
}
コード例 #9
0
ファイル: filestore.go プロジェクト: jhillyerd/inbucket
// 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
}
コード例 #10
0
ファイル: filestore.go プロジェクト: jhillyerd/inbucket
// 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())
}
コード例 #11
0
ファイル: server.go プロジェクト: jhillyerd/inbucket
// 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))
	}
}
コード例 #12
0
ファイル: listener.go プロジェクト: jhillyerd/inbucket
// 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)
	}
}
コード例 #13
0
ファイル: handler.go プロジェクト: jhillyerd/inbucket
// 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...))
}
コード例 #14
0
ファイル: handler.go プロジェクト: jhillyerd/inbucket
// 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
					}
				}
			}
		}
	}
}