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

@@ -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 -->