From 960119eaaf74a6ab34961254f98ba8de735da3c0 Mon Sep 17 00:00:00 2001
From: Robert Sesek <rsesek@bluestatic.org>
Date: Fri, 7 Apr 2017 00:06:12 -0400
Subject: [PATCH] Support reloading the TLS config via SIGHUP.

- Breaks out running an Accept loop into a goroutine.
- Changes how the servers communicate back to the main event loop,
  using an enum instead of an error object.
---
 README.md     |  5 ++++
 mailpopbox.go | 17 ++++++++++----
 pop3.go       | 65 +++++++++++++++++++++++++++++++++------------------
 server.go     | 36 ++++++++++++++++++++++++++++
 smtp.go       | 54 ++++++++++++++++++++++++++++--------------
 5 files changed, 131 insertions(+), 46 deletions(-)
 create mode 100644 server.go

diff --git a/README.md b/README.md
index 827db6b..e424860 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,11 @@ Mailpopbox is a combination delivery SMTP server and POP mailbox. The purpose is
 catch-all delivery server for an MX domain. All messages that it receives are deposited into a
 single mailbox, which can then be accessed using the POP3 protocol.
 
+## TLS Support
+
+TLS is recommended in production environments. To facilitate live-reloading of certificates, you can
+send a running instance SIGHUP.
+
 ## RFCs
 
 This server implements the following RFCs:
