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

Feature: Add option to only allow SMTP recipients matching a regular expression (disable open-relay behaviour )

This commit is contained in:
Ralph Slooten 2024-01-03 12:06:36 +13:00
parent aad15945b3
commit cdab59b295
6 changed files with 80 additions and 40 deletions
cmd
config
internal/stats
server
smtpd
ui-src/components
ui/api/v1

@ -103,6 +103,7 @@ func init() {
rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Enable insecure PLAIN & LOGIN authentication") rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Enable insecure PLAIN & LOGIN authentication")
rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain <CR><CR><LF>") rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain <CR><CR><LF>")
rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed") rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed")
rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)")
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages") rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages")
rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)") rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)")
@ -170,6 +171,9 @@ func initConfigFromEnv() {
if len(os.Getenv("MP_SMTP_MAX_RECIPIENTS")) > 0 { if len(os.Getenv("MP_SMTP_MAX_RECIPIENTS")) > 0 {
config.SMTPMaxRecipients, _ = strconv.Atoi(os.Getenv("MP_SMTP_MAX_RECIPIENTS")) config.SMTPMaxRecipients, _ = strconv.Atoi(os.Getenv("MP_SMTP_MAX_RECIPIENTS"))
} }
if len(os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")) > 0 {
config.SMTPAllowedRecipients = os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")
}
// Relay server config // Relay server config
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG") config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")

