260 lines
8.8 KiB
JavaScript
260 lines
8.8 KiB
JavaScript
/**
|
|
* 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)';
|
|
}
|