Secure SMTP listener
This commit is contained in:
@@ -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
|
||||||
|
|||||||
24
cmd/cmd.go
24
cmd/cmd.go
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,4 +11,7 @@ var defaultConfig = &Config{
|
|||||||
DSN: "local.db",
|
DSN: "local.db",
|
||||||
Dialect: "sqlite3",
|
Dialect: "sqlite3",
|
||||||
},
|
},
|
||||||
|
TLS: TLS{
|
||||||
|
Port: "587",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
25
smtp/msa.go
25
smtp/msa.go
@@ -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
125
smtp/server.go
Normal 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}}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user