Merge branch 'feature/relay-host' into 'main'

Added support for sending with relay hosts

See merge request etke.cc/postmoogle!43
This commit is contained in:
Aine
2023-05-10 19:33:06 +00:00
7 changed files with 153 additions and 36 deletions

View File

@@ -70,6 +70,10 @@ env vars
* **POSTMOOGLE_MAILBOXES_ACTIVATION** - activation flow for new mailboxes, [docs/mailboxes.md](docs/mailboxes.md) * **POSTMOOGLE_MAILBOXES_ACTIVATION** - activation flow for new mailboxes, [docs/mailboxes.md](docs/mailboxes.md)
* **POSTMOOGLE_MAXSIZE** - max email size (including attachments) in megabytes * **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_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) You can find default values in [config/defaults.go](config/defaults.go)

View File

@@ -143,6 +143,12 @@ func initSMTP(cfg *config.Config) {
MaxSize: cfg.MaxSize, MaxSize: cfg.MaxSize,
Bot: mxb, Bot: mxb,
Callers: []smtp.Caller{mxb, q}, Callers: []smtp.Caller{mxb, q},
Relay: smtp.RelayConfig{
Host: cfg.Relay.Host,
Port: cfg.Relay.Port,
Usename: cfg.Relay.Username,
Password: cfg.Relay.Password,
},
}) })
} }

View File

@@ -46,6 +46,12 @@ func New() *Config {
DSN: env.String("db.dsn", defaultConfig.DB.DSN), DSN: env.String("db.dsn", defaultConfig.DB.DSN),
Dialect: env.String("db.dialect", defaultConfig.DB.Dialect), 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 return cfg

View File

@@ -41,6 +41,8 @@ type Config struct {
// Monitoring config // Monitoring config
Monitoring Monitoring Monitoring Monitoring
Relay Relay
} }
// DB config // DB config
@@ -72,3 +74,11 @@ type Mailboxes struct {
Reserved []string Reserved []string
Activation string Activation string
} }
// Relay config
type Relay struct {
Host string
Port string
Username string
Password string
}

107
smtp/client.go Normal file
View File

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

View File

@@ -29,6 +29,7 @@ type Config struct {
MaxSize int MaxSize int
Bot matrixbot Bot matrixbot
Callers []Caller Callers []Caller
Relay RelayConfig
} }
type TLSConfig struct { type TLSConfig struct {
@@ -40,6 +41,13 @@ type TLSConfig struct {
Mu sync.Mutex Mu sync.Mutex
} }
type RelayConfig struct {
Host string
Port string
Usename string
Password string
}
type Manager struct { type Manager struct {
log *logger.Logger log *logger.Logger
bot matrixbot bot matrixbot
@@ -71,10 +79,14 @@ type Caller interface {
// NewManager creates new SMTP server manager // NewManager creates new SMTP server manager
func NewManager(cfg *Config) *Manager { func NewManager(cfg *Config) *Manager {
log := logger.New("smtp.", cfg.LogLevel) log := logger.New("smtp.", cfg.LogLevel)
smtpClient := newClient(&cfg.Relay, log)
mailsrv := &mailServer{ mailsrv := &mailServer{
log: log, log: log,
bot: cfg.Bot, bot: cfg.Bot,
domains: cfg.Domains, domains: cfg.Domains,
mailSender: smtpClient,
} }
for _, caller := range cfg.Callers { for _, caller := range cfg.Callers {
caller.SetSendmail(mailsrv.SendEmail) caller.SetSendmail(mailsrv.SendEmail)

View File

@@ -2,13 +2,10 @@ package smtp
import ( import (
"context" "context"
"io"
"strings"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"gitlab.com/etke.cc/go/logger" "gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/go/trysmtp"
"gitlab.com/etke.cc/postmoogle/email" "gitlab.com/etke.cc/postmoogle/email"
) )
@@ -32,6 +29,7 @@ type mailServer struct {
bot matrixbot bot matrixbot
log *logger.Logger log *logger.Logger
domains []string domains []string
mailSender MailSender
} }
// Login used for outgoing mail submissions only (when you use postmoogle as smtp server in your scripts) // 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 // SendEmail to external mail server
func (m *mailServer) SendEmail(from, to, data string) error { func (m *mailServer) SendEmail(from, to, data string) error {
m.log.Debug("Sending email from %s to %s", from, to) return m.mailSender.Send(from, to, data)
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
} }
// ReceiveEmail - incoming mail into matrix room // ReceiveEmail - incoming mail into matrix room