diff --git a/cmd/root.go b/cmd/root.go index e972a95..03a3878 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain ") 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().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 { 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 config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG") diff --git a/config/config.go b/config/config.go index c8e7011..15aa286 100644 --- a/config/config.go +++ b/config/config.go @@ -93,6 +93,12 @@ var ( // @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153 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 = 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 { return err } @@ -335,11 +351,11 @@ func parseRelayConfig(c string) error { if SMTPRelayConfig.RecipientAllowlist != "" { 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 - 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) } diff --git a/internal/stats/stats.go b/internal/stats/stats.go index e31ed07..c107926 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -21,9 +21,9 @@ var ( mu sync.RWMutex - smtpReceived int - smtpReceivedSize int - smtpErrors int + smtpAccepted int + smtpAcceptedSize int + smtpRejected int smtpIgnored int ) @@ -52,13 +52,13 @@ type AppInformation struct { Memory uint64 // Messages deleted MessagesDeleted int - // SMTP messages received via since run - SMTPReceived int - // Total size in bytes of received messages since run - SMTPReceivedSize int - // SMTP errors since run - SMTPErrors int - // SMTP messages ignored since run (duplicate IDs) + // SMTP accepted messages since run + SMTPAccepted int + // Total size in bytes of accepted messages since run + SMTPAcceptedSize int + // SMTP rejected messages since run + SMTPRejected int + // SMTP ignored messages since run (duplicate IDs) SMTPIgnored int } } @@ -75,9 +75,9 @@ func Load() AppInformation { info.RuntimeStats.Uptime = int(time.Since(startedAt).Seconds()) info.RuntimeStats.MessagesDeleted = storage.StatsDeleted - info.RuntimeStats.SMTPReceived = smtpReceived - info.RuntimeStats.SMTPReceivedSize = smtpReceivedSize - info.RuntimeStats.SMTPErrors = smtpErrors + info.RuntimeStats.SMTPAccepted = smtpAccepted + info.RuntimeStats.SMTPAcceptedSize = smtpAcceptedSize + info.RuntimeStats.SMTPRejected = smtpRejected info.RuntimeStats.SMTPIgnored = smtpIgnored if latestVersionCache != "" { @@ -116,18 +116,18 @@ func Track() { startedAt = time.Now() } -// LogSMTPReceived logs a successfully SMTP transaction -func LogSMTPReceived(size int) { +// LogSMTPAccepted logs a successful SMTP transaction +func LogSMTPAccepted(size int) { mu.Lock() - smtpReceived = smtpReceived + 1 - smtpReceivedSize = smtpReceivedSize + size + smtpAccepted = smtpAccepted + 1 + smtpAcceptedSize = smtpAcceptedSize + size mu.Unlock() } -// LogSMTPError logs a failed SMTP transaction -func LogSMTPError() { +// LogSMTPRejected logs a rejected SMTP transaction +func LogSMTPRejected() { mu.Lock() - smtpErrors = smtpErrors + 1 + smtpRejected = smtpRejected + 1 mu.Unlock() } diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index ba09058..70d8867 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -28,7 +28,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error { msg, err := mail.ReadMessage(bytes.NewReader(data)) if err != nil { logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error()) - stats.LogSMTPError() + stats.LogSMTPRejected() return err } @@ -121,11 +121,10 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error { _, err = storage.Store(&data) if err != nil { logger.Log().Errorf("[db] error storing message: %s", err.Error()) - stats.LogSMTPError() return err } - stats.LogSMTPReceived(len(data)) + stats.LogSMTPAccepted(len(data)) data = nil // avoid memory leaks @@ -153,6 +152,22 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, _ [] 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 func Listen() error { if config.SMTPAuthAllowInsecure { @@ -178,6 +193,7 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa srv := &smtpd.Server{ Addr: addr, Handler: handler, + HandlerRcpt: handlerRcpt, Appname: "Mailpit", Hostname: "", AuthHandler: nil, diff --git a/server/ui-src/components/AboutMailpit.vue b/server/ui-src/components/AboutMailpit.vue index 06991ed..b2af888 100644 --- a/server/ui-src/components/AboutMailpit.vue +++ b/server/ui-src/components/AboutMailpit.vue @@ -240,19 +240,23 @@ export default { - SMTP messages received + SMTP messages accepted - {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPReceived) }} - ({{ getFileSize(mailbox.appInfo.RuntimeStats.SMTPReceivedSize) }}) + {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPAccepted) }} + + ({{ + getFileSize(mailbox.appInfo.RuntimeStats.SMTPAcceptedSize) + }}) + - SMTP errors + SMTP messages rejected - {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPErrors) }} + {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPRejected) }} diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index afa0773..8aa7adf 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -762,23 +762,23 @@ "type": "integer", "format": "int64" }, - "SMTPErrors": { - "description": "SMTP errors since run", + "SMTPAccepted": { + "description": "SMTP accepted messages since run", + "type": "integer", + "format": "int64" + }, + "SMTPAcceptedSize": { + "description": "Total size in bytes of accepted messages since run", "type": "integer", "format": "int64" }, "SMTPIgnored": { - "description": "SMTP messages ignored since run (duplicate IDs)", + "description": "SMTP ignored messages since run (duplicate IDs)", "type": "integer", "format": "int64" }, - "SMTPReceived": { - "description": "SMTP messages received via since run", - "type": "integer", - "format": "int64" - }, - "SMTPReceivedSize": { - "description": "Total size in bytes of received messages since run", + "SMTPRejected": { + "description": "SMTP rejected messages since run", "type": "integer", "format": "int64" },