Skip to content

Commit

Permalink
Feature: Option to override the From email address in SMTP relay conf…
Browse files Browse the repository at this point in the history
…iguration (#414)
  • Loading branch information
axllent committed Jan 25, 2025
1 parent f278933 commit a95bc3d
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 6 deletions.
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
11 changes: 11 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"net/mail"
"net/url"
"os"
"path"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
10 changes: 10 additions & 0 deletions internal/smtpd/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
}
Expand Down
61 changes: 61 additions & 0 deletions internal/tools/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"bytes"
"net/mail"
"regexp"
"strings"

"github.com/axllent/mailpit/internal/logger"
)
Expand Down Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions server/apiv1/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
20 changes: 14 additions & 6 deletions server/ui-src/components/message/Release.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -125,15 +131,17 @@ export default {

</div>
</div>

<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">
Expand All @@ -142,8 +150,8 @@ export default {
<li class="form-text">
SMTP delivery failures will bounce back to
<code v-if="mailbox.uiConfig.MessageRelay.ReturnPath != ''">
{{ mailbox.uiConfig.MessageRelay.ReturnPath }}
</code>
{{ mailbox.uiConfig.MessageRelay.ReturnPath }}
</code>
<code v-else>{{ message.ReturnPath }}</code>.
</li>
</ul>
Expand Down
4 changes: 4 additions & 0 deletions server/ui/api/v1/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit a95bc3d

Please sign in to comment.