diff --git a/cmd/root.go b/cmd/root.go index eea0f59..631e6ff 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -116,8 +116,9 @@ func init() { rootCmd.Flags().BoolVar(&smtpd.DisableReverseDNS, "smtp-disable-rdns", smtpd.DisableReverseDNS, "Disable SMTP reverse DNS lookups") // SMTP relay - 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().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP relay configuration file to allow releasing messages") + rootCmd.Flags().BoolVar(&config.SMTPRelayAll, "smtp-relay-all", config.SMTPRelayAll, "Auto-relay all new messages via external SMTP server (caution!)") + rootCmd.Flags().StringVar(&config.SMTPRelayMatching, "smtp-relay-matching", config.SMTPRelayMatching, "Auto-relay new messages to only matching recipients (regular expression)") // POP3 server rootCmd.Flags().StringVar(&config.POP3Listen, "pop3", config.POP3Listen, "POP3 server bind interface and port") @@ -253,8 +254,9 @@ func initConfigFromEnv() { // SMTP relay config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG") if getEnabledFromEnv("MP_SMTP_RELAY_ALL") { - config.SMTPRelayAllIncoming = true + config.SMTPRelayAll = true } + config.SMTPRelayMatching = os.Getenv("MP_SMTP_RELAY_MATCHING") config.SMTPRelayConfig = config.SMTPRelayConfigStruct{} config.SMTPRelayConfig.Host = os.Getenv("MP_SMTP_RELAY_HOST") if len(os.Getenv("MP_SMTP_RELAY_PORT")) > 0 { diff --git a/config/config.go b/config/config.go index 2993753..906e190 100644 --- a/config/config.go +++ b/config/config.go @@ -114,9 +114,15 @@ var ( // ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile ReleaseEnabled = false - // SMTPRelayAllIncoming is whether to relay all incoming messages via pre-configured SMTP server. + // SMTPRelayAll is whether to relay all incoming messages via pre-configured SMTP server. // Use with extreme caution! - SMTPRelayAllIncoming = false + SMTPRelayAll = false + + // SMTPRelayMatching if set, will auto-release to recipients matching this regular expression + SMTPRelayMatching string + + // SMTPRelayMatchingRegexp is the compiled version of SMTPRelayMatching + SMTPRelayMatchingRegexp *regexp.Regexp // POP3Listen address - if set then Mailpit will start the POP3 server and listen on this address POP3Listen = "[::]:1110" @@ -405,7 +411,7 @@ func VerifyConfig() error { } SMTPAllowedRecipientsRegexp = restrictRegexp - logger.Log().Infof("[smtp] only allowing recipients matching the following regexp: %s", SMTPAllowedRecipients) + logger.Log().Infof("[smtp] only allowing recipients matching regexp: %s", SMTPAllowedRecipients) } if err := parseRelayConfig(SMTPRelayConfigFile); err != nil { @@ -417,13 +423,28 @@ func VerifyConfig() error { return err } - if !ReleaseEnabled && SMTPRelayAllIncoming { - return errors.New("[smtp] relay config must be set to relay all messages") + if !ReleaseEnabled && SMTPRelayAll || !ReleaseEnabled && SMTPRelayMatching != "" { + return errors.New("[relay] a relay configuration must be set to auto-relay any messages") } - if SMTPRelayAllIncoming { + if SMTPRelayMatching != "" { + if SMTPRelayAll { + logger.Log().Warnf("[relay] ignoring smtp-relay-matching when smtp-relay-all is enabled") + } else { + restrictRegexp, err := regexp.Compile(SMTPRelayMatching) + if err != nil { + return fmt.Errorf("[relay] failed to compile smtp-relay-matching regexp: %s", err.Error()) + } + + SMTPRelayMatchingRegexp = restrictRegexp + logger.Log().Infof("[relay] auto-relaying new messages to recipients matching \"%s\" via %s:%d", + SMTPRelayMatching, SMTPRelayConfig.Host, SMTPRelayConfig.Port) + } + } + + if SMTPRelayAll { // this deserves a warning - logger.Log().Warnf("[smtp] enabling automatic relay of all new messages via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port) + logger.Log().Warnf("[relay] auto-relaying all new messages via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port) } return nil diff --git a/server/apiv1/api.go b/server/apiv1/api.go index 977290c..24a3a66 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -640,13 +640,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) { return } - tos := data.To - if len(tos) == 0 { - httpError(w, "No valid addresses found") - return - } - - for _, to := range tos { + for _, to := range data.To { address, err := mail.ParseAddress(to) if err != nil { @@ -660,6 +654,11 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) { } } + if len(data.To) == 0 { + httpError(w, "No valid addresses found") + return + } + reader := bytes.NewReader(msg) m, err := mail.ReadMessage(reader) if err != nil { @@ -673,6 +672,11 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) { return } + if len(froms) == 0 { + httpError(w, "No From header found") + return + } + from := froms[0].Address // if sender is used, then change from to the sender @@ -716,7 +720,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) { return } - if err := smtpd.Send(from, tos, msg); err != nil { + if err := smtpd.Send(from, data.To, msg); err != nil { logger.Log().Errorf("[smtp] error sending message: %s", err.Error()) httpError(w, "SMTP error: "+err.Error()) return diff --git a/server/smtpd/relay.go b/server/smtpd/relay.go new file mode 100644 index 0000000..534b2b7 --- /dev/null +++ b/server/smtpd/relay.go @@ -0,0 +1,41 @@ +package smtpd + +import ( + "strings" + + "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/logger" +) + +func autoRelayMessage(from string, to []string, data *[]byte) { + if len(to) == 0 { + return + } + + if config.SMTPRelayAll { + if err := Send(from, to, *data); err != nil { + logger.Log().Errorf("[smtp] error relaying message: %s", err.Error()) + } else { + logger.Log().Debugf("[smtp] auto-relay message to %s from %s via %s:%d", + strings.Join(to, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) + } + } else if config.SMTPRelayMatchingRegexp != nil { + filtered := []string{} + for _, t := range to { + if config.SMTPRelayMatchingRegexp.MatchString(t) { + filtered = append(filtered, t) + } + } + + if len(filtered) == 0 { + return + } + + if err := Send(from, filtered, *data); err != nil { + logger.Log().Errorf("[smtp] error relaying message: %s", err.Error()) + } else { + logger.Log().Debugf("[smtp] auto-relay message to %s from %s via %s:%d", + strings.Join(filtered, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) + } + } +} diff --git a/server/smtpd/smtp.go b/server/smtpd/smtp.go index 863c256..5ab0e8f 100644 --- a/server/smtpd/smtp.go +++ b/server/smtpd/smtp.go @@ -4,46 +4,14 @@ import ( "crypto/tls" "errors" "fmt" - "net/mail" "net/smtp" "github.com/axllent/mailpit/config" "github.com/axllent/mailpit/internal/logger" ) -func allowedRecipients(to []string) []string { - if config.SMTPRelayConfig.AllowedRecipientsRegexp == nil { - return to - } - - var ar []string - - for _, recipient := range to { - address, err := mail.ParseAddress(recipient) - - if err != nil { - logger.Log().Warnf("ignoring invalid email address: %s", recipient) - continue - } - - if !config.SMTPRelayConfig.AllowedRecipientsRegexp.MatchString(address.Address) { - logger.Log().Debugf("[smtp] not allowed to relay to %s: does not match the allowlist %s", recipient, config.SMTPRelayConfig.AllowedRecipients) - } else { - ar = append(ar, recipient) - } - } - - return ar -} - // Send will connect to a pre-configured SMTP server and send a message to one or more recipients. func Send(from string, to []string, msg []byte) error { - recipients := allowedRecipients(to) - - if len(recipients) == 0 { - return errors.New("no valid recipients") - } - addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) c, err := smtp.Dial(addr) @@ -75,7 +43,7 @@ func Send(from string, to []string, msg []byte) error { return fmt.Errorf("error response to MAIL command: %s", err.Error()) } - for _, addr := range recipients { + for _, addr := range to { if err = c.Rcpt(addr); err != nil { logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error()) } diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index 63786e5..169b5e5 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -74,14 +74,8 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error { } } - // if enabled, this will route the email 1:1 through to the preconfigured smtp server - if config.SMTPRelayAllIncoming { - if err := Send(from, to, data); err != nil { - logger.Log().Warnf("[smtp] error relaying message: %s", err.Error()) - } else { - logger.Log().Debugf("[smtp] relayed message from %s via %s:%d", from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) - } - } + // if enabled, this may conditionally relay the email through to the preconfigured smtp server + autoRelayMessage(from, to, &data) // build array of all addresses in the header to compare to the []to array emails, hasBccHeader := scanAddressesInHeader(msg.Header)