Skip to content

Commit a95bc3d

Browse files
committed
Feature: Option to override the From email address in SMTP relay configuration (#414)
1 parent f278933 commit a95bc3d

File tree

7 files changed

+104
-6
lines changed

7 files changed

+104
-6
lines changed

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ func initConfigFromEnv() {
283283
config.SMTPRelayConfig.Password = os.Getenv("MP_SMTP_RELAY_PASSWORD")
284284
config.SMTPRelayConfig.Secret = os.Getenv("MP_SMTP_RELAY_SECRET")
285285
config.SMTPRelayConfig.ReturnPath = os.Getenv("MP_SMTP_RELAY_RETURN_PATH")
286+
config.SMTPRelayConfig.OverrideFrom = os.Getenv("MP_SMTP_RELAY_OVERRIDE_FROM")
286287
config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS")
287288
config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS")
288289

config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"net"
8+
"net/mail"
89
"net/url"
910
"os"
1011
"path"
@@ -204,6 +205,7 @@ type SMTPRelayConfigStruct struct {
204205
Password string `yaml:"password"` // plain
205206
Secret string `yaml:"secret"` // cram-md5
206207
ReturnPath string `yaml:"return-path"` // allow overriding the bounce address
208+
OverrideFrom string `yaml:"override-from"` // allow overriding of the from address
207209
AllowedRecipients string `yaml:"allowed-recipients"` // regex, if set needs to match for mails to be relayed
208210
AllowedRecipientsRegexp *regexp.Regexp // compiled regexp using AllowedRecipients
209211
BlockedRecipients string `yaml:"blocked-recipients"` // regex, if set prevents relating to these addresses
@@ -611,6 +613,15 @@ func validateRelayConfig() error {
611613
logger.Log().Infof("[smtp] relay recipient blocklist is active with the following regexp: %s", SMTPRelayConfig.BlockedRecipients)
612614
}
613615

616+
if SMTPRelayConfig.OverrideFrom != "" {
617+
m, err := mail.ParseAddress(SMTPRelayConfig.OverrideFrom)
618+
if err != nil {
619+
return fmt.Errorf("[smtp] relay override-from is not a valid email address: %s", SMTPRelayConfig.OverrideFrom)
620+
}
621+
622+
SMTPRelayConfig.OverrideFrom = m.Address
623+
}
624+
614625
return nil
615626
}
616627

internal/smtpd/relay.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/axllent/mailpit/config"
1111
"github.com/axllent/mailpit/internal/logger"
12+
"github.com/axllent/mailpit/internal/tools"
1213
)
1314

1415
func autoRelayMessage(from string, to []string, data *[]byte) {
@@ -86,6 +87,15 @@ func Relay(from string, to []string, msg []byte) error {
8687
}
8788
}
8889

90+
if config.SMTPRelayConfig.OverrideFrom != "" {
91+
msg, err = tools.OverrideFromHeader(msg, config.SMTPRelayConfig.OverrideFrom)
92+
if err != nil {
93+
return fmt.Errorf("error overriding From header: %s", err.Error())
94+
}
95+
96+
from = config.SMTPRelayConfig.OverrideFrom
97+
}
98+
8999
if err = c.Mail(from); err != nil {
90100
return fmt.Errorf("error response to MAIL command: %s", err.Error())
91101
}

internal/tools/headers.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"bytes"
77
"net/mail"
88
"regexp"
9+
"strings"
910

1011
"github.com/axllent/mailpit/internal/logger"
1112
)
@@ -97,3 +98,63 @@ func UpdateMessageHeader(msg []byte, header, value string) ([]byte, error) {
9798

9899
return msg, nil
99100
}
101+
102+
// OverrideFromHeader scans a message for the From header and replaces it with a different email address.
103+
func OverrideFromHeader(msg []byte, address string) ([]byte, error) {
104+
reader := bytes.NewReader(msg)
105+
m, err := mail.ReadMessage(reader)
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
if m.Header.Get("From") != "" {
111+
reBlank := regexp.MustCompile(`^\s+`)
112+
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta("From:"))
113+
114+
scanner := bufio.NewScanner(bytes.NewReader(msg))
115+
found := false
116+
hdr := []byte("")
117+
for scanner.Scan() {
118+
line := scanner.Bytes()
119+
if !found && reHdr.Match(line) {
120+
// add the first line starting with <header>:
121+
hdr = append(hdr, line...)
122+
hdr = append(hdr, []byte("\r\n")...)
123+
found = true
124+
} else if found && reBlank.Match(line) {
125+
// add any following lines starting with a whitespace (tab or space)
126+
hdr = append(hdr, line...)
127+
hdr = append(hdr, []byte("\r\n")...)
128+
} else if found {
129+
// stop scanning, we have the full <header>
130+
break
131+
}
132+
}
133+
134+
if len(hdr) > 0 {
135+
originalFrom := strings.TrimRight(string(hdr[5:]), "\r\n")
136+
137+
from, err := mail.ParseAddress(originalFrom)
138+
if err != nil {
139+
// error parsing the from address, so just replace the whole line
140+
msg = bytes.Replace(msg, hdr, []byte("From: "+address+"\r\n"), 1)
141+
} else {
142+
originalFrom = from.Address
143+
// replace the from email, but keep the original name
144+
from.Address = address
145+
msg = bytes.Replace(msg, hdr, []byte("From: "+from.String()+"\r\n"), 1)
146+
}
147+
148+
// insert the original From header as X-Original-From
149+
msg = append([]byte("X-Original-From: "+originalFrom+"\r\n"), msg...)
150+
151+
logger.Log().Debugf("[release] Replaced From email address with %s", address)
152+
}
153+
} else {
154+
// no From header, so add one
155+
msg = append([]byte("From: "+address+"\r\n"), msg...)
156+
logger.Log().Debugf("[release] Added From email: %s", address)
157+
}
158+
159+
return msg, nil
160+
}

