diff --git a/internal/config/settings.go b/internal/config/settings.go index c2d4d77..8d2493b 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -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"} } diff --git a/internal/locales/de.json b/internal/locales/de.json index 24334d1..d140864 100644 --- a/internal/locales/de.json +++ b/internal/locales/de.json @@ -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", diff --git a/internal/locales/de_ch.json b/internal/locales/de_ch.json index 274d8c1..9ff3d81 100644 --- a/internal/locales/de_ch.json +++ b/internal/locales/de_ch.json @@ -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ä", diff --git a/internal/locales/en.json b/internal/locales/en.json index 0d9daf1..1c9861d 100644 --- a/internal/locales/en.json +++ b/internal/locales/en.json @@ -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", diff --git a/internal/locales/es.json b/internal/locales/es.json index 6e98e27..b11ddd2 100644 --- a/internal/locales/es.json +++ b/internal/locales/es.json @@ -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", diff --git a/internal/locales/fr.json b/internal/locales/fr.json index 48f67ed..cf10373 100644 --- a/internal/locales/fr.json +++ b/internal/locales/fr.json @@ -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", diff --git a/internal/locales/it.json b/internal/locales/it.json index 7a96703..0c82e12 100644 --- a/internal/locales/it.json +++ b/internal/locales/it.json @@ -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", diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 9cef60f..483cdc1 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -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 diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index 87dbb11..579aefe 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -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 } diff --git a/pkg/web/static/js/settings.js b/pkg/web/static/js/settings.js index 6030f9a..3d17021 100644 --- a/pkg/web/static/js/settings.js +++ b/pkg/web/static/js/settings.js @@ -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); diff --git a/pkg/web/templates/index.html b/pkg/web/templates/index.html index 11ed67e..59f267f 100644 --- a/pkg/web/templates/index.html +++ b/pkg/web/templates/index.html @@ -757,10 +757,9 @@
Common ports: 25 (plain), 587 (STARTTLS), 465 (SMTPS), 2525 (alternative STARTTLS)
LOGIN is recommended for Office365/Gmail. PLAIN is standard SMTP auth. CRAM-MD5 is challenge-response based.
+⚠️ Please save your SMTP settings first before sending a test email.
+