Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Recipient allowlist for SMTP relay configuration #109

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
32 changes: 23 additions & 9 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
9 changes: 8 additions & 1 deletion server/apiv1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions server/apiv1/webui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -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)
Expand Down
38 changes: 35 additions & 3 deletions server/smtpd/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions server/ui-src/templates/MessageRelease.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export default {
<div class="invalid-feedback">Invalid email address</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">
Note: For testing purposes, a unique Message-Id will be generated on send.
<br class="d-none d-md-inline">
Expand Down
2 changes: 1 addition & 1 deletion storage/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down