From a95bc3d29f43492a12d82a3146986531a9a2ca8a Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sun, 26 Jan 2025 00:22:57 +1300 Subject: [PATCH] Feature: Option to override the From email address in SMTP relay configuration (#414) --- cmd/root.go | 1 + config/config.go | 11 ++++ internal/smtpd/relay.go | 10 ++++ internal/tools/headers.go | 61 ++++++++++++++++++++ server/apiv1/application.go | 3 + server/ui-src/components/message/Release.vue | 20 +++++-- server/ui/api/v1/swagger.json | 4 ++ 7 files changed, 104 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 9d87503b49..9c7c6333f5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -283,6 +283,7 @@ func initConfigFromEnv() { config.SMTPRelayConfig.Password = os.Getenv("MP_SMTP_RELAY_PASSWORD") config.SMTPRelayConfig.Secret = os.Getenv("MP_SMTP_RELAY_SECRET") config.SMTPRelayConfig.ReturnPath = os.Getenv("MP_SMTP_RELAY_RETURN_PATH") + config.SMTPRelayConfig.OverrideFrom = os.Getenv("MP_SMTP_RELAY_OVERRIDE_FROM") config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS") config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS") diff --git a/config/config.go b/config/config.go index b82afb1fed..bbe6713339 100644 --- a/config/config.go +++ b/config/config.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "net/mail" "net/url" "os" "path" @@ -204,6 +205,7 @@ type SMTPRelayConfigStruct struct { Password string `yaml:"password"` // plain Secret string `yaml:"secret"` // cram-md5 ReturnPath string `yaml:"return-path"` // allow overriding the bounce address + OverrideFrom string `yaml:"override-from"` // allow overriding of the from address AllowedRecipients string `yaml:"allowed-recipients"` // regex, if set needs to match for mails to be relayed AllowedRecipientsRegexp *regexp.Regexp // compiled regexp using AllowedRecipients BlockedRecipients string `yaml:"blocked-recipients"` // regex, if set prevents relating to these addresses @@ -611,6 +613,15 @@ func validateRelayConfig() error { logger.Log().Infof("[smtp] relay recipient blocklist is active with the following regexp: %s", SMTPRelayConfig.BlockedRecipients) } + if SMTPRelayConfig.OverrideFrom != "" { + m, err := mail.ParseAddress(SMTPRelayConfig.OverrideFrom) + if err != nil { + return fmt.Errorf("[smtp] relay override-from is not a valid email address: %s", SMTPRelayConfig.OverrideFrom) + } + + SMTPRelayConfig.OverrideFrom = m.Address + } + return nil } diff --git a/internal/smtpd/relay.go b/internal/smtpd/relay.go index 6522c7c97f..4b7d80a484 100644 --- a/internal/smtpd/relay.go +++ b/internal/smtpd/relay.go @@ -9,6 +9,7 @@ import ( "github.com/axllent/mailpit/config" "github.com/axllent/mailpit/internal/logger" + "github.com/axllent/mailpit/internal/tools" ) func autoRelayMessage(from string, to []string, data *[]byte) { @@ -86,6 +87,15 @@ func Relay(from string, to []string, msg []byte) error { } } + if config.SMTPRelayConfig.OverrideFrom != "" { + msg, err = tools.OverrideFromHeader(msg, config.SMTPRelayConfig.OverrideFrom) + if err != nil { + return fmt.Errorf("error overriding From header: %s", err.Error()) + } + + from = config.SMTPRelayConfig.OverrideFrom + } + if err = c.Mail(from); err != nil { return fmt.Errorf("error response to MAIL command: %s", err.Error()) } diff --git a/internal/tools/headers.go b/internal/tools/headers.go index 02d0ff86cb..d802089497 100644 --- a/internal/tools/headers.go +++ b/internal/tools/headers.go @@ -6,6 +6,7 @@ import ( "bytes" "net/mail" "regexp" + "strings" "github.com/axllent/mailpit/internal/logger" ) @@ -97,3 +98,63 @@ func UpdateMessageHeader(msg []byte, header, value string) ([]byte, error) { return msg, nil } + +// OverrideFromHeader scans a message for the From header and replaces it with a different email address. +func OverrideFromHeader(msg []byte, address string) ([]byte, error) { + reader := bytes.NewReader(msg) + m, err := mail.ReadMessage(reader) + if err != nil { + return nil, err + } + + if m.Header.Get("From") != "" { + reBlank := regexp.MustCompile(`^\s+`) + reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta("From:")) + + scanner := bufio.NewScanner(bytes.NewReader(msg)) + found := false + hdr := []byte("") + for scanner.Scan() { + line := scanner.Bytes() + if !found && reHdr.Match(line) { + // add the first line starting with
: + hdr = append(hdr, line...) + hdr = append(hdr, []byte("\r\n")...) + found = true + } else if found && reBlank.Match(line) { + // add any following lines starting with a whitespace (tab or space) + hdr = append(hdr, line...) + hdr = append(hdr, []byte("\r\n")...) + } else if found { + // stop scanning, we have the full
+ break + } + } + + if len(hdr) > 0 { + originalFrom := strings.TrimRight(string(hdr[5:]), "\r\n") + + from, err := mail.ParseAddress(originalFrom) + if err != nil { + // error parsing the from address, so just replace the whole line + msg = bytes.Replace(msg, hdr, []byte("From: "+address+"\r\n"), 1) + } else { + originalFrom = from.Address + // replace the from email, but keep the original name + from.Address = address + msg = bytes.Replace(msg, hdr, []byte("From: "+from.String()+"\r\n"), 1) + } + + // insert the original From header as X-Original-From + msg = append([]byte("X-Original-From: "+originalFrom+"\r\n"), msg...) + + logger.Log().Debugf("[release] Replaced From email address with %s", address) + } + } else { + // no From header, so add one + msg = append([]byte("From: "+address+"\r\n"), msg...) + logger.Log().Debugf("[release] Added From email: %s", address) + } + + return msg, nil +} diff --git a/server/apiv1/application.go b/server/apiv1/application.go index 66a6d49ba8..991f7b589d 100644 --- a/server/apiv1/application.go +++ b/server/apiv1/application.go @@ -60,6 +60,8 @@ type webUIConfiguration struct { AllowedRecipients string // Block relaying to these recipients (regex) BlockedRecipients string + // Overrides the "From" address for all relayed messages + OverrideFrom string // DEPRECATED 2024/03/12 // swagger:ignore RecipientAllowlist string @@ -111,6 +113,7 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) { conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath conf.MessageRelay.AllowedRecipients = config.SMTPRelayConfig.AllowedRecipients conf.MessageRelay.BlockedRecipients = config.SMTPRelayConfig.BlockedRecipients + conf.MessageRelay.OverrideFrom = config.SMTPRelayConfig.OverrideFrom // DEPRECATED 2024/03/12 conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.AllowedRecipients } diff --git a/server/ui-src/components/message/Release.vue b/server/ui-src/components/message/Release.vue index 6c7b39339d..2c4ded4b1d 100644 --- a/server/ui-src/components/message/Release.vue +++ b/server/ui-src/components/message/Release.vue @@ -86,7 +86,13 @@ export default {
- + {{ mailbox.uiConfig.MessageRelay.OverrideFrom }} + + * address overridden by the relay configuration. + +
+
@@ -125,15 +131,17 @@ export default { - +
Notes
  • - A recipient allowlist has been configured. Any mail address not matching the following will be rejected: + A recipient allowlist has been configured. Any mail address not matching the + following will be rejected: {{ mailbox.uiConfig.MessageRelay.AllowedRecipients }}
  • - A recipient blocklist has been configured. Any mail address matching the following will be rejected: + A recipient blocklist has been configured. Any mail address matching the following + will be rejected: {{ mailbox.uiConfig.MessageRelay.BlockedRecipients }}
  • @@ -142,8 +150,8 @@ export default {
  • SMTP delivery failures will bounce back to - {{ mailbox.uiConfig.MessageRelay.ReturnPath }} - + {{ mailbox.uiConfig.MessageRelay.ReturnPath }} + {{ message.ReturnPath }}.
diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index 32365fe7a2..fdc4f63c36 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -1948,6 +1948,10 @@ "description": "Whether message relaying (release) is enabled", "type": "boolean" }, + "OverrideFrom": { + "description": "Overrides the \"From\" address for all relayed messages", + "type": "string" + }, "ReturnPath": { "description": "Enforced Return-Path (if set) for relay bounces", "type": "string"