From ee8d8680ac888535f3622d659ba2a0cf637e5861 Mon Sep 17 00:00:00 2001 From: Niels Bouma <14498723-n.bouma@users.noreply.gitlab.com> Date: Wed, 10 May 2023 19:33:05 +0000 Subject: [PATCH] Added support for sending with relay hosts --- README.md | 4 ++ cmd/cmd.go | 6 +++ config/config.go | 6 +++ config/types.go | 10 +++++ smtp/client.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++ smtp/manager.go | 18 ++++++-- smtp/server.go | 38 +++-------------- 7 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 smtp/client.go diff --git a/README.md b/README.md index c55dacf..3f53f68 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ env vars * **POSTMOOGLE_MAILBOXES_ACTIVATION** - activation flow for new mailboxes, [docs/mailboxes.md](docs/mailboxes.md) * **POSTMOOGLE_MAXSIZE** - max email size (including attachments) in megabytes * **POSTMOOGLE_ADMINS** - a space-separated list of admin users. See `POSTMOOGLE_USERS` for syntax examples +* **POSTMOOGLE_RELAY_HOST** - SMTP hostname of relay host (e.g. Sendgrid) +* **POSTMOOGLE_RELAY_PORT** - SMTP port of relay host +* **POSTMOOGLE_RELAY_USERNAME** - Username of relay host +* **POSTMOOGLE_RELAY_PASSWORD** - Password of relay host You can find default values in [config/defaults.go](config/defaults.go) diff --git a/cmd/cmd.go b/cmd/cmd.go index df67ddd..a15ee8c 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -143,6 +143,12 @@ func initSMTP(cfg *config.Config) { MaxSize: cfg.MaxSize, Bot: mxb, Callers: []smtp.Caller{mxb, q}, + Relay: smtp.RelayConfig{ + Host: cfg.Relay.Host, + Port: cfg.Relay.Port, + Usename: cfg.Relay.Username, + Password: cfg.Relay.Password, + }, }) } diff --git a/config/config.go b/config/config.go index 2764e58..e9d7da4 100644 --- a/config/config.go +++ b/config/config.go @@ -46,6 +46,12 @@ func New() *Config { DSN: env.String("db.dsn", defaultConfig.DB.DSN), Dialect: env.String("db.dialect", defaultConfig.DB.Dialect), }, + Relay: Relay{ + Host: env.String("relay.host", defaultConfig.Relay.Host), + Port: env.String("relay.port", defaultConfig.Relay.Port), + Username: env.String("relay.username", defaultConfig.Relay.Username), + Password: env.String("relay.password", defaultConfig.Relay.Password), + }, } return cfg diff --git a/config/types.go b/config/types.go index f39c384..c6771ad 100644 --- a/config/types.go +++ b/config/types.go @@ -41,6 +41,8 @@ type Config struct { // Monitoring config Monitoring Monitoring + + Relay Relay } // DB config @@ -72,3 +74,11 @@ type Mailboxes struct { Reserved []string Activation string } + +// Relay config +type Relay struct { + Host string + Port string + Username string + Password string +} diff --git a/smtp/client.go b/smtp/client.go new file mode 100644 index 0000000..dafc7b2 --- /dev/null +++ b/smtp/client.go @@ -0,0 +1,107 @@ +package smtp + +import ( + "crypto/tls" + "io" + "net/smtp" + "strings" + + "gitlab.com/etke.cc/go/logger" + "gitlab.com/etke.cc/go/trysmtp" +) + +type MailSender interface { + Send(from string, to string, data string) error +} + +type Client struct { + config *RelayConfig + log *logger.Logger +} + +func newClient(cfg *RelayConfig, log *logger.Logger) Client { + return Client{ + config: cfg, + log: log, + } +} + +func (c Client) Send(from string, to string, data string) error { + c.log.Debug("Sending email from %s to %s", from, to) + conn, err := c.createSmtpClient(from, to) + if conn == nil { + c.log.Error("cannot connect to SMTP server of %s: %v", to, err) + return err + } + if err != nil { + c.log.Warn("connection to the SMTP server of %s returned the following non-fatal error(-s): %v", err) + } + defer conn.Close() + + var w io.WriteCloser + w, err = conn.Data() + if err != nil { + c.log.Error("cannot send DATA command: %v", err) + return err + } + defer w.Close() + c.log.Debug("sending DATA:\n%s", data) + _, err = strings.NewReader(data).WriteTo(w) + if err != nil { + c.log.Debug("cannot write DATA: %v", err) + return err + } + + c.log.Debug("email has been sent") + return nil +} + +func (c *Client) createSmtpClient(from string, to string) (*smtp.Client, error) { + if c.config.Host != "" { + return c.createDirectClient(from, to) + } + + return trysmtp.Connect(from, to) +} + +func (c *Client) createDirectClient(from string, to string) (*smtp.Client, error) { + localname := strings.SplitN(from, "@", 2)[1] + target := c.config.Host + ":" + c.config.Port + conn, err := smtp.Dial(target) + + if err != nil { + return nil, err + } + + err = conn.Hello(localname) + if err != nil { + return nil, err + } + + if ok, _ := conn.Extension("STARTTLS"); ok { + config := &tls.Config{ServerName: c.config.Host} + conn.StartTLS(config) //nolint:errcheck // if it doesn't work - we can't do anything anyway + } + + if c.config.Usename != "" { + err = conn.Auth(smtp.PlainAuth("", c.config.Usename, c.config.Password, c.config.Host)) + if err != nil { + conn.Close() + return nil, err + } + } + + err = conn.Mail(from) + if err != nil { + conn.Close() + return nil, err + } + + err = conn.Rcpt(to) + if err != nil { + conn.Close() + return nil, err + } + + return conn, nil +} diff --git a/smtp/manager.go b/smtp/manager.go index 518d276..fe7159a 100644 --- a/smtp/manager.go +++ b/smtp/manager.go @@ -29,6 +29,7 @@ type Config struct { MaxSize int Bot matrixbot Callers []Caller + Relay RelayConfig } type TLSConfig struct { @@ -40,6 +41,13 @@ type TLSConfig struct { Mu sync.Mutex } +type RelayConfig struct { + Host string + Port string + Usename string + Password string +} + type Manager struct { log *logger.Logger bot matrixbot @@ -71,10 +79,14 @@ type Caller interface { // NewManager creates new SMTP server manager func NewManager(cfg *Config) *Manager { log := logger.New("smtp.", cfg.LogLevel) + + smtpClient := newClient(&cfg.Relay, log) + mailsrv := &mailServer{ - log: log, - bot: cfg.Bot, - domains: cfg.Domains, + log: log, + bot: cfg.Bot, + domains: cfg.Domains, + mailSender: smtpClient, } for _, caller := range cfg.Callers { caller.SetSendmail(mailsrv.SendEmail) diff --git a/smtp/server.go b/smtp/server.go index 0016341..35bf1a5 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -2,13 +2,10 @@ package smtp import ( "context" - "io" - "strings" "github.com/emersion/go-smtp" "github.com/getsentry/sentry-go" "gitlab.com/etke.cc/go/logger" - "gitlab.com/etke.cc/go/trysmtp" "gitlab.com/etke.cc/postmoogle/email" ) @@ -29,9 +26,10 @@ var ( ) type mailServer struct { - bot matrixbot - log *logger.Logger - domains []string + bot matrixbot + log *logger.Logger + domains []string + mailSender MailSender } // Login used for outgoing mail submissions only (when you use postmoogle as smtp server in your scripts) @@ -91,33 +89,7 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, // SendEmail to external mail server func (m *mailServer) SendEmail(from, to, data string) error { - m.log.Debug("Sending email from %s to %s", from, to) - conn, err := trysmtp.Connect(from, to) - if conn == nil { - m.log.Error("cannot connect to SMTP server of %s: %v", to, err) - return err - } - if err != nil { - m.log.Warn("connection to the SMTP server of %s returned the following non-fatal error(-s): %v", err) - } - defer conn.Close() - - var w io.WriteCloser - w, err = conn.Data() - if err != nil { - m.log.Error("cannot send DATA command: %v", err) - return err - } - defer w.Close() - m.log.Debug("sending DATA:\n%s", data) - _, err = strings.NewReader(data).WriteTo(w) - if err != nil { - m.log.Debug("cannot write DATA: %v", err) - return err - } - - m.log.Debug("email has been sent") - return nil + return m.mailSender.Send(from, to, data) } // ReceiveEmail - incoming mail into matrix room