From c5b3edf87d4c32f16a3bd47d450f691f58ab1d4c Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 30 May 2025 00:00:05 +1200 Subject: [PATCH 1/4] Fix: Ignore basic auth for OPTIONS requests to API when CORS is set Web browsers do not send authorization headers for preflight requests. --- server/server.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index eb2a3cd..a38ae00 100644 --- a/server/server.go +++ b/server/server.go @@ -239,7 +239,9 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc { w.Header().Set("Access-Control-Allow-Headers", "*") } - if auth.UICredentials != nil { + // Check basic authentication headers if configured. + // OPTIONS requests are skipped if CORS is enabled, since browsers omit credentials for preflight. + if !(AccessControlAllowOrigin != "" && r.Method == http.MethodOptions) && auth.UICredentials != nil { user, pass, ok := r.BasicAuth() if !ok { From 9bfdeb5f7b55b390ee4e3b1baa207f80ee4ec5b9 Mon Sep 17 00:00:00 2001 From: Ben Edmunds Date: Thu, 29 May 2025 21:34:40 +0100 Subject: [PATCH 2/4] Feature: Send API allow separate auth (#504) Co-authored-by: Ben Edmunds --- cmd/root.go | 9 ++ config/config.go | 29 ++++++ internal/auth/auth.go | 21 ++++ server/server.go | 44 ++++++++- server/server_test.go | 218 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 320 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 25ccf05..06ccbcd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -112,6 +112,8 @@ func init() { rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port") 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", config.SMTPAuthAcceptAny, "Accept any SMTP username and password, including none") + rootCmd.Flags().StringVar(&config.SendAPIAuthFile, "send-api-auth-file", config.SendAPIAuthFile, "A password file for Send API authentication") + rootCmd.Flags().BoolVar(&config.SendAPIAuthAcceptAny, "send-api-auth-accept-any", config.SendAPIAuthAcceptAny, "Accept any username and password for the Send API endpoint, including none") 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.SMTPRequireSTARTTLS, "smtp-require-starttls", config.SMTPRequireSTARTTLS, "Require SMTP client use STARTTLS") @@ -260,6 +262,13 @@ func initConfigFromEnv() { if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") { config.SMTPAuthAcceptAny = true } + config.SendAPIAuthFile = os.Getenv("MP_SEND_API_AUTH_FILE") + if err := auth.SetSendAPIAuth(os.Getenv("MP_SEND_API_AUTH")); err != nil { + logger.Log().Error(err.Error()) + } + if getEnabledFromEnv("MP_SEND_API_AUTH_ACCEPT_ANY") { + config.SendAPIAuthAcceptAny = true + } config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT") config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY") if getEnabledFromEnv("MP_SMTP_REQUIRE_STARTTLS") { diff --git a/config/config.go b/config/config.go index d62623c..58b9897 100644 --- a/config/config.go +++ b/config/config.go @@ -96,6 +96,12 @@ var ( // SMTPAuthAcceptAny accepts any username/password including none SMTPAuthAcceptAny bool + // SendAPIAuthFile for Send API authentication + SendAPIAuthFile string + + // SendAPIAuthAcceptAny accepts any username/password for the send API endpoint, including none + SendAPIAuthAcceptAny bool + // SMTPMaxRecipients is the maximum number of recipients a message may have. // The SMTP RFC states that an server must handle a minimum of 100 recipients // however some servers accept more. @@ -383,6 +389,29 @@ func VerifyConfig() error { return errors.New("[smtp] authentication cannot use both credentials and --smtp-auth-accept-any") } + if SendAPIAuthFile != "" { + SendAPIAuthFile = filepath.Clean(SendAPIAuthFile) + + if !isFile(SendAPIAuthFile) { + return fmt.Errorf("[send-api] password file not found or readable: %s", SendAPIAuthFile) + } + + b, err := os.ReadFile(SendAPIAuthFile) + if err != nil { + return err + } + + if err := auth.SetSendAPIAuth(string(b)); err != nil { + return err + } + + logger.Log().Info("[send-api] enabling basic authentication") + } + + if auth.SendAPICredentials != nil && SendAPIAuthAcceptAny { + return errors.New("[send-api] authentication cannot use both credentials and --send-api-auth-accept-any") + } + if SMTPTLSCert == "" && (auth.SMTPCredentials != nil || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure { return errors.New("[smtp] authentication requires STARTTLS or TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication") } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 5225573..a68e6a0 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -15,6 +15,8 @@ var ( SMTPCredentials *htpasswd.File // POP3Credentials passwords POP3Credentials *htpasswd.File + // SendAPICredentials passwords + SendAPICredentials *htpasswd.File ) // SetUIAuth will set Basic Auth credentials required for the UI & API @@ -74,6 +76,25 @@ func SetPOP3Auth(s string) error { return nil } +// SetSendAPIAuth will set Send API credentials +func SetSendAPIAuth(s string) error { + var err error + + credentials := credentialsFromString(s) + if len(credentials) == 0 { + return nil + } + + r := strings.NewReader(strings.Join(credentials, "\n")) + + SendAPICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil) + if err != nil { + return err + } + + return nil +} + func credentialsFromString(s string) []string { // split string by any whitespace character re := regexp.MustCompile(`\s+`) diff --git a/server/server.go b/server/server.go index a38ae00..45458d3 100644 --- a/server/server.go +++ b/server/server.go @@ -158,7 +158,7 @@ func apiRoutes() *mux.Router { r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE") r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.DeleteSearch)).Methods("DELETE") - r.HandleFunc(config.Webroot+"api/v1/send", middleWareFunc(apiv1.SendMessageHandler)).Methods("POST") + r.HandleFunc(config.Webroot+"api/v1/send", sendAPIAuthMiddleware(apiv1.SendMessageHandler)).Methods("POST") r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.GetAllTags)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetMessageTags)).Methods("PUT") r.HandleFunc(config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.RenameTag)).Methods("PUT") @@ -198,6 +198,48 @@ func basicAuthResponse(w http.ResponseWriter) { _, _ = w.Write([]byte("Unauthorised.\n")) } +// sendAPIAuthMiddleware handles authentication specifically for the send API endpoint +// It can use dedicated send API authentication or accept any credentials based on configuration +func sendAPIAuthMiddleware(fn http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // If send API auth accept any is enabled, bypass all authentication + if config.SendAPIAuthAcceptAny { + // Temporarily disable UI auth for this request + originalCredentials := auth.UICredentials + auth.UICredentials = nil + defer func() { auth.UICredentials = originalCredentials }() + // Call the standard middleware + middleWareFunc(fn)(w, r) + return + } + + // If Send API credentials are configured, only accept those credentials + if auth.SendAPICredentials != nil { + user, pass, ok := r.BasicAuth() + + if !ok { + basicAuthResponse(w) + return + } + + if !auth.SendAPICredentials.Match(user, pass) { + basicAuthResponse(w) + return + } + + // Valid Send API credentials - bypass UI auth and call function directly + originalCredentials := auth.UICredentials + auth.UICredentials = nil + defer func() { auth.UICredentials = originalCredentials }() + middleWareFunc(fn)(w, r) + return + } + + // No Send API credentials configured - fall back to UI auth + middleWareFunc(fn)(w, r) + } +} + type gzipResponseWriter struct { io.Writer http.ResponseWriter diff --git a/server/server_test.go b/server/server_test.go index 6681a54..75958db 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -13,10 +13,12 @@ import ( "testing" "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/server/apiv1" "github.com/jhillyerd/enmime/v2" + "golang.org/x/crypto/bcrypt" ) var ( @@ -24,6 +26,18 @@ var ( Read bool IDs []string } + + // Shared test message structure for consistency + testSendMessage = map[string]interface{}{ + "From": map[string]string{ + "Email": "test@example.com", + }, + "To": []map[string]string{ + {"Email": "recipient@example.com"}, + }, + "Subject": "Test", + "Text": "Test message", + } ) func TestAPIv1Messages(t *testing.T) { @@ -312,6 +326,157 @@ func TestAPIv1Send(t *testing.T) { assertEqual(t, `This is a plain text attachment`, string(attachmentBytes), "wrong Attachment content") } +func TestSendAPIAuthMiddleware(t *testing.T) { + setup() + defer storage.Close() + + // Test 1: Send API with accept-any enabled (should bypass all auth) + t.Run("SendAPIAuthAcceptAny", func(t *testing.T) { + // Set up UI auth and enable accept-any for send API + originalSendAPIAuthAcceptAny := config.SendAPIAuthAcceptAny + originalUICredentials := auth.UICredentials + defer func() { + config.SendAPIAuthAcceptAny = originalSendAPIAuthAcceptAny + auth.UICredentials = originalUICredentials + }() + + // Enable accept-any for send API + config.SendAPIAuthAcceptAny = true + + // Set up UI auth that would normally block requests + testHash, _ := bcrypt.GenerateFromPassword([]byte("testpass"), bcrypt.DefaultCost) + auth.SetUIAuth("testuser:" + string(testHash)) + + r := apiRoutes() + ts := httptest.NewServer(r) + defer ts.Close() + + // Should succeed without any auth headers + jsonData, _ := json.Marshal(testSendMessage) + _, err := clientPost(ts.URL+"/api/v1/send", string(jsonData)) + if err != nil { + t.Errorf("Expected send to succeed with accept-any, got error: %s", err.Error()) + } + }) + + // Test 2: Send API with dedicated credentials + t.Run("SendAPIWithDedicatedCredentials", func(t *testing.T) { + originalSendAPIAuthAcceptAny := config.SendAPIAuthAcceptAny + originalUICredentials := auth.UICredentials + originalSendAPICredentials := auth.SendAPICredentials + defer func() { + config.SendAPIAuthAcceptAny = originalSendAPIAuthAcceptAny + auth.UICredentials = originalUICredentials + auth.SendAPICredentials = originalSendAPICredentials + }() + + config.SendAPIAuthAcceptAny = false + + // Set up UI auth + uiHash, _ := bcrypt.GenerateFromPassword([]byte("uipass"), bcrypt.DefaultCost) + auth.SetUIAuth("uiuser:" + string(uiHash)) + + // Set up dedicated Send API auth + sendHash, _ := bcrypt.GenerateFromPassword([]byte("sendpass"), bcrypt.DefaultCost) + auth.SetSendAPIAuth("senduser:" + string(sendHash)) + + r := apiRoutes() + ts := httptest.NewServer(r) + defer ts.Close() + + jsonData, _ := json.Marshal(testSendMessage) + + // Should succeed with correct Send API credentials + _, err := clientPostWithAuth(ts.URL+"/api/v1/send", string(jsonData), "senduser", "sendpass") + if err != nil { + t.Errorf("Expected send to succeed with correct Send API credentials, got error: %s", err.Error()) + } + + // Should fail with wrong Send API credentials + _, err = clientPostWithAuth(ts.URL+"/api/v1/send", string(jsonData), "senduser", "wrongpass") + if err == nil { + t.Error("Expected send to fail with wrong Send API credentials") + } + + // Should fail with UI credentials when Send API credentials are set + _, err = clientPostWithAuth(ts.URL+"/api/v1/send", string(jsonData), "uiuser", "uipass") + if err == nil { + t.Error("Expected send to fail with UI credentials when Send API credentials are required") + } + }) + + // Test 3: Send API fallback to UI auth when no Send API auth is configured + t.Run("SendAPIFallbackToUIAuth", func(t *testing.T) { + originalSendAPIAuthAcceptAny := config.SendAPIAuthAcceptAny + originalUICredentials := auth.UICredentials + originalSendAPICredentials := auth.SendAPICredentials + defer func() { + config.SendAPIAuthAcceptAny = originalSendAPIAuthAcceptAny + auth.UICredentials = originalUICredentials + auth.SendAPICredentials = originalSendAPICredentials + }() + + config.SendAPIAuthAcceptAny = false + auth.SendAPICredentials = nil + + // Set up only UI auth + uiHash, _ := bcrypt.GenerateFromPassword([]byte("uipass"), bcrypt.DefaultCost) + auth.SetUIAuth("uiuser:" + string(uiHash)) + + r := apiRoutes() + ts := httptest.NewServer(r) + defer ts.Close() + + jsonData, _ := json.Marshal(testSendMessage) + + // Should succeed with UI credentials when no Send API auth is configured + _, err := clientPostWithAuth(ts.URL+"/api/v1/send", string(jsonData), "uiuser", "uipass") + if err != nil { + t.Errorf("Expected send to succeed with UI credentials when no Send API auth configured, got error: %s", err.Error()) + } + + // Should fail without any credentials + _, err = clientPost(ts.URL+"/api/v1/send", string(jsonData)) + if err == nil { + t.Error("Expected send to fail without credentials when UI auth is required") + } + }) + + // Test 4: Regular API endpoints should not be affected by Send API auth settings + t.Run("RegularAPINotAffectedBySendAPIAuth", func(t *testing.T) { + originalSendAPIAuthAcceptAny := config.SendAPIAuthAcceptAny + originalUICredentials := auth.UICredentials + originalSendAPICredentials := auth.SendAPICredentials + defer func() { + config.SendAPIAuthAcceptAny = originalSendAPIAuthAcceptAny + auth.UICredentials = originalUICredentials + auth.SendAPICredentials = originalSendAPICredentials + }() + + // Set up UI auth and Send API auth + uiHash, _ := bcrypt.GenerateFromPassword([]byte("uipass"), bcrypt.DefaultCost) + auth.SetUIAuth("uiuser:" + string(uiHash)) + sendHash, _ := bcrypt.GenerateFromPassword([]byte("sendpass"), bcrypt.DefaultCost) + auth.SetSendAPIAuth("senduser:" + string(sendHash)) + + r := apiRoutes() + ts := httptest.NewServer(r) + defer ts.Close() + + // Regular API endpoint should require UI credentials, not Send API credentials + _, err := clientGetWithAuth(ts.URL+"/api/v1/messages", "uiuser", "uipass") + if err != nil { + t.Errorf("Expected regular API to work with UI credentials, got error: %s", err.Error()) + } + + // Regular API endpoint should fail with Send API credentials + _, err = clientGetWithAuth(ts.URL+"/api/v1/messages", "senduser", "sendpass") + if err == nil { + t.Error("Expected regular API to fail with Send API credentials") + } + }) +} + func setup() { logger.NoLogging = true config.MaxMessages = 0 @@ -521,6 +686,59 @@ func clientPost(url, body string) ([]byte, error) { return data, err } +func clientPostWithAuth(url, body, username, password string) ([]byte, error) { + client := new(http.Client) + + b := strings.NewReader(body) + req, err := http.NewRequest("POST", url, b) + if err != nil { + return nil, err + } + + req.SetBasicAuth(username, password) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode) + } + + data, err := io.ReadAll(resp.Body) + + return data, err +} + +func clientGetWithAuth(url, username, password string) ([]byte, error) { + client := new(http.Client) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.SetBasicAuth(username, password) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode) + } + + data, err := io.ReadAll(resp.Body) + + return data, err +} + func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { if a == b { return From 4dff7adc1d8a42ef57f43993919c00a90d9a922f Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 30 May 2025 11:03:30 +1200 Subject: [PATCH 3/4] Reorder send API CLI flags --- cmd/root.go | 22 +++++++++------ config/config.go | 65 ++++++++++++++++++++++++------------------- internal/auth/auth.go | 42 ++++++++++++++-------------- 3 files changed, 70 insertions(+), 59 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 06ccbcd..b5b5dc6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -108,12 +108,14 @@ func init() { rootCmd.Flags().BoolVar(&config.DisableHTTPCompression, "disable-http-compression", config.DisableHTTPCompression, "Disable HTTP compression support (web UI & API)") rootCmd.Flags().BoolVar(&config.HideDeleteAllButton, "hide-delete-all-button", config.HideDeleteAllButton, "Hide the \"Delete all\" button in the web UI") + // Send API + rootCmd.Flags().StringVar(&config.SendAPIAuthFile, "send-api-auth-file", config.SendAPIAuthFile, "A password file for Send API authentication") + rootCmd.Flags().BoolVar(&config.SendAPIAuthAcceptAny, "send-api-auth-accept-any", config.SendAPIAuthAcceptAny, "Accept any username and password for the Send API endpoint, including none") + // SMTP server rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port") 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", config.SMTPAuthAcceptAny, "Accept any SMTP username and password, including none") - rootCmd.Flags().StringVar(&config.SendAPIAuthFile, "send-api-auth-file", config.SendAPIAuthFile, "A password file for Send API authentication") - rootCmd.Flags().BoolVar(&config.SendAPIAuthAcceptAny, "send-api-auth-accept-any", config.SendAPIAuthAcceptAny, "Accept any username and password for the Send API endpoint, including none") 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.SMTPRequireSTARTTLS, "smtp-require-starttls", config.SMTPRequireSTARTTLS, "Require SMTP client use STARTTLS") @@ -251,6 +253,15 @@ func initConfigFromEnv() { config.HideDeleteAllButton = true } + // Send API + config.SendAPIAuthFile = os.Getenv("MP_SEND_API_AUTH_FILE") + if err := auth.SetSendAPIAuth(os.Getenv("MP_SEND_API_AUTH")); err != nil { + logger.Log().Error(err.Error()) + } + if getEnabledFromEnv("MP_SEND_API_AUTH_ACCEPT_ANY") { + config.SendAPIAuthAcceptAny = true + } + // SMTP server if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 { config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR") @@ -262,13 +273,6 @@ func initConfigFromEnv() { if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") { config.SMTPAuthAcceptAny = true } - config.SendAPIAuthFile = os.Getenv("MP_SEND_API_AUTH_FILE") - if err := auth.SetSendAPIAuth(os.Getenv("MP_SEND_API_AUTH")); err != nil { - logger.Log().Error(err.Error()) - } - if getEnabledFromEnv("MP_SEND_API_AUTH_ACCEPT_ANY") { - config.SendAPIAuthAcceptAny = true - } config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT") config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY") if getEnabledFromEnv("MP_SMTP_REQUIRE_STARTTLS") { diff --git a/config/config.go b/config/config.go index 58b9897..dfd85ad 100644 --- a/config/config.go +++ b/config/config.go @@ -72,6 +72,12 @@ var ( // DisableHTTPCompression will explicitly disable HTTP compression in the web UI and API DisableHTTPCompression bool + // SendAPIAuthFile for Send API authentication + SendAPIAuthFile string + + // SendAPIAuthAcceptAny accepts any username/password for the send API endpoint, including none + SendAPIAuthAcceptAny bool + // SMTPTLSCert file SMTPTLSCert string @@ -96,12 +102,6 @@ var ( // SMTPAuthAcceptAny accepts any username/password including none SMTPAuthAcceptAny bool - // SendAPIAuthFile for Send API authentication - SendAPIAuthFile string - - // SendAPIAuthAcceptAny accepts any username/password for the send API endpoint, including none - SendAPIAuthAcceptAny bool - // SMTPMaxRecipients is the maximum number of recipients a message may have. // The SMTP RFC states that an server must handle a minimum of 100 recipients // however some servers accept more. @@ -295,6 +295,7 @@ func VerifyConfig() error { return errors.New("[ui] HTTP bind should be in the format of :") } + // Web UI & API if UIAuthFile != "" { UIAuthFile = filepath.Clean(UIAuthFile) @@ -329,6 +330,35 @@ func VerifyConfig() error { } } + // Send API + if SendAPIAuthFile != "" { + SendAPIAuthFile = filepath.Clean(SendAPIAuthFile) + + if !isFile(SendAPIAuthFile) { + return fmt.Errorf("[send-api] password file not found or readable: %s", SendAPIAuthFile) + } + + b, err := os.ReadFile(SendAPIAuthFile) + if err != nil { + return err + } + + if err := auth.SetSendAPIAuth(string(b)); err != nil { + return err + } + + logger.Log().Info("[send-api] enabling basic authentication") + } + + if auth.SendAPICredentials != nil && SendAPIAuthAcceptAny { + return errors.New("[send-api] authentication cannot use both credentials and --send-api-auth-accept-any") + } + + if SendAPIAuthAcceptAny && auth.UICredentials != nil { + logger.Log().Info("[send-api] disabling authentication") + } + + // SMTP server if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" { return errors.New("[smtp] you must provide both an SMTP TLS certificate and a key") } @@ -389,29 +419,6 @@ func VerifyConfig() error { return errors.New("[smtp] authentication cannot use both credentials and --smtp-auth-accept-any") } - if SendAPIAuthFile != "" { - SendAPIAuthFile = filepath.Clean(SendAPIAuthFile) - - if !isFile(SendAPIAuthFile) { - return fmt.Errorf("[send-api] password file not found or readable: %s", SendAPIAuthFile) - } - - b, err := os.ReadFile(SendAPIAuthFile) - if err != nil { - return err - } - - if err := auth.SetSendAPIAuth(string(b)); err != nil { - return err - } - - logger.Log().Info("[send-api] enabling basic authentication") - } - - if auth.SendAPICredentials != nil && SendAPIAuthAcceptAny { - return errors.New("[send-api] authentication cannot use both credentials and --send-api-auth-accept-any") - } - if SMTPTLSCert == "" && (auth.SMTPCredentials != nil || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure { return errors.New("[smtp] authentication requires STARTTLS or TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication") } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index a68e6a0..184b632 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -11,12 +11,12 @@ import ( var ( // UICredentials passwords UICredentials *htpasswd.File + // SendAPICredentials passwords + SendAPICredentials *htpasswd.File // SMTPCredentials passwords SMTPCredentials *htpasswd.File // POP3Credentials passwords POP3Credentials *htpasswd.File - // SendAPICredentials passwords - SendAPICredentials *htpasswd.File ) // SetUIAuth will set Basic Auth credentials required for the UI & API @@ -38,6 +38,25 @@ func SetUIAuth(s string) error { return nil } +// SetSendAPIAuth will set Send API credentials +func SetSendAPIAuth(s string) error { + var err error + + credentials := credentialsFromString(s) + if len(credentials) == 0 { + return nil + } + + r := strings.NewReader(strings.Join(credentials, "\n")) + + SendAPICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil) + if err != nil { + return err + } + + return nil +} + // SetSMTPAuth will set SMTP credentials func SetSMTPAuth(s string) error { var err error @@ -76,25 +95,6 @@ func SetPOP3Auth(s string) error { return nil } -// SetSendAPIAuth will set Send API credentials -func SetSendAPIAuth(s string) error { - var err error - - credentials := credentialsFromString(s) - if len(credentials) == 0 { - return nil - } - - r := strings.NewReader(strings.Join(credentials, "\n")) - - SendAPICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil) - if err != nil { - return err - } - - return nil -} - func credentialsFromString(s string) []string { // split string by any whitespace character re := regexp.MustCompile(`\s+`) From f2b91ac9d5245a09cdf0799b1e918c0d9a305e22 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 30 May 2025 11:04:20 +1200 Subject: [PATCH 4/4] Chore: Add MP_DATA_FILE deprecation warning --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index b5b5dc6..b4cbcee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -375,9 +375,9 @@ func initConfigFromEnv() { func initDeprecatedConfigFromEnv() { // deprecated 2024/04/12 - but will not be removed to maintain backwards compatibility if len(os.Getenv("MP_DATA_FILE")) > 0 { + logger.Log().Warn("ENV MP_DATA_FILE has been deprecated, use MP_DATABASE") config.Database = os.Getenv("MP_DATA_FILE") } - // deprecated 2023/03/12 if len(os.Getenv("MP_UI_SSL_CERT")) > 0 { logger.Log().Warn("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")