From 73a92a39520dc467d3db6b447be296d50c4c7862 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sun, 12 Mar 2023 10:51:49 +1300 Subject: [PATCH 1/4] Feature: Options to support auth without STARTTLS, and accept any login @see #56 --- cmd/root.go | 117 +++++++++++++++++++++++++++--------------- config/config.go | 14 ++++- server/smtpd/smtpd.go | 51 ++++++++++++++---- 3 files changed, 130 insertions(+), 52 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 9afd61d..5b26efc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -74,47 +74,7 @@ func init() { rootCmd.PersistentFlags().BoolP("help", "h", false, "This help") rootCmd.PersistentFlags().Lookup("help").Hidden = true - // defaults from envars if provided - if len(os.Getenv("MP_DATA_FILE")) > 0 { - config.DataFile = os.Getenv("MP_DATA_FILE") - } - if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 { - config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR") - } - if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 { - config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR") - } - if len(os.Getenv("MP_MAX_MESSAGES")) > 0 { - config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES")) - } - if len(os.Getenv("MP_TAG")) > 0 { - config.SMTPCLITags = os.Getenv("MP_TAG") - } - if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 { - config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE") - } - if len(os.Getenv("MP_UI_SSL_CERT")) > 0 { - config.UISSLCert = os.Getenv("MP_UI_SSL_CERT") - } - if len(os.Getenv("MP_UI_SSL_KEY")) > 0 { - config.UISSLKey = os.Getenv("MP_UI_SSL_KEY") - } - if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 { - config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE") - } - if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 { - config.SMTPSSLCert = os.Getenv("MP_SMTP_SSL_CERT") - } - if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 { - config.SMTPSSLKey = os.Getenv("MP_SMTP_SSL_KEY") - } - if len(os.Getenv("MP_WEBROOT")) > 0 { - config.Webroot = os.Getenv("MP_WEBROOT") - } - if len(os.Getenv("MP_USE_MESSAGE_DATES")) > 0 { - v := strings.ToLower(os.Getenv("MP_USE_MESSAGE_DATES")) - config.UseMessageDates = v != "0" && v != "false" && v != "yes" - } + initConfigFromEnv() // deprecated 2022/08/06 if len(os.Getenv("MP_AUTH_FILE")) > 0 { @@ -149,8 +109,10 @@ func init() { rootCmd.Flags().StringVar(&config.UISSLKey, "ui-ssl-key", config.UISSLKey, "SSL key for web UI - requires ui-ssl-cert") rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication") + rootCmd.Flags().BoolVar(&config.SMTPAuthAcceptAny, "smtp-auth-accept-any", false, "Accept any SMTP username and password, including none") rootCmd.Flags().StringVar(&config.SMTPSSLCert, "smtp-ssl-cert", config.SMTPSSLCert, "SSL certificate for SMTP - requires smtp-ssl-key") rootCmd.Flags().StringVar(&config.SMTPSSLKey, "smtp-ssl-key", config.SMTPSSLKey, "SSL key for SMTP - requires smtp-ssl-cert") + rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", false, "Enable insecure PLAIN & LOGIN authentication") rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters") rootCmd.Flags().BoolVarP(&config.QuietLogging, "quiet", "q", false, "Quiet logging (errors only)") @@ -172,3 +134,76 @@ func init() { rootCmd.Flags().Lookup("data").Hidden = true rootCmd.Flags().Lookup("data").Deprecated = "use --db-file" } + +// Load settings from environment +func initConfigFromEnv() { + // defaults from envars if provided + if len(os.Getenv("MP_DATA_FILE")) > 0 { + config.DataFile = os.Getenv("MP_DATA_FILE") + } + if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 { + config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR") + } + if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 { + config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR") + } + if len(os.Getenv("MP_MAX_MESSAGES")) > 0 { + config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES")) + } + if len(os.Getenv("MP_TAG")) > 0 { + config.SMTPCLITags = os.Getenv("MP_TAG") + } + + // UI + if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 { + config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE") + } + if len(os.Getenv("MP_UI_SSL_CERT")) > 0 { + config.UISSLCert = os.Getenv("MP_UI_SSL_CERT") + } + if len(os.Getenv("MP_UI_SSL_KEY")) > 0 { + config.UISSLKey = os.Getenv("MP_UI_SSL_KEY") + } + + // SMTP + if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 { + config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE") + } + if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 { + config.SMTPSSLCert = os.Getenv("MP_SMTP_SSL_CERT") + } + if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 { + config.SMTPSSLKey = os.Getenv("MP_SMTP_SSL_KEY") + } + if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") { + config.SMTPAuthAcceptAny = true + } + if getEnabledFromEnv("MP_SMTP_AUTH_ALLOW_INSECURE") { + config.SMTPAuthAllowInsecure = true + } + + if len(os.Getenv("MP_WEBROOT")) > 0 { + config.Webroot = os.Getenv("MP_WEBROOT") + } + if getEnabledFromEnv("MP_USE_MESSAGE_DATES") { + config.UseMessageDates = true + } + if getEnabledFromEnv("MP_USE_MESSAGE_DATES") { + config.UseMessageDates = true + } + if getEnabledFromEnv("MP_QUIET") { + config.QuietLogging = true + } + if getEnabledFromEnv("MP_VERBOSE") { + config.VerboseLogging = true + } +} + +func getEnabledFromEnv(k string) bool { + if len(os.Getenv(k)) > 0 { + v := strings.ToLower(os.Getenv(k)) + return v == "1" || v == "true" || v == "yes" + } + + return false +} diff --git a/config/config.go b/config/config.go index ea012a7..5592d68 100644 --- a/config/config.go +++ b/config/config.go @@ -66,6 +66,12 @@ var ( // SMTPAuth used for euthentication SMTPAuth *htpasswd.File + // SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication + SMTPAuthAllowInsecure bool + + // SMTPAuthAcceptAny accepts any username/password including none + SMTPAuthAcceptAny bool + // SMTPCLITags is used to map the CLI args SMTPCLITags string @@ -153,8 +159,8 @@ func VerifyConfig() error { return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile) } - if SMTPSSLCert == "" { - return errors.New("SMTP authentication requires SMTP encryption") + if SMTPAuthAcceptAny { + return errors.New("SMTP authentication can either use --smtp-auth-file or --smtp-auth-accept-any") } a, err := htpasswd.New(SMTPAuthFile, htpasswd.DefaultSystems, nil) @@ -164,6 +170,10 @@ func VerifyConfig() error { SMTPAuth = a } + if SMTPSSLCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure { + return errors.New("SMTP authentication requires SSL encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication") + } + validWebrootRe := regexp.MustCompile(`[^0-9a-zA-Z\/\-\_\.]`) if validWebrootRe.MatchString(Webroot) { return fmt.Errorf("Invalid characters in Webroot (%s). Valid chars include: [a-z A-Z 0-9 _ . - /]", Webroot) diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index 88f0325..ac857a5 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -5,6 +5,7 @@ import ( "net" "net/mail" "regexp" + "strings" "github.com/axllent/mailpit/config" "github.com/axllent/mailpit/storage" @@ -33,21 +34,40 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error { } subject := msg.Header.Get("Subject") - logger.Log().Debugf("[smtp] received mail from %s for %s with subject %s", from, to[0], subject) + logger.Log().Debugf("[smtp] received (%s) from:%s to:%s subject:%q", cleanIP(origin), from, to[0], subject) return nil } func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) { - return config.SMTPAuth.Match(string(username), string(password)), nil + allow := config.SMTPAuth.Match(string(username), string(password)) + if allow { + logger.Log().Debugf("[smtp] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr)) + } else { + logger.Log().Warnf("[smtp] deny %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr)) + } + return allow, nil +} + +// Allow any username and password +func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) { + logger.Log().Debugf("[smtp] allow %s login %q from %s", mechanism, string(username), cleanIP(remoteAddr)) + return true, nil } // Listen starts the SMTPD server func Listen() error { - if config.SMTPSSLCert != "" { - logger.Log().Info("[smtp] enabling TLS") - } - if config.SMTPAuthFile != "" { - logger.Log().Info("[smtp] enabling authentication") + if config.SMTPAuthAllowInsecure { + if config.SMTPAuthFile != "" { + logger.Log().Infof("[smtp] enabling login auth via %s (insecure)", config.SMTPAuthFile) + } else if config.SMTPAuthAcceptAny { + logger.Log().Info("[smtp] enabling all auth (insecure)") + } + } else { + if config.SMTPAuthFile != "" { + logger.Log().Infof("[smtp] enabling login auth via %s (TLS)", config.SMTPAuthFile) + } else if config.SMTPAuthAcceptAny { + logger.Log().Info("[smtp] enabling any auth (TLS)") + } } logger.Log().Infof("[smtp] starting on %s", config.SMTPListen) @@ -65,17 +85,30 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa AuthRequired: false, } + if config.SMTPAuthAllowInsecure { + srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true} + } + if config.SMTPAuthFile != "" { + srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true} srv.AuthHandler = authHandler srv.AuthRequired = true + } else if config.SMTPAuthAcceptAny { + srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true} + srv.AuthHandler = authHandlerAny } if config.SMTPSSLCert != "" { - err := srv.ConfigureTLS(config.SMTPSSLCert, config.SMTPSSLKey) - if err != nil { + if err := srv.ConfigureTLS(config.SMTPSSLCert, config.SMTPSSLKey); err != nil { return err } } return srv.ListenAndServe() } + +func cleanIP(i net.Addr) string { + parts := strings.Split(i.String(), ":") + + return parts[0] +} From aeeb7326816a642ba874712de4e848b5148e6364 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sun, 12 Mar 2023 11:31:15 +1300 Subject: [PATCH 2/4] Feature: Rename SSL to TLS, add deprecation warnings to flags & ENV variables referring to SSL --- cmd/root.go | 117 ++++++++++++++++++++++++++++-------------- config/config.go | 48 ++++++++--------- server/server.go | 17 +++--- server/smtpd/smtpd.go | 5 +- 4 files changed, 114 insertions(+), 73 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5b26efc..cbd5541 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -74,28 +74,11 @@ func init() { rootCmd.PersistentFlags().BoolP("help", "h", false, "This help") rootCmd.PersistentFlags().Lookup("help").Hidden = true - initConfigFromEnv() + // load and warn deprecated ENV vars + initDeprecatedConfigFromEnv() - // deprecated 2022/08/06 - if len(os.Getenv("MP_AUTH_FILE")) > 0 { - fmt.Println("MP_AUTH_FILE has been deprecated, use MP_UI_AUTH_FILE") - config.UIAuthFile = os.Getenv("MP_AUTH_FILE") - } - // deprecated 2022/08/06 - if len(os.Getenv("MP_SSL_CERT")) > 0 { - fmt.Println("MP_SSL_CERT has been deprecated, use MP_UI_SSL_CERT") - config.UISSLCert = os.Getenv("MP_SSL_CERT") - } - // deprecated 2022/08/06 - if len(os.Getenv("MP_SSL_KEY")) > 0 { - fmt.Println("MP_SSL_KEY has been deprecated, use MP_UI_SSL_KEY") - config.UISSLKey = os.Getenv("MP_SSL_KEY") - } - // deprecated 2022/08/28 - if len(os.Getenv("MP_DATA_DIR")) > 0 { - fmt.Println("MP_DATA_DIR has been deprecated, use MP_DATA_FILE") - config.DataFile = os.Getenv("MP_DATA_DIR") - } + // load ENV vars + initConfigFromEnv() rootCmd.Flags().StringVarP(&config.DataFile, "db-file", "d", config.DataFile, "Database file to store persistent data") rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port") @@ -105,34 +88,48 @@ func init() { rootCmd.Flags().BoolVar(&config.UseMessageDates, "use-message-dates", false, "Use message dates as the received dates") rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication") - rootCmd.Flags().StringVar(&config.UISSLCert, "ui-ssl-cert", config.UISSLCert, "SSL certificate for web UI - requires ui-ssl-key") - rootCmd.Flags().StringVar(&config.UISSLKey, "ui-ssl-key", config.UISSLKey, "SSL key for web UI - requires ui-ssl-cert") + rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key") + rootCmd.Flags().StringVar(&config.UITLSKey, "ui-tls-key", config.UITLSKey, "TLS key for web UI (HTTPS) - requires ui-tls-cert") rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication") rootCmd.Flags().BoolVar(&config.SMTPAuthAcceptAny, "smtp-auth-accept-any", false, "Accept any SMTP username and password, including none") - rootCmd.Flags().StringVar(&config.SMTPSSLCert, "smtp-ssl-cert", config.SMTPSSLCert, "SSL certificate for SMTP - requires smtp-ssl-key") - rootCmd.Flags().StringVar(&config.SMTPSSLKey, "smtp-ssl-key", config.SMTPSSLKey, "SSL key for SMTP - requires smtp-ssl-cert") + rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-tls-cert", config.SMTPTLSCert, "TLS certificate for SMTP (STARTTLS) - requires smtp-tls-key") + rootCmd.Flags().StringVar(&config.SMTPTLSKey, "smtp-tls-key", config.SMTPTLSKey, "TLS key for SMTP (STARTTLS) - requires smtp-tls-cert") rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", false, "Enable insecure PLAIN & LOGIN authentication") rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters") rootCmd.Flags().BoolVarP(&config.QuietLogging, "quiet", "q", false, "Quiet logging (errors only)") rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", false, "Verbose logging") - // deprecated 2022/08/06 + // deprecated flags 2022/08/06 rootCmd.Flags().StringVarP(&config.UIAuthFile, "auth-file", "a", config.UIAuthFile, "A password file for web UI authentication") - rootCmd.Flags().StringVar(&config.UISSLCert, "ssl-cert", config.UISSLCert, "SSL certificate - requires ssl-key") - rootCmd.Flags().StringVar(&config.UISSLKey, "ssl-key", config.UISSLKey, "SSL key - requires ssl-cert") + rootCmd.Flags().StringVar(&config.UITLSCert, "ssl-cert", config.UITLSCert, "SSL certificate - requires ssl-key") + rootCmd.Flags().StringVar(&config.UITLSKey, "ssl-key", config.UITLSKey, "SSL key - requires ssl-cert") rootCmd.Flags().Lookup("auth-file").Hidden = true rootCmd.Flags().Lookup("auth-file").Deprecated = "use --ui-auth-file" rootCmd.Flags().Lookup("ssl-cert").Hidden = true - rootCmd.Flags().Lookup("ssl-cert").Deprecated = "use --ui-ssl-cert" + rootCmd.Flags().Lookup("ssl-cert").Deprecated = "use --ui-tls-cert" rootCmd.Flags().Lookup("ssl-key").Hidden = true - rootCmd.Flags().Lookup("ssl-key").Deprecated = "use --ui-ssl-key" + rootCmd.Flags().Lookup("ssl-key").Deprecated = "use --ui-tls-key" - // deprecated 2022/08/30 + // deprecated flags 2022/08/30 rootCmd.Flags().StringVar(&config.DataFile, "data", config.DataFile, "Database file to store persistent data") rootCmd.Flags().Lookup("data").Hidden = true rootCmd.Flags().Lookup("data").Deprecated = "use --db-file" + + // deprecated flags 2023/03/12 + rootCmd.Flags().StringVar(&config.UITLSCert, "ui-ssl-cert", config.UITLSCert, "SSL certificate for web UI - requires ui-ssl-key") + rootCmd.Flags().StringVar(&config.UITLSKey, "ui-ssl-key", config.UITLSKey, "SSL key for web UI - requires ui-ssl-cert") + rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-ssl-cert", config.SMTPTLSCert, "SSL certificate for SMTP - requires smtp-ssl-key") + rootCmd.Flags().StringVar(&config.SMTPTLSKey, "smtp-ssl-key", config.SMTPTLSKey, "SSL key for SMTP - requires smtp-ssl-cert") + rootCmd.Flags().Lookup("ui-ssl-cert").Hidden = true + rootCmd.Flags().Lookup("ui-ssl-cert").Deprecated = "use --ui-tls-cert" + rootCmd.Flags().Lookup("ui-ssl-key").Hidden = true + rootCmd.Flags().Lookup("ui-ssl-key").Deprecated = "use --ui-tls-key" + rootCmd.Flags().Lookup("smtp-ssl-cert").Hidden = true + rootCmd.Flags().Lookup("smtp-ssl-cert").Deprecated = "use --smtp-tls-cert" + rootCmd.Flags().Lookup("smtp-ssl-key").Hidden = true + rootCmd.Flags().Lookup("smtp-ssl-key").Deprecated = "use --smtp-tls-key" } // Load settings from environment @@ -158,22 +155,22 @@ func initConfigFromEnv() { if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 { config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE") } - if len(os.Getenv("MP_UI_SSL_CERT")) > 0 { - config.UISSLCert = os.Getenv("MP_UI_SSL_CERT") + if len(os.Getenv("MP_UI_TLS_CERT")) > 0 { + config.UITLSCert = os.Getenv("MP_UI_TLS_CERT") } - if len(os.Getenv("MP_UI_SSL_KEY")) > 0 { - config.UISSLKey = os.Getenv("MP_UI_SSL_KEY") + if len(os.Getenv("MP_UI_TLS_KEY")) > 0 { + config.UITLSKey = os.Getenv("MP_UI_TLS_KEY") } // SMTP if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 { config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE") } - if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 { - config.SMTPSSLCert = os.Getenv("MP_SMTP_SSL_CERT") + if len(os.Getenv("MP_SMTP_TLS_CERT")) > 0 { + config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT") } - if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 { - config.SMTPSSLKey = os.Getenv("MP_SMTP_SSL_KEY") + if len(os.Getenv("MP_SMTP_TLS_KEY")) > 0 { + config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY") } if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") { config.SMTPAuthAcceptAny = true @@ -199,6 +196,48 @@ func initConfigFromEnv() { } } +// load deprecated settings from environment and warn +func initDeprecatedConfigFromEnv() { + // deprecated 2022/08/06 + if len(os.Getenv("MP_AUTH_FILE")) > 0 { + fmt.Println("ENV MP_AUTH_FILE has been deprecated, use MP_UI_AUTH_FILE") + config.UIAuthFile = os.Getenv("MP_AUTH_FILE") + } + // deprecated 2022/08/06 + if len(os.Getenv("MP_SSL_CERT")) > 0 { + fmt.Println("ENV MP_SSL_CERT has been deprecated, use MP_UI_TLS_CERT") + config.UITLSCert = os.Getenv("MP_SSL_CERT") + } + // deprecated 2022/08/06 + if len(os.Getenv("MP_SSL_KEY")) > 0 { + fmt.Println("ENV MP_SSL_KEY has been deprecated, use MP_UI_TLS_KEY") + config.UITLSKey = os.Getenv("MP_TLS_KEY") + } + // deprecated 2022/08/28 + if len(os.Getenv("MP_DATA_DIR")) > 0 { + fmt.Println("ENV MP_DATA_DIR has been deprecated, use MP_DATA_FILE") + config.DataFile = os.Getenv("MP_DATA_DIR") + } + // deprecated 2023/03/12 + if len(os.Getenv("MP_UI_SSL_CERT")) > 0 { + fmt.Println("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT") + config.UITLSCert = os.Getenv("MP_UI_SSL_CERT") + } + if len(os.Getenv("MP_UI_SSL_KEY")) > 0 { + fmt.Println("ENV MP_UI_SSL_KEY has been deprecated, use MP_UI_TLS_KEY") + config.UITLSKey = os.Getenv("MP_UI_SSL_KEY") + } + if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 { + fmt.Println("ENV MP_SMTP_CERT has been deprecated, use MP_SMTP_TLS_CERT") + config.SMTPTLSCert = os.Getenv("MP_SMTP_SSL_CERT") + } + if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 { + fmt.Println("ENV MP_SMTP_KEY has been deprecated, use MP_SMTP_TLS_KEY") + config.SMTPTLSKey = os.Getenv("MP_SMTP_SMTP_KEY") + } +} + +// Wrapper to get a boolean from an environment variable func getEnabledFromEnv(k string) bool { if len(os.Getenv(k)) > 0 { v := strings.ToLower(os.Getenv(k)) diff --git a/config/config.go b/config/config.go index 5592d68..4ef87df 100644 --- a/config/config.go +++ b/config/config.go @@ -39,11 +39,11 @@ var ( // NoLogging for tests NoLogging = false - // UISSLCert file - UISSLCert string + // UITLSCert file + UITLSCert string - // UISSLKey file - UISSLKey string + // UITLSKey file + UITLSKey string // UIAuthFile for basic authentication UIAuthFile string @@ -54,11 +54,11 @@ var ( // Webroot to define the base path for the UI and API Webroot = "/" - // SMTPSSLCert file - SMTPSSLCert string + // SMTPTLSCert file + SMTPTLSCert string - // SMTPSSLKey file - SMTPSSLKey string + // SMTPTLSKey file + SMTPTLSKey string // SMTPAuthFile for SMTP authentication SMTPAuthFile string @@ -126,31 +126,31 @@ func VerifyConfig() error { UIAuth = a } - if UISSLCert != "" && UISSLKey == "" || UISSLCert == "" && UISSLKey != "" { - return errors.New("You must provide both a UI SSL certificate and a key") + if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" { + return errors.New("You must provide both a UI TLS certificate and a key") } - if UISSLCert != "" { - if !isFile(UISSLCert) { - return fmt.Errorf("SSL certificate not found: %s", UISSLCert) + if UITLSCert != "" { + if !isFile(UITLSCert) { + return fmt.Errorf("TLS certificate not found: %s", UITLSCert) } - if !isFile(UISSLKey) { - return fmt.Errorf("SSL key not found: %s", UISSLKey) + if !isFile(UITLSKey) { + return fmt.Errorf("TLS key not found: %s", UITLSKey) } } - if SMTPSSLCert != "" && SMTPSSLKey == "" || SMTPSSLCert == "" && SMTPSSLKey != "" { - return errors.New("You must provide both an SMTP SSL certificate and a key") + if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" { + return errors.New("You must provide both an SMTP TLS certificate and a key") } - if SMTPSSLCert != "" { - if !isFile(SMTPSSLCert) { - return fmt.Errorf("SMTP SSL certificate not found: %s", SMTPSSLCert) + if SMTPTLSCert != "" { + if !isFile(SMTPTLSCert) { + return fmt.Errorf("SMTP TLS certificate not found: %s", SMTPTLSCert) } - if !isFile(SMTPSSLKey) { - return fmt.Errorf("SMTP SSL key not found: %s", SMTPSSLKey) + if !isFile(SMTPTLSKey) { + return fmt.Errorf("SMTP TLS key not found: %s", SMTPTLSKey) } } @@ -170,8 +170,8 @@ func VerifyConfig() error { SMTPAuth = a } - if SMTPSSLCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure { - return errors.New("SMTP authentication requires SSL encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication") + if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure { + return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication") } validWebrootRe := regexp.MustCompile(`[^0-9a-zA-Z\/\-\_\.]`) diff --git a/server/server.go b/server/server.go index 129242e..4d7523c 100644 --- a/server/server.go +++ b/server/server.go @@ -3,18 +3,19 @@ package server import ( "compress/gzip" "embed" - "github.com/axllent/mailpit/config" - "github.com/axllent/mailpit/server/apiv1" - "github.com/axllent/mailpit/server/handlers" - "github.com/axllent/mailpit/server/websockets" - "github.com/axllent/mailpit/utils/logger" - "github.com/gorilla/mux" "io" "io/fs" "net/http" "os" "strings" "sync/atomic" + + "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/server/apiv1" + "github.com/axllent/mailpit/server/handlers" + "github.com/axllent/mailpit/server/websockets" + "github.com/axllent/mailpit/utils/logger" + "github.com/gorilla/mux" ) //go:embed ui @@ -62,9 +63,9 @@ func Listen() { // Mark the application here as ready isReady.Store(true) - if config.UISSLCert != "" && config.UISSLKey != "" { + if config.UITLSCert != "" && config.UITLSKey != "" { logger.Log().Infof("[http] starting secure server on https://%s%s", config.HTTPListen, config.Webroot) - logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UISSLCert, config.UISSLKey, nil)) + logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UITLSCert, config.UITLSKey, nil)) } else { logger.Log().Infof("[http] starting server on http://%s%s", config.HTTPListen, config.Webroot) logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil)) diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index ac857a5..6588710 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -1,3 +1,4 @@ +// Package smtpd is the SMTP daemon package smtpd import ( @@ -98,8 +99,8 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa srv.AuthHandler = authHandlerAny } - if config.SMTPSSLCert != "" { - if err := srv.ConfigureTLS(config.SMTPSSLCert, config.SMTPSSLKey); err != nil { + if config.SMTPTLSCert != "" { + if err := srv.ConfigureTLS(config.SMTPTLSCert, config.SMTPTLSKey); err != nil { return err } } From 27d49417d7d83e105fbb1d4bdf95149d884aee29 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sun, 12 Mar 2023 14:27:20 +1300 Subject: [PATCH 3/4] API: Return received datetime when message does not contain a date header --- storage/database.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/storage/database.go b/storage/database.go index 0adcbcc..21e71fb 100644 --- a/storage/database.go +++ b/storage/database.go @@ -381,7 +381,8 @@ func Search(search string, start, limit int) ([]MessageSummary, error) { return results, err } -// GetMessage returns a data.Message generated from the mailbox_data collection. +// GetMessage returns a Message generated from the mailbox_data collection. +// If the message lacks a date header, then the received datetime is used. func GetMessage(id string) (*Message, error) { raw, err := GetMessageRaw(id) if err != nil { @@ -403,7 +404,35 @@ func GetMessage(id string) (*Message, error) { from = &mail.Address{Name: env.GetHeader("From")} } - date, _ := env.Date() + date, err := env.Date() + if err != nil { + // return received datetime when message does not contain a date header + q := sqlf.From("mailbox"). + Select(`Data`). + OrderBy("Sort DESC"). + Where(`ID = ?`, id) + + if err := q.QueryAndClose(nil, db, func(row *sql.Rows) { + var summary string + em := MessageSummary{} + + if err := row.Scan(&summary); err != nil { + logger.Log().Error(err) + return + } + + if err := json.Unmarshal([]byte(summary), &em); err != nil { + logger.Log().Error(err) + return + } + + logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id) + + date = em.Created + }); err != nil { + logger.Log().Error(err) + } + } obj := Message{ ID: id, From 03f30b01bf2e0a94b2e4fa00f260c2b8be6d425a Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sun, 12 Mar 2023 15:06:26 +1300 Subject: [PATCH 4/4] Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 15dbf05..6305813 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![CodeQL](https://github.com/axllent/mailpit/actions/workflows/codeql-analysis.yml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/axllent/mailpit)](https://goreportcard.com/report/github.com/axllent/mailpit) -Mailpit is a multi-platform email testing tool for developers. +Mailpit is a multi-platform email testing tool 7 API for developers. It acts as both an SMTP server, and provides a web interface to view all captured emails. @@ -27,8 +27,8 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster. - Configurable automatic email pruning (default keeps the most recent 500 emails) - Email storage either in a temporary or persistent database ([see wiki](https://github.com/axllent/mailpit/wiki/Email-storage)) - Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size -- Can handle hundreds of thousands of emails -- Optional SMTP with STARTTLS & SMTP authentication ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication)) +- Can handle tens of thousands of emails +- Optional SMTP with STARTTLS & SMTP authentication, including an "accept anything" mode ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication)) - Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS)) - Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication)) - A simple REST API ([see docs](docs/apiv1/README.md))