server/apiv1/application.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ type webUIConfiguration struct {
6060
AllowedRecipients string
6161
// Block relaying to these recipients (regex)
6262
BlockedRecipients string
63+
// Overrides the "From" address for all relayed messages
64+
OverrideFrom string
6365
// DEPRECATED 2024/03/12
6466
// swagger:ignore
6567
RecipientAllowlist string
@@ -111,6 +113,7 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
111113
conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath
112114
conf.MessageRelay.AllowedRecipients = config.SMTPRelayConfig.AllowedRecipients
113115
conf.MessageRelay.BlockedRecipients = config.SMTPRelayConfig.BlockedRecipients
116+
conf.MessageRelay.OverrideFrom = config.SMTPRelayConfig.OverrideFrom
114117
// DEPRECATED 2024/03/12
115118
conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.AllowedRecipients
116119
}

server/ui-src/components/message/Release.vue

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ export default {
8686
<div class="row">
8787
<label class="col-sm-2 col-form-label text-body-secondary">From</label>
8888
<div class="col-sm-10">
89-
<input type="text" aria-label="From address" readonly class="form-control-plaintext"
89+
<div v-if="mailbox.uiConfig.MessageRelay.OverrideFrom != ''" class="form-control-plaintext">
90+
{{ mailbox.uiConfig.MessageRelay.OverrideFrom }}
91+
<span class="text-muted small ms-2">
92+
* address overridden by the relay configuration.
93+
</span>
94+
</div>
95+
<input v-else type="text" aria-label="From address" readonly class="form-control-plaintext"
9096
:value="message.From ? message.From.Address : ''">
9197
</div>
9298
</div>
@@ -125,15 +131,17 @@ export default {
125131

126132
</div>
127133
</div>
128-
134+
129135
<h6>Notes</h6>
130136
<ul>
131137
<li v-if="mailbox.uiConfig.MessageRelay.AllowedRecipients != ''" class="form-text">
132-
A recipient <b>allowlist</b> has been configured. Any mail address not matching the following will be rejected:
138+
A recipient <b>allowlist</b> has been configured. Any mail address not matching the
139+
following will be rejected:
133140
<code>{{ mailbox.uiConfig.MessageRelay.AllowedRecipients }}</code>
134141
</li>
135142
<li v-if="mailbox.uiConfig.MessageRelay.BlockedRecipients != ''" class="form-text">
136-
A recipient <b>blocklist</b> has been configured. Any mail address matching the following will be rejected:
143+
A recipient <b>blocklist</b> has been configured. Any mail address matching the following
144+
will be rejected:
137145
<code>{{ mailbox.uiConfig.MessageRelay.BlockedRecipients }}</code>
138146
</li>
139147
<li class="form-text">
@@ -142,8 +150,8 @@ export default {
142150
<li class="form-text">
143151
SMTP delivery failures will bounce back to
144152
<code v-if="mailbox.uiConfig.MessageRelay.ReturnPath != ''">
145-
{{ mailbox.uiConfig.MessageRelay.ReturnPath }}
146-
</code>
153+
{{ mailbox.uiConfig.MessageRelay.ReturnPath }}
154+
</code>
147155
<code v-else>{{ message.ReturnPath }}</code>.
148156
</li>
149157
</ul>

server/ui/api/v1/swagger.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,6 +1948,10 @@
19481948
"description": "Whether message relaying (release) is enabled",
19491949
"type": "boolean"
19501950
},
1951+
"OverrideFrom": {
1952+
"description": "Overrides the \"From\" address for all relayed messages",
1953+
"type": "string"
1954+
},
19511955
"ReturnPath": {
19521956
"description": "Enforced Return-Path (if set) for relay bounces",
19531957
"type": "string"

0 commit comments

Comments
 (0)