Refactor sendEmail function and add support for multiple SMTP auth methods (LOGIN, PLAIN, CRAM-MD5) and TLS verification option, fix syntax error in sendSMTPMessage function

This commit is contained in:
2026-01-22 19:34:05 +01:00
parent 4e61fdf9f4
commit 90d4ff4e9a
11 changed files with 275 additions and 81 deletions

View File

@@ -40,12 +40,14 @@ import (
// SMTPSettings holds the SMTP server configuration for sending alert emails
type SMTPSettings struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
From string `json:"from"`
UseTLS bool `json:"useTLS"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
From string `json:"from"`
UseTLS bool `json:"useTLS"`
InsecureSkipVerify bool `json:"insecureSkipVerify"`
AuthMethod string `json:"authMethod"`
}
// AppSettings holds the main UI settings and Fail2ban configuration
@@ -413,12 +415,14 @@ func applyAppSettingsRecordLocked(rec storage.AppSettingsRecord) {
currentSettings.Banaction = rec.Banaction
currentSettings.BanactionAllports = rec.BanactionAllports
currentSettings.SMTP = SMTPSettings{
Host: rec.SMTPHost,
Port: rec.SMTPPort,
Username: rec.SMTPUsername,
Password: rec.SMTPPassword,
From: rec.SMTPFrom,
UseTLS: rec.SMTPUseTLS,
Host: rec.SMTPHost,
Port: rec.SMTPPort,
Username: rec.SMTPUsername,
Password: rec.SMTPPassword,
From: rec.SMTPFrom,
UseTLS: rec.SMTPUseTLS,
InsecureSkipVerify: rec.SMTPInsecureSkipVerify,
AuthMethod: rec.SMTPAuthMethod,
}
if rec.AlertCountriesJSON != "" {
@@ -504,12 +508,14 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) {
EmailAlertsForBans: currentSettings.EmailAlertsForBans,
EmailAlertsForUnbans: currentSettings.EmailAlertsForUnbans,
// SMTP settings
SMTPHost: currentSettings.SMTP.Host,
SMTPPort: currentSettings.SMTP.Port,
SMTPUsername: currentSettings.SMTP.Username,
SMTPPassword: currentSettings.SMTP.Password,
SMTPFrom: currentSettings.SMTP.From,
SMTPUseTLS: currentSettings.SMTP.UseTLS,
SMTPHost: currentSettings.SMTP.Host,
SMTPPort: currentSettings.SMTP.Port,
SMTPUsername: currentSettings.SMTP.Username,
SMTPPassword: currentSettings.SMTP.Password,
SMTPFrom: currentSettings.SMTP.From,
SMTPUseTLS: currentSettings.SMTP.UseTLS,
SMTPInsecureSkipVerify: currentSettings.SMTP.InsecureSkipVerify,
SMTPAuthMethod: currentSettings.SMTP.AuthMethod,
// Fail2Ban DEFAULT settings
BantimeIncrement: currentSettings.BantimeIncrement,
DefaultJailEnable: currentSettings.DefaultJailEnable,
@@ -654,6 +660,9 @@ func setDefaultsLocked() {
if !currentSettings.SMTP.UseTLS {
currentSettings.SMTP.UseTLS = true
}
if currentSettings.SMTP.AuthMethod == "" {
currentSettings.SMTP.AuthMethod = "auto"
}
if len(currentSettings.IgnoreIPs) == 0 {
currentSettings.IgnoreIPs = []string{"127.0.0.1/8", "::1"}
}

View File

@@ -142,6 +142,16 @@
"settings.smtp_host": "SMTP-Host",
"settings.smtp_host_placeholder": "z.B. smtp.gmail.com",
"settings.smtp_port": "SMTP-Port",
"settings.smtp_port_placeholder": "587",
"settings.smtp_port_hint": "Häufige Ports: 25 (unverschlüsselt), 587 (STARTTLS), 465 (SMTPS), 2525 (alternatives STARTTLS)",
"settings.smtp_auth_method": "Authentifizierungsmethode",
"settings.smtp_auth_method_auto": "Auto (LOGIN bevorzugt)",
"settings.smtp_auth_method_login": "LOGIN",
"settings.smtp_auth_method_plain": "PLAIN",
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN wird für Office365/Gmail empfohlen. PLAIN ist die Standard-SMTP-Authentifizierung. CRAM-MD5 ist challenge-response-basiert.",
"settings.smtp_insecure_skip_verify": "TLS-Zertifikatsüberprüfung überspringen",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Nicht für Produktion empfohlen",
"settings.smtp_username": "SMTP-Benutzername",
"settings.smtp_username_placeholder": "z.B. user@example.com",
"settings.smtp_password": "SMTP-Passwort",
@@ -150,6 +160,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "TLS verwenden (empfohlen)",
"settings.send_test_email": "Test-E-Mail senden",
"settings.send_test_email_hint": "⚠️ Bitte speichern Sie zuerst Ihre SMTP-Einstellungen, bevor Sie eine Test-E-Mail senden.",
"settings.fail2ban": "Globale Standard-Fail2Ban-Konfigurationen",
"settings.fail2ban.description": "Diese Einstellungen werden auf allen aktivierten Fail2Ban-Servern angewendet und in deren jail.local [DEFAULT]-Abschnitt gespeichert.",
"settings.enable_bantime_increment": "Bantime-Inkrement aktivieren",

View File

@@ -142,6 +142,16 @@
"settings.smtp_host": "SMTP-Host",
"settings.smtp_host_placeholder": "z.B. smtp.gmail.com",
"settings.smtp_port": "SMTP-Port",
"settings.smtp_port_placeholder": "587",
"settings.smtp_port_hint": "Hüfigi Ports: 25 (unverschlüsselt), 587 (STARTTLS), 465 (SMTPS), 2525 (alternativs STARTTLS)",
"settings.smtp_auth_method": "Authentifizierungsmethode",
"settings.smtp_auth_method_auto": "Auto (LOGIN bevorzugt)",
"settings.smtp_auth_method_login": "LOGIN",
"settings.smtp_auth_method_plain": "PLAIN",
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN wird für Office365/Gmail empfohle. PLAIN isch d Standard-SMTP-Authentifizierung. CRAM-MD5 isch challenge-response-basiert.",
"settings.smtp_insecure_skip_verify": "TLS-Zertifikatsüberprüfig überspringe",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Nid fürd Produktion empfohle",
"settings.smtp_username": "SMTP-Benutzername",
"settings.smtp_username_placeholder": "z.B. user@example.com",
"settings.smtp_password": "SMTP-Passwort",
@@ -150,6 +160,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "TLS bruuche (empfohlen)",
"settings.send_test_email": "Test-Email schicke",
"settings.send_test_email_hint": "⚠️ Bitte speichere zersch dini SMTP-Iistellige, bevor du e Test-Email schicksch.",
"settings.fail2ban": "Globale Standard-Fail2Ban-Konfiguratione",
"settings.fail2ban.description": "Die Einstellige werde uf alli aktivierte Fail2Ban-Server aagwändet und i däre jail.local [DEFAULT]-Abschnitt gspeicheret.",
"settings.enable_bantime_increment": "Bantime-Inkrement aktivierä",

View File

@@ -142,6 +142,16 @@
"settings.smtp_host": "SMTP Host",
"settings.smtp_host_placeholder": "e.g., smtp.gmail.com",
"settings.smtp_port": "SMTP Port",
"settings.smtp_port_placeholder": "587",
"settings.smtp_port_hint": "Common ports: 25 (plain), 587 (STARTTLS), 465 (SMTPS), 2525 (alternative STARTTLS)",
"settings.smtp_auth_method": "Authentication Method",
"settings.smtp_auth_method_auto": "Auto (LOGIN preferred)",
"settings.smtp_auth_method_login": "LOGIN",
"settings.smtp_auth_method_plain": "PLAIN",
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN is recommended for Office365/Gmail. PLAIN is standard SMTP auth. CRAM-MD5 is challenge-response based.",
"settings.smtp_insecure_skip_verify": "Skip TLS Certificate Verification",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Not recommended for production",
"settings.smtp_username": "SMTP Username",
"settings.smtp_username_placeholder": "e.g., user@example.com",
"settings.smtp_password": "SMTP Password",
@@ -150,6 +160,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Use TLS (Recommended)",
"settings.send_test_email": "Send Test Email",
"settings.send_test_email_hint": "⚠️ Please save your SMTP settings first before sending a test email.",
"settings.fail2ban": "Global Default Fail2Ban Configurations",
"settings.fail2ban.description": "These settings will be applied to all enabled Fail2Ban servers and stored in their jail.local [DEFAULT] section.",
"settings.enable_bantime_increment": "Enable Bantime Increment",

View File

@@ -142,6 +142,16 @@
"settings.smtp_host": "Host SMTP",
"settings.smtp_host_placeholder": "p.ej., smtp.gmail.com",
"settings.smtp_port": "Puerto SMTP",
"settings.smtp_port_placeholder": "587",
"settings.smtp_port_hint": "Puertos comunes: 25 (sin cifrar), 587 (STARTTLS), 465 (SMTPS), 2525 (STARTTLS alternativo)",
"settings.smtp_auth_method": "Método de Autenticación",
"settings.smtp_auth_method_auto": "Auto (LOGIN preferido)",
"settings.smtp_auth_method_login": "LOGIN",
"settings.smtp_auth_method_plain": "PLAIN",
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN se recomienda para Office365/Gmail. PLAIN es la autenticación SMTP estándar. CRAM-MD5 está basado en challenge-response.",
"settings.smtp_insecure_skip_verify": "Omitir Verificación de Certificado TLS",
"settings.smtp_insecure_skip_verify_warning": "⚠️ No recomendado para producción",
"settings.smtp_username": "Nombre de usuario SMTP",
"settings.smtp_username_placeholder": "p.ej., usuario@example.com",
"settings.smtp_password": "Contraseña SMTP",
@@ -150,6 +160,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Usar TLS (recomendado)",
"settings.send_test_email": "Enviar correo de prueba",
"settings.send_test_email_hint": "⚠️ Por favor, guarde primero su configuración SMTP antes de enviar un correo de prueba.",
"settings.fail2ban": "Configuraciones Globales Predeterminadas de Fail2Ban",
"settings.fail2ban.description": "Estas configuraciones se aplicarán a todos los servidores Fail2Ban habilitados y se almacenarán en su sección [DEFAULT] de jail.local.",
"settings.enable_bantime_increment": "Habilitar incremento de Bantime",

View File

@@ -142,6 +142,16 @@
"settings.smtp_host": "Hôte SMTP",
"settings.smtp_host_placeholder": "par exemple, smtp.gmail.com",
"settings.smtp_port": "Port SMTP",
"settings.smtp_port_placeholder": "587",
"settings.smtp_port_hint": "Ports communs: 25 (non chiffré), 587 (STARTTLS), 465 (SMTPS), 2525 (STARTTLS alternatif)",
"settings.smtp_auth_method": "Méthode d'Authentification",
"settings.smtp_auth_method_auto": "Auto (LOGIN préféré)",
"settings.smtp_auth_method_login": "LOGIN",
"settings.smtp_auth_method_plain": "PLAIN",
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN est recommandé pour Office365/Gmail. PLAIN est l'authentification SMTP standard. CRAM-MD5 est basé sur challenge-response.",
"settings.smtp_insecure_skip_verify": "Ignorer la Vérification du Certificat TLS",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Non recommandé pour la production",
"settings.smtp_username": "Nom d'utilisateur SMTP",
"settings.smtp_username_placeholder": "par exemple, utilisateur@example.com",
"settings.smtp_password": "Mot de passe SMTP",
@@ -150,6 +160,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Utiliser TLS (recommandé)",
"settings.send_test_email": "Envoyer un email de test",
"settings.send_test_email_hint": "⚠️ Veuillez d'abord enregistrer vos paramètres SMTP avant d'envoyer un email de test.",
"settings.fail2ban": "Configurations Globales par Défaut de Fail2Ban",
"settings.fail2ban.description": "Ces paramètres seront appliqués à tous les serveurs Fail2Ban activés et stockés dans leur section [DEFAULT] de jail.local.",
"settings.enable_bantime_increment": "Activer l'incrémentation du Bantime",

View File

@@ -142,6 +142,16 @@
"settings.smtp_host": "Host SMTP",
"settings.smtp_host_placeholder": "es. smtp.gmail.com",
"settings.smtp_port": "Porta SMTP",
"settings.smtp_port_placeholder": "587",
"settings.smtp_port_hint": "Porte comuni: 25 (non crittografato), 587 (STARTTLS), 465 (SMTPS), 2525 (STARTTLS alternativo)",
"settings.smtp_auth_method": "Metodo di Autenticazione",
"settings.smtp_auth_method_auto": "Auto (LOGIN preferito)",
"settings.smtp_auth_method_login": "LOGIN",
"settings.smtp_auth_method_plain": "PLAIN",
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN è raccomandato per Office365/Gmail. PLAIN è l'autenticazione SMTP standard. CRAM-MD5 è basato su challenge-response.",
"settings.smtp_insecure_skip_verify": "Ignora Verifica Certificato TLS",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Non raccomandato per la produzione",
"settings.smtp_username": "Nome utente SMTP",
"settings.smtp_username_placeholder": "es. utente@example.com",
"settings.smtp_password": "Password SMTP",
@@ -150,6 +160,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Usa TLS (raccomandato)",
"settings.send_test_email": "Invia email di test",
"settings.send_test_email_hint": "⚠️ Si prega di salvare prima le impostazioni SMTP prima di inviare un'email di test.",
"settings.fail2ban": "Configurazioni Globali Predefinite di Fail2Ban",
"settings.fail2ban.description": "Queste impostazioni verranno applicate a tutti i server Fail2Ban abilitati e memorizzate nella loro sezione [DEFAULT] di jail.local.",
"settings.enable_bantime_increment": "Abilita incremento del Bantime",

View File

@@ -63,12 +63,14 @@ type AppSettingsRecord struct {
// Console output settings
ConsoleOutput bool
// SMTP settings
SMTPHost string
SMTPPort int
SMTPUsername string
SMTPPassword string
SMTPFrom string
SMTPUseTLS bool
SMTPHost string
SMTPPort int
SMTPUsername string
SMTPPassword string
SMTPFrom string
SMTPUseTLS bool
SMTPInsecureSkipVerify bool
SMTPAuthMethod string
// Fail2Ban DEFAULT settings
BantimeIncrement bool
DefaultJailEnable bool
@@ -192,17 +194,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) {
}
row := db.QueryRowContext(ctx, `
SELECT language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, console_output
SELECT language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, console_output, smtp_insecure_skip_verify, smtp_auth_method
FROM app_settings
WHERE id = 1`)
var (
lang, callback, callbackSecret, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath sql.NullString
port, smtpPort, maxretry, maxLogLines sql.NullInt64
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn, emailAlertsForBans, emailAlertsForUnbans, consoleOutput sql.NullInt64
lang, callback, callbackSecret, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath, smtpAuthMethod sql.NullString
port, smtpPort, maxretry, maxLogLines sql.NullInt64
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn, emailAlertsForBans, emailAlertsForUnbans, consoleOutput, smtpInsecureSkipVerify sql.NullInt64
)
err := row.Scan(&lang, &port, &debug, &restartNeeded, &callback, &callbackSecret, &alerts, &emailAlertsForBans, &emailAlertsForUnbans, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines, &consoleOutput)
err := row.Scan(&lang, &port, &debug, &restartNeeded, &callback, &callbackSecret, &alerts, &emailAlertsForBans, &emailAlertsForUnbans, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines, &consoleOutput, &smtpInsecureSkipVerify, &smtpAuthMethod)
if errors.Is(err, sql.ErrNoRows) {
return AppSettingsRecord{}, false, nil
}
@@ -224,12 +226,14 @@ WHERE id = 1`)
EmailAlertsForBans: intToBool(intFromNull(emailAlertsForBans)),
EmailAlertsForUnbans: intToBool(intFromNull(emailAlertsForUnbans)),
// SMTP settings
SMTPHost: stringFromNull(smtpHost),
SMTPPort: intFromNull(smtpPort),
SMTPUsername: stringFromNull(smtpUser),
SMTPPassword: stringFromNull(smtpPass),
SMTPFrom: stringFromNull(smtpFrom),
SMTPUseTLS: intToBool(intFromNull(smtpTLS)),
SMTPHost: stringFromNull(smtpHost),
SMTPPort: intFromNull(smtpPort),
SMTPUsername: stringFromNull(smtpUser),
SMTPPassword: stringFromNull(smtpPass),
SMTPFrom: stringFromNull(smtpFrom),
SMTPUseTLS: intToBool(intFromNull(smtpTLS)),
SMTPInsecureSkipVerify: intToBool(intFromNull(smtpInsecureSkipVerify)),
SMTPAuthMethod: stringFromNull(smtpAuthMethod),
// Fail2Ban DEFAULT settings
BantimeIncrement: intToBool(intFromNull(bantimeInc)),
DefaultJailEnable: intToBool(intFromNull(defaultJailEn)),
@@ -258,9 +262,9 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error {
}
_, err := db.ExecContext(ctx, `
INSERT INTO app_settings (
id, language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, console_output
id, language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, console_output, smtp_insecure_skip_verify, smtp_auth_method
) VALUES (
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) ON CONFLICT(id) DO UPDATE SET
language = excluded.language,
port = excluded.port,
@@ -290,7 +294,9 @@ INSERT INTO app_settings (
geoip_provider = excluded.geoip_provider,
geoip_database_path = excluded.geoip_database_path,
max_log_lines = excluded.max_log_lines,
console_output = excluded.console_output
console_output = excluded.console_output,
smtp_insecure_skip_verify = excluded.smtp_insecure_skip_verify,
smtp_auth_method = excluded.smtp_auth_method
`, rec.Language,
rec.Port,
boolToInt(rec.Debug),
@@ -319,7 +325,9 @@ INSERT INTO app_settings (
rec.GeoIPProvider,
rec.GeoIPDatabasePath,
rec.MaxLogLines,
boolToInt(rec.ConsoleOutput))
boolToInt(rec.ConsoleOutput),
boolToInt(rec.SMTPInsecureSkipVerify),
rec.SMTPAuthMethod)
return err
}
@@ -940,13 +948,25 @@ CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status);
// return err
// }
// }
// Migration: Add console_output column if it doesn't exist
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN console_output INTEGER DEFAULT 0`); err != nil {
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
return err
}
}
// Migration: Add new SMTP columns if they don't exist
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN smtp_insecure_skip_verify INTEGER DEFAULT 0`); err != nil {
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
return err
}
}
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN smtp_auth_method TEXT DEFAULT 'auto'`); err != nil {
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
return err
}
}
_ = strings.Contains // Keep strings import for migration example above
return nil

View File

@@ -2413,10 +2413,19 @@ func isLOTRModeActive(alertCountries []string) bool {
func sendEmail(to, subject, body string, settings config.AppSettings) error {
// Validate SMTP settings
if settings.SMTP.Host == "" || settings.SMTP.Username == "" || settings.SMTP.Password == "" || settings.SMTP.From == "" {
return errors.New("SMTP settings are incomplete. Please configure all required fields")
err := errors.New("SMTP settings are incomplete. Please configure all required fields")
log.Printf("❌ sendEmail validation failed: %v (Host: %q, Username: %q, From: %q)", err, settings.SMTP.Host, settings.SMTP.Username, settings.SMTP.From)
return err
}
// Format message with **correct HTML headers**
// Validate port range
if settings.SMTP.Port <= 0 || settings.SMTP.Port > 65535 {
err := errors.New("SMTP port must be between 1 and 65535")
log.Printf("❌ sendEmail validation failed: %v (Port: %d)", err, settings.SMTP.Port)
return err
}
// Format message with correct HTML headers
message := fmt.Sprintf("From: %s\nTo: %s\nSubject: %s\n"+
"MIME-Version: 1.0\nContent-Type: text/html; charset=\"UTF-8\"\n\n%s",
settings.SMTP.From, to, subject, body)
@@ -2425,60 +2434,91 @@ func sendEmail(to, subject, body string, settings config.AppSettings) error {
// SMTP Connection Config
smtpHost := settings.SMTP.Host
smtpPort := settings.SMTP.Port
auth := LoginAuth(settings.SMTP.Username, settings.SMTP.Password)
smtpAddr := net.JoinHostPort(smtpHost, fmt.Sprintf("%d", smtpPort))
// **Choose Connection Type**
switch smtpPort {
case 465:
// SMTPS (Implicit TLS) - Not supported at the moment.
tlsConfig := &tls.Config{ServerName: smtpHost}
// Determine TLS configuration
tlsConfig := &tls.Config{
ServerName: smtpHost,
InsecureSkipVerify: settings.SMTP.InsecureSkipVerify,
}
// Determine authentication method
authMethod := settings.SMTP.AuthMethod
if authMethod == "" {
authMethod = "auto" // Default to auto if not set
}
auth, err := getSMTPAuth(settings.SMTP.Username, settings.SMTP.Password, authMethod, smtpHost)
if err != nil {
log.Printf("❌ sendEmail: failed to create SMTP auth (method: %q): %v", authMethod, err)
return fmt.Errorf("failed to create SMTP auth: %w", err)
}
log.Printf("📧 sendEmail: Using SMTP auth method: %q, host: %s, port: %d, useTLS: %v, insecureSkipVerify: %v", authMethod, smtpHost, smtpPort, settings.SMTP.UseTLS, settings.SMTP.InsecureSkipVerify)
// Determine connection type based on port and UseTLS setting
// Port 465 typically uses implicit TLS (SMTPS)
// Port 587 typically uses STARTTLS
// Other ports: use UseTLS setting to determine behavior
useImplicitTLS := (smtpPort == 465) || (settings.SMTP.UseTLS && smtpPort != 587 && smtpPort != 25)
useSTARTTLS := settings.SMTP.UseTLS && (smtpPort == 587 || (smtpPort != 465 && smtpPort != 25))
var client *smtp.Client
if useImplicitTLS {
// SMTPS (Implicit TLS) - Connect directly with TLS
conn, err := tls.Dial("tcp", smtpAddr, tlsConfig)
if err != nil {
return fmt.Errorf("failed to connect via TLS: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, smtpHost)
client, err = smtp.NewClient(conn, smtpHost)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
defer client.Quit()
if err := client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
return sendSMTPMessage(client, settings.SMTP.From, to, msg)
case 587:
// STARTTLS (Explicit TLS)
conn, err := net.Dial("tcp", smtpAddr)
} else {
// Plain connection (may upgrade to STARTTLS)
conn, err := net.DialTimeout("tcp", smtpAddr, 30*time.Second)
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, smtpHost)
client, err = smtp.NewClient(conn, smtpHost)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
defer client.Quit()
// Start TLS Upgrade
tlsConfig := &tls.Config{ServerName: smtpHost}
if err := client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("failed to start TLS: %w", err)
// Upgrade to STARTTLS if requested
if useSTARTTLS {
if err := client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("failed to start TLS: %w", err)
}
}
if err := client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
return sendSMTPMessage(client, settings.SMTP.From, to, msg)
}
return errors.New("unsupported SMTP port. Use 587 (STARTTLS) or 465 (SMTPS)")
// Ensure client is closed
defer func() {
if client != nil {
client.Quit()
}
}()
// Authenticate if credentials are provided
if auth != nil {
if err := client.Auth(auth); err != nil {
log.Printf("❌ sendEmail: SMTP authentication failed: %v", err)
return fmt.Errorf("SMTP authentication failed: %w", err)
}
log.Printf("📧 sendEmail: SMTP authentication successful")
}
err = sendSMTPMessage(client, settings.SMTP.From, to, msg)
if err != nil {
log.Printf("❌ sendEmail: Failed to send message: %v", err)
return err
}
log.Printf("📧 sendEmail: Successfully sent email to %s", to)
return nil
}
// Helper Function to Send SMTP Message
@@ -3104,8 +3144,37 @@ func TestEmailHandler(c *gin.Context) {
}
// *******************************************************************
// * Office365 LOGIN Authentication : *
// * SMTP Authentication Methods : *
// *******************************************************************
// getSMTPAuth returns the appropriate SMTP authentication mechanism
// based on the authMethod parameter: "auto", "login", "plain", "cram-md5"
func getSMTPAuth(username, password, authMethod, host string) (smtp.Auth, error) {
if username == "" || password == "" {
return nil, nil // No auth if credentials are empty
}
// Normalize auth method
authMethod = strings.ToLower(strings.TrimSpace(authMethod))
if authMethod == "" || authMethod == "auto" {
// Auto-detect: prefer LOGIN for Office365/Gmail, fallback to PLAIN
authMethod = "login"
}
switch authMethod {
case "login":
return LoginAuth(username, password), nil
case "plain":
return smtp.PlainAuth("", username, password, host), nil
case "cram-md5":
return smtp.CRAMMD5Auth(username, password), nil
default:
return nil, fmt.Errorf("unsupported auth method: %s (supported: login, plain, cram-md5)", authMethod)
}
}
// LoginAuth implements the LOGIN authentication mechanism
// Used by Office365, Gmail, and other providers that require LOGIN instead of PLAIN
type loginAuth struct {
username, password string
}

View File

@@ -27,7 +27,9 @@ function updateEmailFieldsState() {
document.getElementById('smtpUsername'),
document.getElementById('smtpPassword'),
document.getElementById('smtpFrom'),
document.getElementById('smtpAuthMethod'),
document.getElementById('smtpUseTLS'),
document.getElementById('smtpInsecureSkipVerify'),
document.getElementById('sendTestEmailBtn')
];
@@ -143,7 +145,9 @@ function loadSettings() {
document.getElementById('smtpUsername').value = data.smtp.username || '';
document.getElementById('smtpPassword').value = data.smtp.password || '';
document.getElementById('smtpFrom').value = data.smtp.from || '';
document.getElementById('smtpUseTLS').checked = data.smtp.useTLS || false;
document.getElementById('smtpUseTLS').checked = data.smtp.useTLS !== undefined ? data.smtp.useTLS : true;
document.getElementById('smtpInsecureSkipVerify').checked = data.smtp.insecureSkipVerify || false;
document.getElementById('smtpAuthMethod').value = data.smtp.authMethod || 'auto';
}
document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false;
@@ -186,13 +190,22 @@ function saveSettings(event) {
showLoading(true);
const smtpPort = parseInt(document.getElementById('smtpPort').value, 10);
if (isNaN(smtpPort) || smtpPort < 1 || smtpPort > 65535) {
showToast('SMTP port must be between 1 and 65535', 'error');
showLoading(false);
return;
}
const smtpSettings = {
host: document.getElementById('smtpHost').value.trim(),
port: parseInt(document.getElementById('smtpPort').value, 10) || 587,
port: smtpPort,
username: document.getElementById('smtpUsername').value.trim(),
password: document.getElementById('smtpPassword').value.trim(),
from: document.getElementById('smtpFrom').value.trim(),
useTLS: document.getElementById('smtpUseTLS').checked,
insecureSkipVerify: document.getElementById('smtpInsecureSkipVerify').checked,
authMethod: document.getElementById('smtpAuthMethod').value || 'auto',
};
const selectedCountries = Array.from(document.getElementById('alertCountries').selectedOptions).map(opt => opt.value);

View File

@@ -757,10 +757,9 @@
</div>
<div class="mb-4">
<label for="smtpPort" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_port">SMTP Port</label>
<select id="smtpPort" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed">
<option value="587" selected>587 (Recommended - STARTTLS)</option>
<option value="465" disabled>465 (Not Supported)</option>
</select>
<input type="number" min="1" max="65535" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpPort"
data-i18n-placeholder="settings.smtp_port_placeholder" placeholder="587" value="587" required />
<p class="mt-1 text-xs text-gray-500" data-i18n="settings.smtp_port_hint">Common ports: 25 (plain), 587 (STARTTLS), 465 (SMTPS), 2525 (alternative STARTTLS)</p>
</div>
<div class="mb-4">
<label for="smtpUsername" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_username">SMTP Username</label>
@@ -777,11 +776,29 @@
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpFrom"
data-i18n-placeholder="settings.smtp_sender_placeholder" placeholder="noreply@swissmakers.ch" required />
</div>
<div class="mb-4">
<label for="smtpAuthMethod" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_auth_method">Authentication Method</label>
<select id="smtpAuthMethod" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed">
<option value="auto" selected data-i18n="settings.smtp_auth_method_auto">Auto (LOGIN preferred)</option>
<option value="login" data-i18n="settings.smtp_auth_method_login">LOGIN</option>
<option value="plain" data-i18n="settings.smtp_auth_method_plain">PLAIN</option>
<option value="cram-md5" data-i18n="settings.smtp_auth_method_cram_md5">CRAM-MD5</option>
</select>
<p class="mt-1 text-xs text-gray-500" data-i18n="settings.smtp_auth_method_hint">LOGIN is recommended for Office365/Gmail. PLAIN is standard SMTP auth. CRAM-MD5 is challenge-response based.</p>
</div>
<div class="flex items-center mb-4">
<input type="checkbox" id="smtpUseTLS" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed">
<label for="smtpUseTLS" class="ml-2 block text-sm text-gray-700" data-i18n="settings.smtp_tls">Use TLS (Recommended)</label>
</div>
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed" onclick="sendTestEmail()" id="sendTestEmailBtn" data-i18n="settings.send_test_email">Send Test Email</button>
<div class="flex items-center mb-4">
<input type="checkbox" id="smtpInsecureSkipVerify" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed">
<label for="smtpInsecureSkipVerify" class="ml-2 block text-sm text-gray-700" data-i18n="settings.smtp_insecure_skip_verify">Skip TLS Certificate Verification</label>
<span class="ml-2 text-xs text-red-600" data-i18n="settings.smtp_insecure_skip_verify_warning">⚠️ Not recommended for production</span>
</div>
<div class="mb-4">
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed" onclick="sendTestEmail()" id="sendTestEmailBtn" data-i18n="settings.send_test_email">Send Test Email</button>
<p class="mt-2 text-xs text-amber-600" data-i18n="settings.send_test_email_hint">⚠️ Please save your SMTP settings first before sending a test email.</p>
</div>
</div>
<!-- Fail2Ban Configuration Group -->