diff --git a/README.md b/README.md index f9ea46b912..865d3284c2 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster. - Configurable automatic email pruning (default keeps the most recent 500 emails) - Email storage either in a temporary or persistent database ([see wiki](https://github.com/axllent/mailpit/wiki/Email-storage)) - Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size, easily handling tens of thousands of emails -- SMTP relaying / message release - relay messages via a different SMTP server ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-relay)) +- SMTP relaying / message release - relay messages via a different SMTP server including a allowlist of accepted recipients ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-relay)) - Optional SMTP with STARTTLS & SMTP authentication, including an "accept anything" mode ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication)) - Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS)) - Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication)) diff --git a/config/config.go b/config/config.go index de65bab74d..dbc9a82534 100644 --- a/config/config.go +++ b/config/config.go @@ -111,15 +111,17 @@ type AutoTag struct { // SMTPRelayConfigStruct struct for parsing yaml & storing variables type smtpRelayConfigStruct struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - STARTTLS bool `yaml:"starttls"` - AllowInsecure bool `yaml:"allow-insecure"` - Auth string `yaml:"auth"` // none, plain, cram-md5 - Username string `yaml:"username"` // plain & cram-md5 - Password string `yaml:"password"` // plain - Secret string `yaml:"secret"` // cram-md5 - ReturnPath string `yaml:"return-path"` // allows overriding the boune address + Host string `yaml:"host"` + Port int `yaml:"port"` + STARTTLS bool `yaml:"starttls"` + AllowInsecure bool `yaml:"allow-insecure"` + Auth string `yaml:"auth"` // none, plain, cram-md5 + Username string `yaml:"username"` // plain & cram-md5 + Password string `yaml:"password"` // plain + Secret string `yaml:"secret"` // cram-md5 + ReturnPath string `yaml:"return-path"` // allows overriding the boune address + RecipientAllowlist string `yaml:"recipient-allowlist"` // regex, if set needs to match for mails to be relayed + RecipientAllowlistRegexp *regexp.Regexp } // VerifyConfig wil do some basic checking @@ -296,6 +298,18 @@ func parseRelayConfig(c string) error { logger.Log().Infof("[smtp] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port) + allowlistRegexp, err := regexp.Compile(SMTPRelayConfig.RecipientAllowlist) + + if SMTPRelayConfig.RecipientAllowlist != "" { + if err != nil { + return fmt.Errorf("failed to compile recipient allowlist regexp: %e", err) + } + + SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp + logger.Log().Infof("[smtp] recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist) + + } + return nil } diff --git a/server/apiv1/api.go b/server/apiv1/api.go index 260d6db52c..b1a019db3d 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -554,10 +554,17 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) { } for _, to := range tos { - if _, err := mail.ParseAddress(to); err != nil { + address, err := mail.ParseAddress(to) + + if err != nil { httpError(w, "Invalid email address: "+to) return } + + if config.SMTPRelayConfig.RecipientAllowlistRegexp != nil && !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) { + httpError(w, "Mail address does not match allowlist: "+to) + return + } } reader := bytes.NewReader(msg) diff --git a/server/apiv1/webui.go b/server/apiv1/webui.go index 60c0a39b71..c586944469 100644 --- a/server/apiv1/webui.go +++ b/server/apiv1/webui.go @@ -20,6 +20,8 @@ type webUIConfiguration struct { SMTPServer string // Enforced Return-Path (if set) for relay bounces ReturnPath string + // Allowlist of accepted recipients + RecipientAllowlist string } } @@ -45,6 +47,7 @@ func WebUIConfig(w http.ResponseWriter, r *http.Request) { if config.ReleaseEnabled { conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath + conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.RecipientAllowlist } bytes, _ := json.Marshal(conf) diff --git a/server/smtpd/smtp.go b/server/smtpd/smtp.go index 34eaac3e5a..7f2c3bacbe 100644 --- a/server/smtpd/smtp.go +++ b/server/smtpd/smtp.go @@ -3,13 +3,45 @@ package smtpd import ( "crypto/tls" "fmt" - "net/smtp" - "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/utils/logger" + "net/mail" + "net/smtp" ) +func allowedRecipients(to []string) []string { + if config.SMTPRelayConfig.RecipientAllowlistRegexp == nil { + return to + } + + var ar []string + + for _, recipient := range to { + address, err := mail.ParseAddress(recipient) + + if err != nil { + logger.Log().Warnf("ignoring invalid email address: %s", recipient) + continue + } + + if !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) { + logger.Log().Debugf("[smtp] not allowed to relay to %s: does not match the allowlist %s", recipient, config.SMTPRelayConfig.RecipientAllowlist) + } else { + ar = append(ar, recipient) + } + } + + return ar +} + // Send will connect to a pre-configured SMTP server and send a message to one or more recipients. func Send(from string, to []string, msg []byte) error { + recipients := allowedRecipients(to) + + if len(recipients) == 0 { + return nil + } + addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) c, err := smtp.Dial(addr) @@ -48,7 +80,7 @@ func Send(from string, to []string, msg []byte) error { return err } - for _, addr := range to { + for _, addr := range recipients { if err = c.Rcpt(addr); err != nil { return err } diff --git a/server/ui-src/templates/MessageRelease.vue b/server/ui-src/templates/MessageRelease.vue index 2ac453200b..6c4e019de9 100644 --- a/server/ui-src/templates/MessageRelease.vue +++ b/server/ui-src/templates/MessageRelease.vue @@ -83,6 +83,11 @@ export default {
Invalid email address
+
+ Note: A recipient allowlist has been configured. Any mail address not matching it will be rejected. +
+ Configured allowlist: {{ relayConfig.MessageRelay.RecipientAllowlist }} +
Note: For testing purposes, a unique Message-Id will be generated on send.
diff --git a/storage/database.go b/storage/database.go index 3c4637fd43..d4ec9265a3 100644 --- a/storage/database.go +++ b/storage/database.go @@ -470,7 +470,7 @@ func GetMessage(id string) (*Message, error) { messageID := strings.Trim(env.GetHeader("Message-ID"), "<>") returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>") - if returnPath == "" { + if returnPath == "" && from != nil { returnPath = from.Address }