/** * settings.js * ----------- * SMTP email settings modal. * * Loads saved settings from the server on open, lets the user edit and * save them, and sends a test email to verify the configuration works. * * Exports * ------- * initSettings() — wire up all listeners; call once at startup * smtpIsConfigured() — returns true if the server has smtp_host saved */ import { announce } from './state.js'; // ─── DOM refs (local to this module) ───────────────────────────────────────── const $ = id => document.getElementById(id); const modal = $('settings-modal'); const openBtn = $('settings-btn'); const openFromHint = $('open-settings-from-hint'); const closeBtn = $('close-settings'); const cancelBtn = $('settings-cancel'); const saveBtn = $('settings-save'); const saveStatus = $('settings-save-status'); const hostInput = $('smtp-host'); const portInput = $('smtp-port'); const securitySel = $('smtp-security'); const fromInput = $('smtp-from'); const userInput = $('smtp-user'); const passwordInput = $('smtp-password'); const passwordHint = $('smtp-password-hint'); const togglePwBtn = $('toggle-password'); const testToInput = $('smtp-test-to'); const testBtn = $('smtp-test-btn'); const testResult = $('smtp-test-result'); // ─── Module-level state ─────────────────────────────────────────────────────── let _configured = false; // whether smtp_host is set on the server // ─── Public API ─────────────────────────────────────────────────────────────── /** * Returns true if the server has an SMTP host configured. * Used by compress.js to warn the user before they start a job with * notifications enabled but no SMTP server set up. */ export function smtpIsConfigured() { return _configured; } /** * Attach all event listeners for the settings modal. * Call once during app initialisation. */ export function initSettings() { openBtn.addEventListener('click', openSettings); if (openFromHint) openFromHint.addEventListener('click', openSettings); const openFromWarn = document.getElementById('open-settings-from-warn'); if (openFromWarn) openFromWarn.addEventListener('click', openSettings); closeBtn.addEventListener('click', closeSettings); cancelBtn.addEventListener('click', closeSettings); modal.addEventListener('click', e => { if (e.target === modal) closeSettings(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && !modal.hidden) closeSettings(); }); saveBtn.addEventListener('click', saveSettings); testBtn.addEventListener('click', sendTestEmail); // Password show/hide toggle togglePwBtn.addEventListener('click', () => { const isHidden = passwordInput.type === 'password'; passwordInput.type = isHidden ? 'text' : 'password'; togglePwBtn.setAttribute('aria-label', isHidden ? 'Hide password' : 'Show password'); }); // Auto-fill port when security mode changes securitySel.addEventListener('change', () => { const presets = { tls: '587', ssl: '465', none: '25' }; portInput.value = presets[securitySel.value] || portInput.value; }); // Load current config silently at startup so smtpIsConfigured() works _fetchConfig(false); } // ─── Open / close ───────────────────────────────────────────────────────────── async function openSettings() { modal.hidden = false; document.body.style.overflow = 'hidden'; clearStatus(); await _fetchConfig(true); closeBtn.focus(); announce('SMTP settings panel opened'); } function closeSettings() { modal.hidden = true; document.body.style.overflow = ''; openBtn.focus(); announce('SMTP settings panel closed'); } // ─── Load settings from server ──────────────────────────────────────────────── async function _fetchConfig(populateForm) { try { const resp = await fetch('/api/settings/smtp'); if (!resp.ok) return; const cfg = await resp.json(); _configured = Boolean(cfg.host); if (!populateForm) return; hostInput.value = cfg.host || ''; portInput.value = cfg.port || '587'; fromInput.value = cfg.from_addr || ''; userInput.value = cfg.user || ''; passwordInput.value = ''; // never pre-fill passwords // Select the right security option const opt = securitySel.querySelector(`option[value="${cfg.security || 'tls'}"]`); if (opt) opt.selected = true; passwordHint.textContent = cfg.password_set ? 'A password is saved. Enter a new value to replace it, or leave blank to keep it.' : ''; } catch { // Silently ignore — server may not be reachable during init } } // ─── Save ──────────────────────────────────────────────────────────────────── async function saveSettings() { const host = hostInput.value.trim(); const port = portInput.value.trim(); const security = securitySel.value; const from = fromInput.value.trim(); const user = userInput.value.trim(); const password = passwordInput.value; // not trimmed — passwords may have spaces if (!host) { showStatus('SMTP server host is required.', 'fail'); hostInput.focus(); return; } if (!port || isNaN(Number(port))) { showStatus('A valid port number is required.', 'fail'); portInput.focus(); return; } if (!from || !from.includes('@')) { showStatus('A valid From address is required.', 'fail'); fromInput.focus(); return; } saveBtn.disabled = true; saveBtn.textContent = 'Saving…'; clearStatus(); try { const body = { host, port, security, from_addr: from, user }; if (password) body.password = password; const resp = await fetch('/api/settings/smtp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await resp.json(); if (!resp.ok) { showStatus(`Error: ${data.error}`, 'fail'); return; } _configured = Boolean(data.config?.host); passwordInput.value = ''; passwordHint.textContent = 'Password saved. Enter a new value to replace it, or leave blank to keep it.'; showStatus('Settings saved successfully.', 'ok'); announce('SMTP settings saved.'); } catch (err) { showStatus(`Network error: ${err.message}`, 'fail'); } finally { saveBtn.disabled = false; saveBtn.textContent = 'Save Settings'; } } // ─── Test email ─────────────────────────────────────────────────────────────── async function sendTestEmail() { const to = testToInput.value.trim(); if (!to || !to.includes('@')) { setTestResult('Please enter a valid recipient address.', 'fail'); testToInput.focus(); return; } testBtn.disabled = true; testBtn.textContent = 'Sending…'; setTestResult('Sending test email…', ''); try { const resp = await fetch('/api/settings/smtp/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to }), }); const data = await resp.json(); if (data.ok) { setTestResult(`✓ ${data.message}`, 'ok'); announce(`Test email sent to ${to}.`); } else { setTestResult(`✗ ${data.message}`, 'fail'); announce(`Test email failed: ${data.message}`); } } catch (err) { setTestResult(`Network error: ${err.message}`, 'fail'); } finally { testBtn.disabled = false; testBtn.textContent = 'Send Test'; } } // ─── Helpers ───────────────────────────────────────────────────────────────── function showStatus(msg, type) { saveStatus.textContent = msg; saveStatus.className = `settings-save-status ${type}`; } function clearStatus() { saveStatus.textContent = ''; saveStatus.className = 'settings-save-status'; setTestResult('', ''); } function setTestResult(msg, type) { testResult.textContent = msg; testResult.style.color = type === 'ok' ? 'var(--text-success)' : type === 'fail' ? 'var(--text-danger)' : 'var(--text-muted)'; }