Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
209 changes: 209 additions & 0 deletions cmd/settings.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions frontend/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -536,6 +538,7 @@ export default {
getUsersCompact,
getEmailNotificationSettings,
updateEmailNotificationSettings,
testEmailNotificationSettings,
getCurrentUserViews,
createView,
updateView,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,58 @@
</FormItem>
</FormField>

<Button type="submit" :isLoading="isLoading"> {{ submitLabel }} </Button>
<div class="flex gap-2">
<Button type="submit" :isLoading="isLoading"> {{ submitLabel }} </Button>
</div>

<!-- Test Connection Section -->
<div class="border-t pt-6 mt-6">
<h3 class="text-lg font-medium mb-4">Test Connection</h3>
<div class="space-y-4">
<div class="space-y-2">
<Label for="test-email">Send test email to</Label>
<div class="flex gap-2">
<Input
id="test-email"
v-model="testEmail"
type="email"
placeholder="[email protected]"
class="flex-1"
/>
<Button
type="button"
variant="outline"
@click="runTest"
:disabled="isTesting || !testEmail"
>
<Loader2 v-if="isTesting" class="w-4 h-4 mr-2 animate-spin" />
{{ isTesting ? 'Testing...' : 'Test' }}
</Button>
</div>
</div>

<!-- Debug Log -->
<div v-if="testLogs.length > 0" class="space-y-2">
<Label>Debug Log</Label>
<div
class="bg-muted p-3 rounded-md font-mono text-xs max-h-48 overflow-y-auto"
:class="testSuccess === true ? 'border-green-500 border' : testSuccess === false ? 'border-red-500 border' : ''"
>
<div v-for="(log, index) in testLogs" :key="index" class="py-0.5">
{{ log }}
</div>
</div>
<div v-if="testSuccess === true" class="text-green-600 text-sm flex items-center gap-1">
<CheckCircle class="w-4 h-4" />
Test successful! Check your inbox.
</div>
<div v-else-if="testSuccess === false" class="text-red-600 text-sm flex items-center gap-1">
<XCircle class="w-4 h-4" />
Test failed. Check the log above for details.
</div>
</div>
</div>
</div>
</form>
</template>

Expand Down Expand Up @@ -228,8 +279,14 @@ import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { useI18n } from 'vue-i18n'
import { Loader2, CheckCircle, XCircle } from 'lucide-vue-next'
import api from '@/api'

const isLoading = ref(false)
const isTesting = ref(false)
const testEmail = ref('')
const testLogs = ref([])
const testSuccess = ref(null)
const { t } = useI18n()
const props = defineProps({
initialValues: {
Expand Down Expand Up @@ -275,4 +332,34 @@ watch(
},
{ deep: true, immediate: true }
)

// Run SMTP test
const runTest = async () => {
isTesting.value = true
testLogs.value = []
testSuccess.value = null

try {
const values = smtpForm.values
const response = await api.testEmailNotificationSettings({
'notification.email.host': values.host,
'notification.email.port': values.port,
'notification.email.username': values.username,
'notification.email.password': values.password,
'notification.email.auth_protocol': values.auth_protocol,
'notification.email.tls_type': values.tls_type,
'notification.email.tls_skip_verify': values.tls_skip_verify,
'notification.email.email_address': values.email_address,
'notification.email.hello_hostname': values.hello_hostname,
test_email: testEmail.value
})
testLogs.value = response.data.data.logs || []
testSuccess.value = response.data.data.success
} catch (error) {
testLogs.value = [`Error: ${error.response?.data?.message || error.message}`]
testSuccess.value = false
} finally {
isTesting.value = false
}
}
</script>