From a324d817b3f28ca718ad9615a5bf66d26db14d94 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Tue, 12 Mar 2024 17:07:25 +1300 Subject: [PATCH] Feature: Allow setting SMTP relay configuration values via environment variables (#262) --- cmd/root.go | 15 ++++- config/config.go | 66 +++++++++++++------- server/apiv1/api.go | 2 +- server/apiv1/webui.go | 9 ++- server/smtpd/smtp.go | 6 +- server/ui-src/components/message/Release.vue | 4 +- server/ui/api/v1/swagger.json | 8 +-- 7 files changed, 76 insertions(+), 34 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index b5b0443..0866e04 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -79,7 +79,7 @@ func init() { // load and warn deprecated ENV vars initDeprecatedConfigFromEnv() - // load ENV vars + // load environment variables initConfigFromEnv() rootCmd.Flags().StringVarP(&config.DataFile, "db-file", "d", config.DataFile, "Database file to store persistent data") @@ -237,6 +237,19 @@ func initConfigFromEnv() { if getEnabledFromEnv("MP_SMTP_RELAY_ALL") { config.SMTPRelayAllIncoming = true } + config.SMTPRelayConfig = config.SMTPRelayConfigStruct{} + config.SMTPRelayConfig.Host = os.Getenv("MP_SMTP_RELAY_HOST") + if len(os.Getenv("MP_SMTP_RELAY_PORT")) > 0 { + config.SMTPRelayConfig.Port, _ = strconv.Atoi(os.Getenv("MP_SMTP_RELAY_PORT")) + } + config.SMTPRelayConfig.STARTTLS = getEnabledFromEnv("MP_SMTP_RELAY_STARTTLS") + config.SMTPRelayConfig.AllowInsecure = getEnabledFromEnv("MP_SMTP_RELAY_ALLOW_INSECURE") + config.SMTPRelayConfig.Auth = os.Getenv("MP_SMTP_RELAY_AUTH") + config.SMTPRelayConfig.Username = os.Getenv("MP_SMTP_RELAY_USERNAME") + 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.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS") // POP3 server if len(os.Getenv("MP_POP3_BIND_ADDR")) > 0 { diff --git a/config/config.go b/config/config.go index 0efc32c..94d6a70 100644 --- a/config/config.go +++ b/config/config.go @@ -94,7 +94,7 @@ var ( SMTPRelayConfigFile string // SMTPRelayConfig to parse a yaml file and store config of relay SMTP server - SMTPRelayConfig smtpRelayConfigStruct + SMTPRelayConfig SMTPRelayConfigStruct // SMTPStrictRFCHeaders will return an error if the email headers contain (\r\r\n) // @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153 @@ -154,18 +154,20 @@ type AutoTag struct { } // SMTPRelayConfigStruct struct for parsing yaml & storing variables -type smtpRelayConfigStruct struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - STARTTLS bool `yaml:"starttls"` - AllowInsecure bool `yaml:"allow-insecure"` - Auth string `yaml:"auth"` // none, plain, login, cram-md5 - Username string `yaml:"username"` // plain & cram-md5 - Password string `yaml:"password"` // plain - Secret string `yaml:"secret"` // cram-md5 - ReturnPath string `yaml:"return-path"` // allow overriding the bounce address - RecipientAllowlist string `yaml:"recipient-allowlist"` // regex, if set needs to match for mails to be relayed - RecipientAllowlistRegexp *regexp.Regexp +type SMTPRelayConfigStruct struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + STARTTLS bool `yaml:"starttls"` + AllowInsecure bool `yaml:"allow-insecure"` + Auth string `yaml:"auth"` // none, plain, login, cram-md5 + Username string `yaml:"username"` // plain & cram-md5 + Password string `yaml:"password"` // plain + Secret string `yaml:"secret"` // cram-md5 + ReturnPath string `yaml:"return-path"` // allow overriding the bounce address + AllowedRecipients string `yaml:"allowed-recipients"` // regex, if set needs to match for mails to be relayed + AllowedRecipientsRegexp *regexp.Regexp // compiled regexp using AllowedRecipients + // DEPRECATED 2024/03/12 + RecipientAllowlist string `yaml:"recipient-allowlist"` } // VerifyConfig wil do some basic checking @@ -371,6 +373,11 @@ func VerifyConfig() error { return err } + // separate relay config validation to account for environment variables + if err := validateRelayConfig(); err != nil { + return err + } + if !ReleaseEnabled && SMTPRelayAllIncoming { return errors.New("[smtp] relay config must be set to relay all messages") } @@ -383,7 +390,7 @@ func VerifyConfig() error { return nil } -// Parse & validate the SMTPRelayConfigFile (if set) +// Parse the SMTPRelayConfigFile (if set) func parseRelayConfig(c string) error { if c == "" { return nil @@ -408,6 +415,23 @@ func parseRelayConfig(c string) error { return errors.New("[smtp] relay host not set") } + // DEPRECATED 2024/03/12 + if SMTPRelayConfig.RecipientAllowlist != "" { + logger.Log().Warn("[smtp] relay 'recipient-allowlist' is deprecated, use 'allowed_recipients' instead") + if SMTPRelayConfig.AllowedRecipients == "" { + SMTPRelayConfig.AllowedRecipients = SMTPRelayConfig.RecipientAllowlist + } + } + + return nil +} + +// Validate the SMTPRelayConfig (if Host is set) +func validateRelayConfig() error { + if SMTPRelayConfig.Host == "" { + return nil + } + if SMTPRelayConfig.Port == 0 { SMTPRelayConfig.Port = 25 // default } @@ -418,17 +442,17 @@ func parseRelayConfig(c string) error { SMTPRelayConfig.Auth = "none" } else if SMTPRelayConfig.Auth == "plain" { if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" { - return fmt.Errorf("[smtp] relay host username or password not set for PLAIN authentication (%s)", c) + return fmt.Errorf("[smtp] relay host username or password not set for PLAIN authentication") } } else if SMTPRelayConfig.Auth == "login" { SMTPRelayConfig.Auth = "login" if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" { - return fmt.Errorf("[smtp] relay host username or password not set for LOGIN authentication (%s)", c) + return fmt.Errorf("[smtp] relay host username or password not set for LOGIN authentication") } } else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") { SMTPRelayConfig.Auth = "cram-md5" if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" { - return fmt.Errorf("[smtp] relay host username or secret not set for CRAM-MD5 authentication (%s)", c) + return fmt.Errorf("[smtp] relay host username or secret not set for CRAM-MD5 authentication") } } else { return fmt.Errorf("[smtp] relay authentication method not supported: %s", SMTPRelayConfig.Auth) @@ -438,15 +462,15 @@ func parseRelayConfig(c string) error { logger.Log().Infof("[smtp] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port) - allowlistRegexp, err := regexp.Compile(SMTPRelayConfig.RecipientAllowlist) + allowlistRegexp, err := regexp.Compile(SMTPRelayConfig.AllowedRecipients) - if SMTPRelayConfig.RecipientAllowlist != "" { + if SMTPRelayConfig.AllowedRecipients != "" { if err != nil { return fmt.Errorf("[smtp] failed to compile relay recipient allowlist regexp: %s", err.Error()) } - SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp - logger.Log().Infof("[smtp] relay recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist) + SMTPRelayConfig.AllowedRecipientsRegexp = allowlistRegexp + logger.Log().Infof("[smtp] relay recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.AllowedRecipients) } diff --git a/server/apiv1/api.go b/server/apiv1/api.go index cf185e6..9c30abe 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -642,7 +642,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) { return } - if config.SMTPRelayConfig.RecipientAllowlistRegexp != nil && !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) { + if config.SMTPRelayConfig.AllowedRecipientsRegexp != nil && !config.SMTPRelayConfig.AllowedRecipientsRegexp.MatchString(address.Address) { httpError(w, "Mail address does not match allowlist: "+to) return } diff --git a/server/apiv1/webui.go b/server/apiv1/webui.go index c8f4107..0b134c3 100644 --- a/server/apiv1/webui.go +++ b/server/apiv1/webui.go @@ -20,7 +20,10 @@ type webUIConfiguration struct { SMTPServer string // Enforced Return-Path (if set) for relay bounces ReturnPath string - // Allowlist of accepted recipients + // Only allow relaying to these recipients (regex) + AllowedRecipients string + // DEPRECATED 2024/03/12 + // swagger:ignore RecipientAllowlist string } @@ -57,7 +60,9 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) { if config.ReleaseEnabled { conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port) conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath - conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.RecipientAllowlist + conf.MessageRelay.AllowedRecipients = config.SMTPRelayConfig.AllowedRecipients + // DEPRECATED 2024/03/12 + conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.AllowedRecipients } conf.DisableHTMLCheck = config.DisableHTMLCheck diff --git a/server/smtpd/smtp.go b/server/smtpd/smtp.go index 25f20bb..863c256 100644 --- a/server/smtpd/smtp.go +++ b/server/smtpd/smtp.go @@ -12,7 +12,7 @@ import ( ) func allowedRecipients(to []string) []string { - if config.SMTPRelayConfig.RecipientAllowlistRegexp == nil { + if config.SMTPRelayConfig.AllowedRecipientsRegexp == nil { return to } @@ -26,8 +26,8 @@ func allowedRecipients(to []string) []string { continue } - if !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) { - logger.Log().Debugf("[smtp] not allowed to relay to %s: does not match the allowlist %s", recipient, config.SMTPRelayConfig.RecipientAllowlist) + 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) } diff --git a/server/ui-src/components/message/Release.vue b/server/ui-src/components/message/Release.vue index 12a9043..799bb35 100644 --- a/server/ui-src/components/message/Release.vue +++ b/server/ui-src/components/message/Release.vue @@ -126,10 +126,10 @@ export default { -
+
Note: A recipient allowlist has been configured. Any mail address not matching it will be rejected.
- Configured allowlist: {{ mailbox.uiConfig.MessageRelay.RecipientAllowlist }} + Allowed recipients: {{ mailbox.uiConfig.MessageRelay.AllowedRecipients }}
Note: For testing purposes, a unique Message-Id will be generated on send. diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index 8b99aab..54dd822 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -1407,14 +1407,14 @@ "description": "Message Relay information", "type": "object", "properties": { + "AllowedRecipients": { + "description": "Only allow relaying to these recipients (regex)", + "type": "string" + }, "Enabled": { "description": "Whether message relaying (release) is enabled", "type": "boolean" }, - "RecipientAllowlist": { - "description": "Allowlist of accepted recipients", - "type": "string" - }, "ReturnPath": { "description": "Enforced Return-Path (if set) for relay bounces", "type": "string"