diff --git a/mailpopbox.go b/mailpopbox.go
index ece1e6b..2966d4f 100644
--- a/mailpopbox.go
+++ b/mailpopbox.go
@@ -32,10 +32,17 @@ func main() {
 	pop3 := runPOP3Server(config, log)
 	smtp := runSMTPServer(config, log)
 
-	select {
-	case err := <-pop3:
-		fmt.Println(err)
-	case err := <-smtp:
-		fmt.Println(err)
+	for {
+		select {
+		case cm := <-pop3:
+			if cm == ServerControlRestart {
+				pop3 = runPOP3Server(config, log)
+			} else {
+				break
+			}
+		case <-smtp:
+			// smtp never reloads.
+			break
+		}
 	}
 }
diff --git a/pop3.go b/pop3.go
index 53c4ebc..01b753c 100644
--- a/pop3.go
+++ b/pop3.go
@@ -15,35 +15,64 @@ import (
 	"src.bluestatic.org/mailpopbox/pop3"
 )
 
-func runPOP3Server(config Config, log zap.Logger) <-chan error {
+func runPOP3Server(config Config, log zap.Logger) <-chan ServerControlMessage {
 	server := pop3Server{
-		config: config,
-		rc:     make(chan error),
-		log:    log.With(zap.String("server", "pop3")),
+		config:      config,
+		controlChan: make(chan ServerControlMessage),
+		log:         log.With(zap.String("server", "pop3")),
 	}
 	go server.run()
-	return server.rc
+	return server.controlChan
 }
 
 type pop3Server struct {
-	config Config
-	rc     chan error
-	log    zap.Logger
+	config      Config
+	controlChan chan ServerControlMessage
+	log         zap.Logger
 }
 
 func (server *pop3Server) run() {
 	for _, s := range server.config.Servers {
 		if err := os.Mkdir(s.MaildropPath, 0700); err != nil && !os.IsExist(err) {
 			server.log.Error("failed to open maildrop", zap.Error(err))
-			server.rc <- err
+			server.controlChan <- ServerControlFatalError
 		}
 	}
 
+	l, err := server.newListener()
+	if err != nil {
+		server.controlChan <- ServerControlFatalError
+		return
+	}
+
+	connChan := make(chan net.Conn)
+	go RunAcceptLoop(l, connChan, server.log)
+
+	reloadChan := CreateReloadSignal()
+
+	for {
+		select {
+		case <-reloadChan:
+			server.log.Info("restarting server")
+			l.Close()
+			server.controlChan <- ServerControlRestart
+			break
+		case conn, ok := <-connChan:
+			if ok {
+				go pop3.AcceptConnection(conn, server, server.log)
+			} else {
+				server.controlChan <- ServerControlFatalError
+				break
+			}
+		}
+	}
+}
+
+func (server *pop3Server) newListener() (net.Listener, error) {
 	tlsConfig, err := server.config.GetTLSConfig()
 	if err != nil {
 		server.log.Error("failed to configure TLS", zap.Error(err))
-		server.rc <- err
-		return
+		return nil, err
 	}
 
 	addr := fmt.Sprintf(":%d", server.config.POP3Port)
@@ -57,20 +86,10 @@ func (server *pop3Server) run() {
 	}
 	if err != nil {
 		server.log.Error("listen", zap.Error(err))
-		server.rc <- err
-		return
+		return nil, err
 	}
 
-	for {
-		conn, err := l.Accept()
-		if err != nil {
-			server.log.Error("accept", zap.Error(err))
-			server.rc <- err
-			break
-		}
-
-		go pop3.AcceptConnection(conn, server, server.log)
-	}
+	return l, nil
 }
 
 func (server *pop3Server) Name() string {
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..04990d8
--- /dev/null
+++ b/server.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+	"net"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/uber-go/zap"
+)
+
+type ServerControlMessage int
+
+const (
+	ServerControlFatalError ServerControlMessage = iota
+	ServerControlRestart
+)
+
+func RunAcceptLoop(l net.Listener, c chan<- net.Conn, log zap.Logger) {
+	for {
+		conn, err := l.Accept()
+		if err != nil {
+			log.Error("accept", zap.Error(err))
+			close(c)
+			return
+		}
+
+		c <- conn
+	}
+}
+
+func CreateReloadSignal() <-chan os.Signal {
+	reloadChan := make(chan os.Signal, 1)
+	signal.Notify(reloadChan, syscall.SIGHUP)
+	return reloadChan
+}
diff --git a/smtp.go b/smtp.go
index a4d1c19..ef71dab 100644
--- a/smtp.go
+++ b/smtp.go
@@ -14,14 +14,14 @@ import (
 	"src.bluestatic.org/mailpopbox/smtp"
 )
 
-func runSMTPServer(config Config, log zap.Logger) <-chan error {
+func runSMTPServer(config Config, log zap.Logger) <-chan ServerControlMessage {
 	server := smtpServer{
-		config: config,
-		rc:     make(chan error),
-		log:    log.With(zap.String("server", "smtp")),
+		config:      config,
+		controlChan: make(chan ServerControlMessage),
+		log:         log.With(zap.String("server", "smtp")),
 	}
 	go server.run()
-	return server.rc
+	return server.controlChan
 }
 
 type smtpServer struct {
@@ -30,15 +30,12 @@ type smtpServer struct {
 
 	log zap.Logger
 
-	rc chan error
+	controlChan chan ServerControlMessage
 }
 
 func (server *smtpServer) run() {
-	var err error
-	server.tlsConfig, err = server.config.GetTLSConfig()
-	if err != nil {
-		server.log.Error("failed to configure TLS", zap.Error(err))
-		server.rc <- err
+	if !server.loadTLSConfig() {
+		return
 	}
 
 	addr := fmt.Sprintf(":%d", server.config.SMTPPort)
@@ -47,20 +44,41 @@ func (server *smtpServer) run() {
 	l, err := net.Listen("tcp", addr)
 	if err != nil {
 		server.log.Error("listen", zap.Error(err))
-		server.rc <- err
+		server.controlChan <- ServerControlFatalError
 		return
 	}
 
+	connChan := make(chan net.Conn)
+	go RunAcceptLoop(l, connChan, server.log)
+
+	reloadChan := CreateReloadSignal()
+
 	for {
-		conn, err := l.Accept()
-		if err != nil {
-			server.log.Error("accept", zap.Error(err))
-			server.rc <- err
-			return
+		select {
+		case <-reloadChan:
+			if !server.loadTLSConfig() {
+				return
+			}
+		case conn, ok := <-connChan:
+			if ok {
+				go smtp.AcceptConnection(conn, server, server.log)
+			} else {
+				break
+			}
 		}
+	}
+}
 
-		go smtp.AcceptConnection(conn, server, server.log)
+func (server *smtpServer) loadTLSConfig() bool {
+	var err error
+	server.tlsConfig, err = server.config.GetTLSConfig()
+	if err != nil {
+		server.log.Error("failed to configure TLS", zap.Error(err))
+		server.controlChan <- ServerControlFatalError
+		return false
 	}
+	server.log.Info("loaded TLS config")
+	return true
 }
 
 func (server *smtpServer) Name() string {
-- 
2.43.5