From 59ed33638b2715492a08a4870b4a5ae6c4d88355 Mon Sep 17 00:00:00 2001 From: Aine Date: Wed, 7 Sep 2022 21:29:52 +0300 Subject: [PATCH 1/3] Secure SMTP listener --- README.md | 8 ++- cmd/cmd.go | 24 +++++++-- config/config.go | 6 +++ config/defaults.go | 3 ++ config/types.go | 11 ++++ smtp/msa.go | 25 --------- smtp/server.go | 125 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 smtp/server.go diff --git a/README.md b/README.md index ec74e8e..74ebf15 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It can't be used with arbitrary email providers, but setup your own provider "wi ### Receive -- [x] SMTP server +- [x] SMTP server (plaintext and SSL) - [x] Matrix bot - [x] Configuration in room's account data - [x] Receive emails to matrix rooms @@ -43,11 +43,15 @@ env vars * **POSTMOOGLE_LOGIN** - user login/localpart, eg: `moogle` * **POSTMOOGLE_PASSWORD** - user password * **POSTMOOGLE_DOMAIN** - SMTP domain to listen for new emails -* **POSTMOOGLE_PORT** - SMTP port to listen for new emails
other optional config parameters +* **POSTMOOGLE_PORT** - SMTP port to listen for new emails +* **POSTMOOGLE_TLS_PORT** - secure SMTP port to listen for new emails. Requires valid cert and key as well +* **POSTMOOGLE_TLS_CERT** - path to your SSL certificate (chain) +* **POSTMOOGLE_TLS_KEY** - path to your SSL certificate's private key +* **POSTMOOGLE_TLS_REQUIRED** - require TLS connection * **POSTMOOGLE_NOENCRYPTION** - disable encryption support * **POSTMOOGLE_STATUSMSG** - presence status message * **POSTMOOGLE_SENTRY_DSN** - sentry DSN diff --git a/cmd/cmd.go b/cmd/cmd.go index 9d578bd..e176ecc 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -20,8 +20,9 @@ import ( ) var ( - mxb *bot.Bot - log *logger.Logger + mxb *bot.Bot + smtpserv *smtp.Server + log *logger.Logger ) func main() { @@ -38,11 +39,13 @@ func main() { log.Debug("starting internal components...") initSentry(cfg) initBot(cfg) + initSMTP(cfg) initShutdown(quit) defer recovery() go startBot(cfg.StatusMsg) - if err := smtp.Start(cfg.Domain, cfg.Port, cfg.LogLevel, cfg.MaxSize, mxb); err != nil { + + if err := smtpserv.Start(); err != nil { //nolint:gocritic log.Fatal("SMTP server crashed: %v", err) } @@ -91,6 +94,20 @@ func initBot(cfg *config.Config) { log.Debug("bot has been created") } +func initSMTP(cfg *config.Config) { + smtpserv = smtp.NewServer(&smtp.Config{ + Domain: cfg.Domain, + Port: cfg.Port, + TLSCert: cfg.TLS.Cert, + TLSKey: cfg.TLS.Key, + TLSPort: cfg.TLS.Port, + TLSRequired: cfg.TLS.Required, + LogLevel: cfg.LogLevel, + MaxSize: cfg.MaxSize, + Bot: mxb, + }) +} + func initShutdown(quit chan struct{}) { listener := make(chan os.Signal, 1) signal.Notify(listener, os.Interrupt, syscall.SIGABRT, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) @@ -114,6 +131,7 @@ func startBot(statusMsg string) { func shutdown() { log.Info("Shutting down...") + smtpserv.Stop() mxb.Stop() sentry.Flush(5 * time.Second) diff --git a/config/config.go b/config/config.go index 82950da..68fc369 100644 --- a/config/config.go +++ b/config/config.go @@ -21,6 +21,12 @@ func New() *Config { MaxSize: env.Int("maxsize", defaultConfig.MaxSize), StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg), Admins: env.Slice("admins"), + TLS: TLS{ + Cert: env.String("tls.cert", defaultConfig.TLS.Cert), + Key: env.String("tls.key", defaultConfig.TLS.Key), + Required: env.Bool("tls.required"), + Port: env.String("tls.port", defaultConfig.TLS.Port), + }, Sentry: Sentry{ DSN: env.String("sentry.dsn", defaultConfig.Sentry.DSN), }, diff --git a/config/defaults.go b/config/defaults.go index 7c7f546..c9cff1e 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -11,4 +11,7 @@ var defaultConfig = &Config{ DSN: "local.db", Dialect: "sqlite3", }, + TLS: TLS{ + Port: "587", + }, } diff --git a/config/types.go b/config/types.go index 50ca24b..9bf6352 100644 --- a/config/types.go +++ b/config/types.go @@ -28,6 +28,9 @@ type Config struct { // DB config DB DB + // TLS config + TLS TLS + // Sentry config Sentry Sentry } @@ -40,6 +43,14 @@ type DB struct { Dialect string } +// TLS config +type TLS struct { + Cert string + Key string + Port string + Required bool +} + // Sentry config type Sentry struct { DSN string diff --git a/smtp/msa.go b/smtp/msa.go index 94922ba..dd18f92 100644 --- a/smtp/msa.go +++ b/smtp/msa.go @@ -2,8 +2,6 @@ package smtp import ( "context" - "os" - "time" "github.com/emersion/go-smtp" "github.com/getsentry/sentry-go" @@ -33,26 +31,3 @@ func (m *msa) Login(state *smtp.ConnectionState, username, password string) (smt func (m *msa) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { return m.newSession(), nil } - -func Start(domain, port, loglevel string, maxSize int, bot Bot) error { - log := logger.New("smtp.", loglevel) - sender := NewMTA(loglevel) - receiver := &msa{ - log: log, - bot: bot, - domain: domain, - } - receiver.bot.SetMTA(sender) - s := smtp.NewServer(receiver) - s.Addr = ":" + port - s.Domain = domain - s.ReadTimeout = 10 * time.Second - s.WriteTimeout = 10 * time.Second - s.MaxMessageBytes = maxSize * 1024 * 1024 - if log.GetLevel() == "DEBUG" || log.GetLevel() == "TRACE" { - s.Debug = os.Stdout - } - - log.Info("Starting SMTP server on %s:%s", domain, port) - return s.ListenAndServe() -} diff --git a/smtp/server.go b/smtp/server.go new file mode 100644 index 0000000..e0f0216 --- /dev/null +++ b/smtp/server.go @@ -0,0 +1,125 @@ +package smtp + +import ( + "crypto/tls" + "net" + "os" + "time" + + "github.com/emersion/go-smtp" + "gitlab.com/etke.cc/go/logger" +) + +type Config struct { + Domain string + Port string + + TLSCert string + TLSKey string + TLSPort string + TLSRequired bool + + LogLevel string + MaxSize int + Bot Bot +} + +type Server struct { + log *logger.Logger + msa *smtp.Server + errs chan error + + port string + tlsPort string + tlsCfg *tls.Config +} + +// NewServer creates new SMTP server +func NewServer(cfg *Config) *Server { + log := logger.New("smtp/msa.", cfg.LogLevel) + sender := NewMTA(cfg.LogLevel) + receiver := &msa{ + log: log, + bot: cfg.Bot, + domain: cfg.Domain, + } + receiver.bot.SetMTA(sender) + + s := smtp.NewServer(receiver) + s.Domain = cfg.Domain + s.ReadTimeout = 10 * time.Second + s.WriteTimeout = 10 * time.Second + s.MaxMessageBytes = cfg.MaxSize * 1024 * 1024 + s.EnableREQUIRETLS = cfg.TLSRequired + if log.GetLevel() == "DEBUG" || log.GetLevel() == "TRACE" { + s.Debug = os.Stdout + } + + server := &Server{ + msa: s, + log: log, + port: cfg.Port, + tlsPort: cfg.TLSPort, + } + server.loadTLSConfig(cfg.TLSCert, cfg.TLSKey) + return server +} + +// Start SMTP server +func (s *Server) Start() error { + s.errs = make(chan error, 1) + go s.listen(s.port, nil) + if s.tlsCfg != nil { + go s.listen(s.tlsPort, s.tlsCfg) + } + + return <-s.errs +} + +// Stop SMTP server +func (s *Server) Stop() { + err := s.msa.Close() + if err != nil { + s.log.Error("cannot stop SMTP server properly: %v", err) + } + s.log.Info("SMTP server has been stopped") +} + +func (s *Server) listen(port string, tlsCfg *tls.Config) { + var l net.Listener + var err error + if tlsCfg != nil { + l, err = tls.Listen("tcp", ":"+port, tlsCfg) + } else { + l, err = net.Listen("tcp", ":"+port) + } + if err != nil { + s.log.Error("cannot start listener on %s: %v", port, err) + s.errs <- err + close(s.errs) + return + } + + s.log.Info("Starting SMTP server on port %s", port) + + err = s.msa.Serve(l) + if err != nil { + s.log.Error("cannot start SMTP server on %s: %v", port, err) + s.errs <- err + close(s.errs) + } +} + +func (s *Server) loadTLSConfig(cert, key string) { + if cert == "" || key == "" { + s.log.Warn("SSL certificate is not provided") + return + } + + tlsCert, err := tls.LoadX509KeyPair(cert, key) + if err != nil { + s.log.Error("cannot load SSL certificate: %v", err) + return + } + s.tlsCfg = &tls.Config{Certificates: []tls.Certificate{tlsCert}} +} From eb88b74ff786245e3116394daeeeb8dc0b5f3e46 Mon Sep 17 00:00:00 2001 From: Aine Date: Thu, 8 Sep 2022 11:44:49 +0300 Subject: [PATCH 2/3] remove close() on error --- smtp/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/smtp/server.go b/smtp/server.go index e0f0216..e1753ff 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -96,7 +96,6 @@ func (s *Server) listen(port string, tlsCfg *tls.Config) { if err != nil { s.log.Error("cannot start listener on %s: %v", port, err) s.errs <- err - close(s.errs) return } From 613767a86d84c789c76445525b79336c0e9c04c8 Mon Sep 17 00:00:00 2001 From: Aine Date: Thu, 8 Sep 2022 11:49:59 +0300 Subject: [PATCH 3/3] update POSTMOOGLE_TLS_REQUIRED comment --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74ebf15..dd2a1b0 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ env vars * **POSTMOOGLE_TLS_PORT** - secure SMTP port to listen for new emails. Requires valid cert and key as well * **POSTMOOGLE_TLS_CERT** - path to your SSL certificate (chain) * **POSTMOOGLE_TLS_KEY** - path to your SSL certificate's private key -* **POSTMOOGLE_TLS_REQUIRED** - require TLS connection +* **POSTMOOGLE_TLS_REQUIRED** - require TLS connection, **even** on the non-TLS port (`POSTMOOGLE_PORT`). TLS connections are always required on the TLS port (`POSTMOOGLE_TLS_PORT`) regardless of this setting. * **POSTMOOGLE_NOENCRYPTION** - disable encryption support * **POSTMOOGLE_STATUSMSG** - presence status message * **POSTMOOGLE_SENTRY_DSN** - sentry DSN