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.Password = os.Getenv("MP_SMTP_RELAY_PASSWORD")
config.SMTPRelayConfig.Secret = os.Getenv("MP_SMTP_RELAY_SECRET") config.SMTPRelayConfig.Secret = os.Getenv("MP_SMTP_RELAY_SECRET")
config.SMTPRelayConfig.ReturnPath = os.Getenv("MP_SMTP_RELAY_RETURN_PATH") 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.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS")
config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS") config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS")

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/mail"
"net/url" "net/url"
"os" "os"
"path" "path"
@ -204,6 +205,7 @@ 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"` // allow overriding the bounce address 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 AllowedRecipients string `yaml:"allowed-recipients"` // regex, if set needs to match for mails to be relayed
AllowedRecipientsRegexp *regexp.Regexp // compiled regexp using AllowedRecipients AllowedRecipientsRegexp *regexp.Regexp // compiled regexp using AllowedRecipients
BlockedRecipients string `yaml:"blocked-recipients"` // regex, if set prevents relating to these addresses 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) 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 return nil
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
) )
func autoRelayMessage(from string, to []string, data *[]byte) { 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 { if err = c.Mail(from); err != nil {
return fmt.Errorf("error response to MAIL command: %s", err.Error()) return fmt.Errorf("error response to MAIL command: %s", err.Error())
} }

View File

@ -6,6 +6,7 @@ import (
"bytes" "bytes"
"net/mail" "net/mail"
"regexp" "regexp"
"strings"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
) )
@ -97,3 +98,63 @@ func UpdateMessageHeader(msg []byte, header, value string) ([]byte, error) {
return msg, nil 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 AllowedRecipients string
// Block relaying to these recipients (regex) // Block relaying to these recipients (regex)
BlockedRecipients string BlockedRecipients string
// Overrides the "From" address for all relayed messages
OverrideFrom string
// DEPRECATED 2024/03/12 // DEPRECATED 2024/03/12
// swagger:ignore // swagger:ignore
RecipientAllowlist string RecipientAllowlist string
@ -111,6 +113,7 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath
conf.MessageRelay.AllowedRecipients = config.SMTPRelayConfig.AllowedRecipients conf.MessageRelay.AllowedRecipients = config.SMTPRelayConfig.AllowedRecipients
conf.MessageRelay.BlockedRecipients = config.SMTPRelayConfig.BlockedRecipients conf.MessageRelay.BlockedRecipients = config.SMTPRelayConfig.BlockedRecipients
conf.MessageRelay.OverrideFrom = config.SMTPRelayConfig.OverrideFrom
// DEPRECATED 2024/03/12 // DEPRECATED 2024/03/12
conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.AllowedRecipients conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.AllowedRecipients
} }

View File

@ -86,7 +86,13 @@ export default {
<div class="row"> <div class="row">
<label class="col-sm-2 col-form-label text-body-secondary">From</label> <label class="col-sm-2 col-form-label text-body-secondary">From</label>
<div class="col-sm-10"> <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 : ''"> :value="message.From ? message.From.Address : ''">
</div> </div>
</div> </div>
@ -125,15 +131,17 @@ export default {
</div> </div>
</div> </div>
<h6>Notes</h6> <h6>Notes</h6>
<ul> <ul>
<li v-if="mailbox.uiConfig.MessageRelay.AllowedRecipients != ''" class="form-text"> <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> <code>{{ mailbox.uiConfig.MessageRelay.AllowedRecipients }}</code>
</li> </li>
<li v-if="mailbox.uiConfig.MessageRelay.BlockedRecipients != ''" class="form-text"> <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> <code>{{ mailbox.uiConfig.MessageRelay.BlockedRecipients }}</code>
</li> </li>
<li class="form-text"> <li class="form-text">
@ -142,8 +150,8 @@ export default {
<li class="form-text"> <li class="form-text">
SMTP delivery failures will bounce back to SMTP delivery failures will bounce back to
<code v-if="mailbox.uiConfig.MessageRelay.ReturnPath != ''"> <code v-if="mailbox.uiConfig.MessageRelay.ReturnPath != ''">
{{ mailbox.uiConfig.MessageRelay.ReturnPath }} {{ mailbox.uiConfig.MessageRelay.ReturnPath }}
</code> </code>
<code v-else>{{ message.ReturnPath }}</code>. <code v-else>{{ message.ReturnPath }}</code>.
</li> </li>
</ul> </ul>

View File

@ -1948,6 +1948,10 @@
"description": "Whether message relaying (release) is enabled", "description": "Whether message relaying (release) is enabled",
"type": "boolean" "type": "boolean"
}, },
"OverrideFrom": {
"description": "Overrides the \"From\" address for all relayed messages",
"type": "string"
},
"ReturnPath": { "ReturnPath": {
"description": "Enforced Return-Path (if set) for relay bounces", "description": "Enforced Return-Path (if set) for relay bounces",
"type": "string" "type": "string"