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] +}