@ -93,6 +93,12 @@ var (
// @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153 // @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153
SMTPStrictRFCHeaders bool SMTPStrictRFCHeaders bool
// SMTPAllowedRecipients if set, will only accept recipients matching this regular expression
SMTPAllowedRecipients string
// SMTPAllowedRecipientsRegexp is the compiled version of SMTPAllowedRecipients
SMTPAllowedRecipientsRegexp *regexp.Regexp
// ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile // ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile
ReleaseEnabled = false ReleaseEnabled = false
@ -262,6 +268,16 @@ func VerifyConfig() error {
} }
} }
if SMTPAllowedRecipients != "" {
restrictRegexp, err := regexp.Compile(SMTPAllowedRecipients)
if err != nil {
return fmt.Errorf("Failed to compile smtp-allowed-recipients regexp: %s", err.Error())
}
SMTPAllowedRecipientsRegexp = restrictRegexp
logger.Log().Infof("[smtp] only allowing recipients matching the following regexp: %s", SMTPAllowedRecipients)
}
if err := parseRelayConfig(SMTPRelayConfigFile); err != nil { if err := parseRelayConfig(SMTPRelayConfigFile); err != nil {
return err return err
} }
@ -335,11 +351,11 @@ func parseRelayConfig(c string) error {
if SMTPRelayConfig.RecipientAllowlist != "" { if SMTPRelayConfig.RecipientAllowlist != "" {
if err != nil { if err != nil {
return fmt.Errorf("failed to compile recipient allowlist regexp: %e", err) return fmt.Errorf("Failed to compile relay recipient allowlist regexp: %s", err.Error())
} }
SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp
logger.Log().Infof("[smtp] recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist) logger.Log().Infof("[smtp] relay recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist)
} }

@ -21,9 +21,9 @@ var (
mu sync.RWMutex mu sync.RWMutex
smtpReceived int smtpAccepted int
smtpReceivedSize int smtpAcceptedSize int
smtpErrors int smtpRejected int
smtpIgnored int smtpIgnored int
) )
@ -52,13 +52,13 @@ type AppInformation struct {
Memory uint64 Memory uint64
// Messages deleted // Messages deleted
MessagesDeleted int MessagesDeleted int
// SMTP messages received via since run // SMTP accepted messages since run
SMTPReceived int SMTPAccepted int
// Total size in bytes of received messages since run // Total size in bytes of accepted messages since run
SMTPReceivedSize int SMTPAcceptedSize int
// SMTP errors since run // SMTP rejected messages since run
SMTPErrors int SMTPRejected int
// SMTP messages ignored since run (duplicate IDs) // SMTP ignored messages since run (duplicate IDs)
SMTPIgnored int SMTPIgnored int
} }
} }
@ -75,9 +75,9 @@ func Load() AppInformation {
info.RuntimeStats.Uptime = int(time.Since(startedAt).Seconds()) info.RuntimeStats.Uptime = int(time.Since(startedAt).Seconds())
info.RuntimeStats.MessagesDeleted = storage.StatsDeleted info.RuntimeStats.MessagesDeleted = storage.StatsDeleted
info.RuntimeStats.SMTPReceived = smtpReceived info.RuntimeStats.SMTPAccepted = smtpAccepted
info.RuntimeStats.SMTPReceivedSize = smtpReceivedSize info.RuntimeStats.SMTPAcceptedSize = smtpAcceptedSize
info.RuntimeStats.SMTPErrors = smtpErrors info.RuntimeStats.SMTPRejected = smtpRejected
info.RuntimeStats.SMTPIgnored = smtpIgnored info.RuntimeStats.SMTPIgnored = smtpIgnored
if latestVersionCache != "" { if latestVersionCache != "" {
@ -116,18 +116,18 @@ func Track() {
startedAt = time.Now() startedAt = time.Now()
} }
// LogSMTPReceived logs a successfully SMTP transaction // LogSMTPAccepted logs a successful SMTP transaction
func LogSMTPReceived(size int) { func LogSMTPAccepted(size int) {
mu.Lock() mu.Lock()
smtpReceived = smtpReceived + 1 smtpAccepted = smtpAccepted + 1
smtpReceivedSize = smtpReceivedSize + size smtpAcceptedSize = smtpAcceptedSize + size
mu.Unlock() mu.Unlock()
} }
// LogSMTPError logs a failed SMTP transaction // LogSMTPRejected logs a rejected SMTP transaction
func LogSMTPError() { func LogSMTPRejected() {
mu.Lock() mu.Lock()
smtpErrors = smtpErrors + 1 smtpRejected = smtpRejected + 1
mu.Unlock() mu.Unlock()
} }

@ -28,7 +28,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
msg, err := mail.ReadMessage(bytes.NewReader(data)) msg, err := mail.ReadMessage(bytes.NewReader(data))
if err != nil { if err != nil {
logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error()) logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error())
stats.LogSMTPError() stats.LogSMTPRejected()
return err return err
} }
@ -121,11 +121,10 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
_, err = storage.Store(&data) _, err = storage.Store(&data)
if err != nil { if err != nil {
logger.Log().Errorf("[db] error storing message: %s", err.Error()) logger.Log().Errorf("[db] error storing message: %s", err.Error())
stats.LogSMTPError()
return err return err
} }
stats.LogSMTPReceived(len(data)) stats.LogSMTPAccepted(len(data))
data = nil // avoid memory leaks data = nil // avoid memory leaks
@ -153,6 +152,22 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, _ []
return true, nil return true, nil
} }
// HandlerRcpt used to optionally restrict recipients based on `--smtp-allowed-recipients`
func handlerRcpt(remoteAddr net.Addr, from string, to string) bool {
if config.SMTPAllowedRecipientsRegexp == nil {
return true
}
result := config.SMTPAllowedRecipientsRegexp.MatchString(to)
if !result {
logger.Log().Warnf("[smtpd] rejected message to %s from %s (%s)", to, from, cleanIP(remoteAddr))
stats.LogSMTPRejected()
}
return result
}
// Listen starts the SMTPD server // Listen starts the SMTPD server
func Listen() error { func Listen() error {
if config.SMTPAuthAllowInsecure { if config.SMTPAuthAllowInsecure {
@ -178,6 +193,7 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa
srv := &smtpd.Server{ srv := &smtpd.Server{
Addr: addr, Addr: addr,
Handler: handler, Handler: handler,
HandlerRcpt: handlerRcpt,
Appname: "Mailpit", Appname: "Mailpit",
Hostname: "", Hostname: "",
AuthHandler: nil, AuthHandler: nil,

@ -240,19 +240,23 @@ export default {
</tr> </tr>
<tr> <tr>
<td> <td>
SMTP messages received SMTP messages accepted
</td> </td>
<td> <td>
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPReceived) }} {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPAccepted) }}
({{ getFileSize(mailbox.appInfo.RuntimeStats.SMTPReceivedSize) }}) <small class="text-secondary">
({{
getFileSize(mailbox.appInfo.RuntimeStats.SMTPAcceptedSize)
}})
</small>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
SMTP errors SMTP messages rejected
</td> </td>
<td> <td>
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPErrors) }} {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPRejected) }}
</td> </td>
</tr> </tr>
<tr> <tr>

@ -762,23 +762,23 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"SMTPErrors": { "SMTPAccepted": {
"description": "SMTP errors since run", "description": "SMTP accepted messages since run",
"type": "integer",
"format": "int64"
},
"SMTPAcceptedSize": {
"description": "Total size in bytes of accepted messages since run",
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"SMTPIgnored": { "SMTPIgnored": {
"description": "SMTP messages ignored since run (duplicate IDs)", "description": "SMTP ignored messages since run (duplicate IDs)",
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"SMTPReceived": { "SMTPRejected": {
"description": "SMTP messages received via since run", "description": "SMTP rejected messages since run",
"type": "integer",
"format": "int64"
},
"SMTPReceivedSize": {
"description": "Total size in bytes of received messages since run",
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },