Secure SMTP listener

This commit is contained in:
Aine
2022-09-07 21:29:52 +03:00
parent 715ec1ef2a
commit 59ed33638b
7 changed files with 172 additions and 30 deletions

View File

@@ -11,7 +11,7 @@ It can't be used with arbitrary email providers, but setup your own provider "wi
### Receive ### Receive
- [x] SMTP server - [x] SMTP server (plaintext and SSL)
- [x] Matrix bot - [x] Matrix bot
- [x] Configuration in room's account data - [x] Configuration in room's account data
- [x] Receive emails to matrix rooms - [x] Receive emails to matrix rooms
@@ -43,11 +43,15 @@ env vars
* **POSTMOOGLE_LOGIN** - user login/localpart, eg: `moogle` * **POSTMOOGLE_LOGIN** - user login/localpart, eg: `moogle`
* **POSTMOOGLE_PASSWORD** - user password * **POSTMOOGLE_PASSWORD** - user password
* **POSTMOOGLE_DOMAIN** - SMTP domain to listen for new emails * **POSTMOOGLE_DOMAIN** - SMTP domain to listen for new emails
* **POSTMOOGLE_PORT** - SMTP port to listen for new emails
<details> <details>
<summary>other optional config parameters</summary> <summary>other optional config parameters</summary>
* **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_NOENCRYPTION** - disable encryption support
* **POSTMOOGLE_STATUSMSG** - presence status message * **POSTMOOGLE_STATUSMSG** - presence status message
* **POSTMOOGLE_SENTRY_DSN** - sentry DSN * **POSTMOOGLE_SENTRY_DSN** - sentry DSN

View File

@@ -20,8 +20,9 @@ import (
) )
var ( var (
mxb *bot.Bot mxb *bot.Bot
log *logger.Logger smtpserv *smtp.Server
log *logger.Logger
) )
func main() { func main() {
@@ -38,11 +39,13 @@ func main() {
log.Debug("starting internal components...") log.Debug("starting internal components...")
initSentry(cfg) initSentry(cfg)
initBot(cfg) initBot(cfg)
initSMTP(cfg)
initShutdown(quit) initShutdown(quit)
defer recovery() defer recovery()
go startBot(cfg.StatusMsg) 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 //nolint:gocritic
log.Fatal("SMTP server crashed: %v", err) log.Fatal("SMTP server crashed: %v", err)
} }
@@ -91,6 +94,20 @@ func initBot(cfg *config.Config) {
log.Debug("bot has been created") 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{}) { func initShutdown(quit chan struct{}) {
listener := make(chan os.Signal, 1) listener := make(chan os.Signal, 1)
signal.Notify(listener, os.Interrupt, syscall.SIGABRT, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 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() { func shutdown() {
log.Info("Shutting down...") log.Info("Shutting down...")
smtpserv.Stop()
mxb.Stop() mxb.Stop()
sentry.Flush(5 * time.Second) sentry.Flush(5 * time.Second)

View File

@@ -21,6 +21,12 @@ func New() *Config {
MaxSize: env.Int("maxsize", defaultConfig.MaxSize), MaxSize: env.Int("maxsize", defaultConfig.MaxSize),
StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg), StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg),
Admins: env.Slice("admins"), 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{ Sentry: Sentry{
DSN: env.String("sentry.dsn", defaultConfig.Sentry.DSN), DSN: env.String("sentry.dsn", defaultConfig.Sentry.DSN),
}, },

View File

@@ -11,4 +11,7 @@ var defaultConfig = &Config{
DSN: "local.db", DSN: "local.db",
Dialect: "sqlite3", Dialect: "sqlite3",
}, },
TLS: TLS{
Port: "587",
},
} }

View File

@@ -28,6 +28,9 @@ type Config struct {
// DB config // DB config
DB DB DB DB
// TLS config
TLS TLS
// Sentry config // Sentry config
Sentry Sentry Sentry Sentry
} }
@@ -40,6 +43,14 @@ type DB struct {
Dialect string Dialect string
} }
// TLS config
type TLS struct {
Cert string
Key string
Port string
Required bool
}
// Sentry config // Sentry config
type Sentry struct { type Sentry struct {
DSN string DSN string

View File

@@ -2,8 +2,6 @@ package smtp
import ( import (
"context" "context"
"os"
"time"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/getsentry/sentry-go" "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) { func (m *msa) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
return m.newSession(), nil 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()
}

125
smtp/server.go Normal file
View File

@@ -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}}
}