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

Feature: Option to override the From email address in SMTP relay configuration (#414)

This commit is contained in:
Ralph Slooten 2025-01-26 00:22:57 +13:00
parent f278933bb9
commit a95bc3d29f
7 changed files with 104 additions and 6 deletions

View File

@ -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")

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,13 @@ export default {
<div class="row">
<label class="col-sm-2 col-form-label text-body-secondary">From</label>
<div class="col-sm-10">
<input type="text" aria-label="From address" readonly class="form-control-plaintext"
<div v-if="mailbox.uiConfig.MessageRelay.OverrideFrom != ''" class="form-control-plaintext">
{{ mailbox.uiConfig.MessageRelay.OverrideFrom }}
<span class="text-muted small ms-2">
* address overridden by the relay configuration.
</span>
</div>
<input v-else type="text" aria-label="From address" readonly class="form-control-plaintext"
:value="message.From ? message.From.Address : ''">
</div>
</div>
@ -129,11 +135,13 @@ export default {
<h6>Notes</h6>
<ul>
<li v-if="mailbox.uiConfig.MessageRelay.AllowedRecipients != ''" class="form-text">
A recipient <b>allowlist</b> has been configured. Any mail address not matching the following will be rejected:
A recipient <b>allowlist</b> has been configured. Any mail address not matching the
following will be rejected:
<code>{{ mailbox.uiConfig.MessageRelay.AllowedRecipients }}</code>
</li>
<li v-if="mailbox.uiConfig.MessageRelay.BlockedRecipients != ''" class="form-text">
A recipient <b>blocklist</b> has been configured. Any mail address matching the following will be rejected:
A recipient <b>blocklist</b> has been configured. Any mail address matching the following
will be rejected:
<code>{{ mailbox.uiConfig.MessageRelay.BlockedRecipients }}</code>
</li>
<li class="form-text">

View File

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