1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-04-23 12:18:56 +02:00

Merge branch 'feature/recipient-allowlist' into develop

This commit is contained in:
Ralph Slooten 2023-05-05 22:11:00 +12:00
commit 3b75bf3fa3
8 changed files with 78 additions and 15 deletions

View File

@ -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) - 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)) - 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 - 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 an optional 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 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 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)) - Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))

View File

@ -120,6 +120,8 @@ type smtpRelayConfigStruct struct {
Password string `yaml:"password"` // plain Password string `yaml:"password"` // plain
Secret string `yaml:"secret"` // cram-md5 Secret string `yaml:"secret"` // cram-md5
ReturnPath string `yaml:"return-path"` // allows overriding the boune address 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 // 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) 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 return nil
} }

View File

@ -554,10 +554,17 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
} }
for _, to := range tos { 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) httpError(w, "Invalid email address: "+to)
return 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) reader := bytes.NewReader(msg)

View File

@ -20,6 +20,8 @@ type webUIConfiguration struct {
SMTPServer string SMTPServer string
// Enforced Return-Path (if set) for relay bounces // Enforced Return-Path (if set) for relay bounces
ReturnPath string ReturnPath string
// Allowlist of accepted recipients
RecipientAllowlist string
} }
} }
@ -45,6 +47,7 @@ func WebUIConfig(w http.ResponseWriter, r *http.Request) {
if config.ReleaseEnabled { if config.ReleaseEnabled {
conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath
conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.RecipientAllowlist
} }
bytes, _ := json.Marshal(conf) bytes, _ := json.Marshal(conf)

View File

@ -2,14 +2,48 @@ package smtpd
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net/mail"
"net/smtp" "net/smtp"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
) )
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. // 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 { func Send(from string, to []string, msg []byte) error {
recipients := allowedRecipients(to)
if len(recipients) == 0 {
return errors.New("no valid recipients")
}
addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
c, err := smtp.Dial(addr) c, err := smtp.Dial(addr)
@ -48,7 +82,7 @@ func Send(from string, to []string, msg []byte) error {
return err return err
} }
for _, addr := range to { for _, addr := range recipients {
if err = c.Rcpt(addr); err != nil { if err = c.Rcpt(addr); err != nil {
return err return err
} }

View File

@ -42,7 +42,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
// if enabled, this will route the email 1:1 through to the preconfigured smtp server // if enabled, this will route the email 1:1 through to the preconfigured smtp server
if config.SMTPRelayAllIncoming { if config.SMTPRelayAllIncoming {
if err := Send(from, to, data); err != nil { if err := Send(from, to, data); err != nil {
logger.Log().Errorf("[smtp] error relaying message: %s", err.Error()) logger.Log().Warnf("[smtp] error relaying message: %s", err.Error())
} else { } else {
logger.Log().Debugf("[smtp] relayed message from %s via %s:%d", from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) logger.Log().Debugf("[smtp] relayed message from %s via %s:%d", from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
} }

View File

@ -83,6 +83,11 @@ export default {
<div class="invalid-feedback">Invalid email address</div> <div class="invalid-feedback">Invalid email address</div>
</div> </div>
</div> </div>
<div class="form-text text-center" v-if="relayConfig.MessageRelay.RecipientAllowlist != ''">
Note: A recipient allowlist has been configured. Any mail address not matching it will be rejected.
<br class="d-none d-md-inline">
Configured allowlist: <b>{{ relayConfig.MessageRelay.RecipientAllowlist }}</b>
</div>
<div class="form-text text-center"> <div class="form-text text-center">
Note: For testing purposes, a unique Message-Id will be generated on send. Note: For testing purposes, a unique Message-Id will be generated on send.
<br class="d-none d-md-inline"> <br class="d-none d-md-inline">

View File

@ -470,7 +470,7 @@ func GetMessage(id string) (*Message, error) {
messageID := strings.Trim(env.GetHeader("Message-ID"), "<>") messageID := strings.Trim(env.GetHeader("Message-ID"), "<>")
returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>") returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>")
if returnPath == "" { if returnPath == "" && from != nil {
returnPath = from.Address returnPath = from.Address
} }
@ -491,7 +491,7 @@ func GetMessage(id string) (*Message, error) {
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id) logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
date = time.UnixMicro(created) date = time.UnixMilli(created)
}); err != nil { }); err != nil {
logger.Log().Error(err) logger.Log().Error(err)
} }