diff --git a/cmd/handlers.go b/cmd/handlers.go index c75b3c1e..2759c41c 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -35,6 +35,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.PUT("/api/v1/settings/general", perm(handleUpdateGeneralSettings, "general_settings:manage")) g.GET("/api/v1/settings/notifications/email", perm(handleGetEmailNotificationSettings, "notification_settings:manage")) g.PUT("/api/v1/settings/notifications/email", perm(handleUpdateEmailNotificationSettings, "notification_settings:manage")) + g.POST("/api/v1/settings/notifications/email/test", perm(handleTestEmailNotificationSettings, "notification_settings:manage")) // OpenID connect single sign-on. g.GET("/api/v1/oidc", perm(handleGetAllOIDC, "oidc:manage")) diff --git a/cmd/settings.go b/cmd/settings.go index 33d7b7e0..aa6f69ce 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -1,9 +1,13 @@ package main import ( + "crypto/tls" "encoding/json" + "fmt" "net/mail" + "net/smtp" "strings" + "time" "github.com/abhinavxd/libredesk/internal/envelope" "github.com/abhinavxd/libredesk/internal/setting/models" @@ -143,3 +147,208 @@ func handleUpdateEmailNotificationSettings(r *fastglue.Request) error { return r.SendEnvelope(true) } + +// TestEmailRequest represents the request body for testing email settings. +type TestEmailRequest struct { + models.EmailNotification + TestEmail string `json:"test_email"` +} + +// TestEmailResponse represents the response for testing email settings. +type TestEmailResponse struct { + Success bool `json:"success"` + Logs []string `json:"logs"` +} + +// handleTestEmailNotificationSettings tests the email notification settings by sending a test email. +func handleTestEmailNotificationSettings(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + req = TestEmailRequest{} + cur = models.EmailNotification{} + logs = []string{} + ) + + addLog := func(msg string) { + logs = append(logs, fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), msg)) + } + + if err := r.Decode(&req, "json"); err != nil { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.badRequest"), nil, envelope.InputError) + } + + // Validate test email + if req.TestEmail == "" { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Test email address is required", nil, envelope.InputError) + } + if _, err := mail.ParseAddress(req.TestEmail); err != nil { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid test email address", nil, envelope.InputError) + } + + addLog(fmt.Sprintf("Starting SMTP test to %s", req.TestEmail)) + + // Get current settings to fill in password if not provided + out, err := app.setting.GetByPrefix("notification.email") + if err != nil { + addLog(fmt.Sprintf("Error fetching current settings: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + if err := json.Unmarshal(out, &cur); err != nil { + addLog(fmt.Sprintf("Error parsing current settings: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + + // Use current password if not provided in request + password := req.Password + if password == "" || strings.Contains(password, stringutil.PasswordDummy) { + password = cur.Password + } + + // Build server address + serverAddr := fmt.Sprintf("%s:%d", req.Host, req.Port) + addLog(fmt.Sprintf("Connecting to SMTP server: %s", serverAddr)) + + // Create TLS config + tlsConfig := &tls.Config{ + ServerName: req.Host, + InsecureSkipVerify: req.TLSSkipVerify, + } + + var client *smtp.Client + + // Connect based on TLS type + switch req.TLSType { + case "tls": + addLog("Using SSL/TLS connection") + conn, err := tls.Dial("tcp", serverAddr, tlsConfig) + if err != nil { + addLog(fmt.Sprintf("TLS connection failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + defer conn.Close() + client, err = smtp.NewClient(conn, req.Host) + if err != nil { + addLog(fmt.Sprintf("Failed to create SMTP client: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + default: + addLog("Using plain connection") + var err error + client, err = smtp.Dial(serverAddr) + if err != nil { + addLog(fmt.Sprintf("Connection failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + } + defer client.Close() + + // Send HELO/EHLO + hostname := req.HelloHostname + if hostname == "" { + hostname = "localhost" + } + addLog(fmt.Sprintf("Sending EHLO %s", hostname)) + if err := client.Hello(hostname); err != nil { + addLog(fmt.Sprintf("EHLO failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + + // STARTTLS if required + if req.TLSType == "starttls" { + addLog("Starting TLS (STARTTLS)") + if err := client.StartTLS(tlsConfig); err != nil { + addLog(fmt.Sprintf("STARTTLS failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + addLog("TLS connection established") + } + + // Authenticate if credentials provided + if req.Username != "" && password != "" { + addLog(fmt.Sprintf("Authenticating as %s using %s", req.Username, req.AuthProtocol)) + var auth smtp.Auth + switch req.AuthProtocol { + case "plain": + auth = smtp.PlainAuth("", req.Username, password, req.Host) + case "login": + auth = &loginAuth{username: req.Username, password: password} + case "cram": + auth = smtp.CRAMMD5Auth(req.Username, password) + case "none": + addLog("No authentication required") + default: + auth = smtp.PlainAuth("", req.Username, password, req.Host) + } + if auth != nil { + if err := client.Auth(auth); err != nil { + addLog(fmt.Sprintf("Authentication failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + addLog("Authentication successful") + } + } + + // Set sender + fromAddr := req.EmailAddress + if fromAddr == "" { + fromAddr = req.Username + } + addLog(fmt.Sprintf("Setting sender: %s", fromAddr)) + if err := client.Mail(fromAddr); err != nil { + addLog(fmt.Sprintf("MAIL FROM failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + + // Set recipient + addLog(fmt.Sprintf("Setting recipient: %s", req.TestEmail)) + if err := client.Rcpt(req.TestEmail); err != nil { + addLog(fmt.Sprintf("RCPT TO failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + + // Send test message + addLog("Sending test message") + w, err := client.Data() + if err != nil { + addLog(fmt.Sprintf("DATA command failed: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + + msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: LibreDesk SMTP Test\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis is a test email from LibreDesk to verify your SMTP notification settings are working correctly.\r\n\r\nSent at: %s", + fromAddr, req.TestEmail, time.Now().Format(time.RFC1123)) + + if _, err := w.Write([]byte(msg)); err != nil { + addLog(fmt.Sprintf("Failed to write message: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + if err := w.Close(); err != nil { + addLog(fmt.Sprintf("Failed to close message: %v", err)) + return r.SendEnvelope(TestEmailResponse{Success: false, Logs: logs}) + } + + addLog("Test email sent successfully!") + client.Quit() + + return r.SendEnvelope(TestEmailResponse{Success: true, Logs: logs}) +} + +// loginAuth implements smtp.Auth for LOGIN authentication +type loginAuth struct { + username, password string +} + +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + } + } + return nil, nil +} diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 8c3b6b25..0a3f9995 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -61,6 +61,8 @@ const searchContacts = (params) => http.get('/api/v1/contacts/search', { params const getEmailNotificationSettings = () => http.get('/api/v1/settings/notifications/email') const updateEmailNotificationSettings = (data) => http.put('/api/v1/settings/notifications/email', data) +const testEmailNotificationSettings = (data) => + http.post("/api/v1/settings/notifications/email/test", data) const getPriorities = () => http.get('/api/v1/priorities') const getStatuses = () => http.get('/api/v1/statuses') const createStatus = (data) => http.post('/api/v1/statuses', data) @@ -536,6 +538,7 @@ export default { getUsersCompact, getEmailNotificationSettings, updateEmailNotificationSettings, + testEmailNotificationSettings, getCurrentUserViews, createView, updateView, diff --git a/frontend/src/features/admin/notification/NotificationSettingForm.vue b/frontend/src/features/admin/notification/NotificationSettingForm.vue index 440e5cf5..0e717a1d 100644 --- a/frontend/src/features/admin/notification/NotificationSettingForm.vue +++ b/frontend/src/features/admin/notification/NotificationSettingForm.vue @@ -197,7 +197,58 @@ - +