// static/js/emailLibrary/settingsPage.js // // The Email Settings page: the in-panel settings view, its form markup and // controls, the away/auto-reply configuration, and the display preferences // (inline images, tag visibility, compact rows). // // Two things here reach outside settings, which is why they live together. // Turning away-replies on writes a matching calendar event, so this module owns // the `odysseus.email.autoReplyCalendarEvent.*` keys and the create/update/ // delete sync against /api/calendar. And the inline-image preference is read by // ./bodyRender.js while rendering a message, not only by the settings form. import spinnerModule from '../spinner.js'; import { emailApiUrl } from '../emailShared.js'; import { showToast } from '../ui.js?v=20260916largetoolscroll1'; import { state } from './state.js'; import { _esc } from './utils.js'; import { _wireEmailInlineImages } from './bodyRender.js'; import { _loadAccounts, _loadEmails, _loadFolders, _loadedEmailsHaveVisibleTags, _notifyNoLoadedEmailTags, _openTasksForEmailTags, _publishActiveAccount, _refreshUnreadBadge, _renderAccountsStrip, _renderGrid, _resetEmailListForFreshLoad, } from './index.js'; import { _openUnsubscribeReviewModal } from './unsubscribe.js'; const API_BASE = window.location.origin; function _emailInlineImagesStorageKey(accountId = state._libAccountId) { return `odysseus.email.viewInlineImages.${String(accountId || 'default')}`; } export function _readEmailInlineImagesPreference(accountId = state._libAccountId) { try { const stored = localStorage.getItem(_emailInlineImagesStorageKey(accountId)); return stored === null ? true : stored !== '0'; } catch (_) { return true; } } function _writeEmailInlineImagesPreference(enabled, accountId = state._libAccountId) { try { localStorage.setItem(_emailInlineImagesStorageKey(accountId), enabled ? '1' : '0'); } catch (_) {} } export async function _disableInlineImages(accountId = state._libAccountId) { state._libViewInlineImages = false; _writeEmailInlineImagesPreference(false, accountId); document.querySelectorAll('#email-settings-view-inline-images').forEach(toggle => { toggle.checked = true; toggle.dispatchEvent(new Event('change', { bubbles: true })); }); try { const res = await fetch(emailApiUrl('/api/email/config', { account_id: accountId || undefined, }), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email_view_inline_images: false }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch (err) { console.error('Failed to disable inline images:', err); } } export const _EMAIL_SETTINGS_ICON = ``; const _EMAIL_SAVE_ICON = ''; const _EMAIL_SAVED_ICON = ''; function _setEmailSaveIcon(button, saved) { if (!button) return; const icon = button.querySelector('svg'); if (icon) icon.outerHTML = saved ? _EMAIL_SAVED_ICON : _EMAIL_SAVE_ICON; button.classList.toggle('is-saved', !!saved); } const _DEFAULT_AUTO_REPLY_SUBJECT = '(Away) {subject}'; const _DEFAULT_AUTO_REPLY_MESSAGE = "Thanks for your email. I'm away and may be slower to reply."; function _normalizeAutoReplyConfig(cfg = {}) { const normalized = Object.prototype.hasOwnProperty.call(cfg, 'enabled'); return { enabled: normalized ? !!cfg.enabled : !!cfg.email_auto_reply, start: String(normalized ? (cfg.start || '') : (cfg.email_auto_reply_start || '')), end: String(normalized ? (cfg.end || '') : (cfg.email_auto_reply_end || '')), subject: String(normalized ? (cfg.subject || _DEFAULT_AUTO_REPLY_SUBJECT) : (cfg.email_auto_reply_subject || _DEFAULT_AUTO_REPLY_SUBJECT)), message: String(normalized ? (cfg.message || '') : (cfg.email_auto_reply_message || '')), cooldown: String(normalized ? (cfg.cooldown || 'period') : (cfg.email_auto_reply_cooldown || 'period')), scope: String(normalized ? (cfg.scope || 'all') : (cfg.email_auto_reply_scope || 'all')), accountId: String(normalized ? (cfg.accountId || '') : (cfg.email_auto_reply_account_id || '')), excludeAutomated: normalized ? cfg.excludeAutomated !== false : cfg.email_auto_reply_exclude_automated !== false, pauseNotifications: normalized ? !!cfg.pauseNotifications : !!cfg.email_auto_reply_pause_notifications, viewInlineImages: normalized ? cfg.viewInlineImages !== false : cfg.email_view_inline_images !== false, }; } function _autoReplyDateValue(value) { const s = String(value || '').trim(); const m = s.match(/^(\d{4}-\d{2}-\d{2})/); return m ? m[1] : ''; } function _todayDateInputValue() { const now = new Date(); const pad = value => String(value).padStart(2, '0'); return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; } const _AUTO_REPLY_CALENDAR_KEY_PREFIX = 'odysseus.email.autoReplyCalendarEvent.'; const _autoReplyCalendarSyncs = new Map(); function _autoReplyCalendarKey(accountId) { return `${_AUTO_REPLY_CALENDAR_KEY_PREFIX}${String(accountId || '').trim()}`; } function _readAutoReplyCalendarUid(accountId) { if (!accountId) return ''; try { return String(localStorage.getItem(_autoReplyCalendarKey(accountId)) || ''); } catch (_) { return ''; } } function _writeAutoReplyCalendarUid(accountId, uid) { if (!accountId) return; try { if (uid) localStorage.setItem(_autoReplyCalendarKey(accountId), uid); else localStorage.removeItem(_autoReplyCalendarKey(accountId)); } catch (_) {} } function _addLocalCalendarDays(value, days) { const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!match) return ''; const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + days); const pad = n => String(n).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } function _autoReplyCalendarPayload(cfg, accountId = state._libAccountId) { const start = _autoReplyDateValue(cfg?.start) || _todayDateInputValue(); let end = _autoReplyDateValue(cfg?.end) || start; if (end < start) end = start; return { summary: 'Email Auto Reply (away)', dtstart: start, dtend: _addLocalCalendarDays(end, 1), all_day: true, description: `Odysseus email auto reply - account:${String(accountId || '')}`, }; } async function _findAutoReplyCalendarEventUids(cfg, accountId) { const start = _autoReplyDateValue(cfg?.start) || _todayDateInputValue(); const end = _autoReplyDateValue(cfg?.end) || start; const startYear = Number(start.slice(0, 4)) || new Date().getFullYear(); const endYear = Math.max(startYear, Number(end.slice(0, 4)) || startYear); try { const response = await fetch(`${API_BASE}/api/calendar/events?start=${startYear - 1}-01-01&end=${endYear + 2}-01-01`, { credentials: 'same-origin', }); if (!response.ok) return []; const data = await response.json().catch(() => ({})); const marker = `Odysseus email auto reply - account:${String(accountId || '')}`; return (data.events || []) .filter(ev => String(ev?.description || '').startsWith(marker)) .map(ev => String(ev?.uid || '').trim()) .filter(Boolean); } catch (_) { return []; } } async function _syncAutoReplyCalendarEventNow(cfg, accountId) { accountId = String(accountId || '').trim(); if (!accountId) return false; let uid = _readAutoReplyCalendarUid(accountId); const headers = { 'Content-Type': 'application/json' }; const request = (url, options = {}) => fetch(url, { credentials: 'same-origin', ...options, headers: { ...headers, ...(options.headers || {}) }, }); if (!cfg?.enabled) { const knownUids = new Set(uid ? [uid] : []); (await _findAutoReplyCalendarEventUids(cfg, accountId)).forEach(foundUid => knownUids.add(foundUid)); for (const eventUid of knownUids) { const response = await request(`${API_BASE}/api/calendar/events/${encodeURIComponent(eventUid)}`, { method: 'DELETE' }); if (!response.ok && response.status !== 404) throw new Error(`Calendar HTTP ${response.status}`); } _writeAutoReplyCalendarUid(accountId, ''); return true; } const payload = _autoReplyCalendarPayload(cfg, accountId); if (!uid) { uid = (await _findAutoReplyCalendarEventUids(cfg, accountId))[0] || ''; if (uid) _writeAutoReplyCalendarUid(accountId, uid); } if (uid) { const response = await request(`${API_BASE}/api/calendar/events/${encodeURIComponent(uid)}`, { method: 'PUT', body: JSON.stringify(payload), }); if (response.ok) return true; if (response.status !== 404) throw new Error(`Calendar HTTP ${response.status}`); _writeAutoReplyCalendarUid(accountId, ''); } const response = await request(`${API_BASE}/api/calendar/events`, { method: 'POST', body: JSON.stringify(payload), }); const data = await response.json().catch(() => ({})); if (!response.ok || !data.uid) throw new Error(`Calendar HTTP ${response.status}`); _writeAutoReplyCalendarUid(accountId, data.uid); return true; } export function _syncAutoReplyCalendarEvent(cfg) { const accountId = String(state._libAccountId || '').trim(); if (!accountId) return Promise.resolve(false); const previous = _autoReplyCalendarSyncs.get(accountId) || Promise.resolve(); const next = previous.catch(() => {}).then(() => _syncAutoReplyCalendarEventNow(cfg, accountId)); _autoReplyCalendarSyncs.set(accountId, next); return next.finally(() => { if (_autoReplyCalendarSyncs.get(accountId) === next) _autoReplyCalendarSyncs.delete(accountId); }); } export function _isAutoReplyActiveForCurrentAccount(cfg) { cfg = _normalizeAutoReplyConfig(cfg || {}); if (!cfg.enabled) return false; if (cfg.scope === 'account' && cfg.accountId && cfg.accountId !== String(state._libAccountId || '')) return false; const today = new Date().toISOString().slice(0, 10); const start = _autoReplyDateValue(cfg.start); const end = _autoReplyDateValue(cfg.end); if (start && today < start) return false; if (end && today > end) return false; return true; } export function _syncEmailAutoReplyTitle(active) { const titleText = document.getElementById('email-lib-title-text'); const statusDot = document.getElementById('email-lib-auto-reply-status-dot'); const badge = document.getElementById('email-lib-auto-reply-badge'); if (titleText) titleText.textContent = active ? 'Auto Reply - Active' : 'Email'; if (statusDot) statusDot.style.display = active ? 'inline-block' : 'none'; if (badge) { badge.style.display = 'none'; badge.textContent = 'Auto Reply'; } } export async function _fetchEmailSettingsConfig() { const res = await fetch(emailApiUrl('/api/email/config', { account_id: state._libAccountId || undefined, }), { credentials: 'include' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const normalized = _normalizeAutoReplyConfig(await res.json()); state._libViewInlineImages = normalized.viewInlineImages; _writeEmailInlineImagesPreference(state._libViewInlineImages); return normalized; } export async function _fetchEmailWritingStyle() { const res = await fetch(emailApiUrl('/api/email/style', { account_id: state._libAccountId || undefined, }), { credentials: 'include' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json().catch(() => ({})); return String(data.style || ''); } export function _emailSettingsLoadingHtml(label = 'Loading') { const wp = spinnerModule.createWhirlpool(16); const host = document.createElement('span'); host.className = 'email-settings-loading-spinner'; host.appendChild(wp.element); const wrap = document.createElement('div'); wrap.className = 'email-settings-loading'; wrap.appendChild(host); const text = document.createElement('span'); text.textContent = label; wrap.appendChild(text); return wrap.outerHTML; } export function _emailSettingsFormHtml(cfg) { const activeAccount = state._libAccounts?.find(a => a && a.id === state._libAccountId); const accountAddress = activeAccount ? String(activeAccount.from_address || activeAccount.imap_user || activeAccount.email || '').trim() : ''; const accountScope = accountAddress ? ` (${_esc(accountAddress)})` : ''; return `
`; } export function _emailCleanupSettingsHtml() { const activeAccount = state._libAccounts?.find(a => a && a.id === state._libAccountId); const accountAddress = activeAccount ? String(activeAccount.from_address || activeAccount.imap_user || activeAccount.email || '').trim() : ''; const accountScope = accountAddress ? ` (${_esc(accountAddress)})` : ''; return `
`; } export function _emailDisplaySettingsHtml(cfg = {}) { return `
`; } export function _emailSettingsAccountSelectHtml() { const accounts = Array.isArray(state._libAccounts) ? state._libAccounts .filter(a => a && a.enabled !== false) .slice() .sort((a, b) => Number(!!b.is_default) - Number(!!a.is_default)) : []; if (!accounts.length) return ''; const nameCounts = new Map(); accounts.forEach(a => { const name = String(a.name || '').trim(); if (name) nameCounts.set(name, (nameCounts.get(name) || 0) + 1); }); const opts = accounts.map(a => { const name = String(a.name || '').trim(); const address = String(a.from_address || a.imap_user || '').trim(); const label = name && nameCounts.get(name) > 1 && address ? `${name} · ${address}` : (name || address || 'account'); const suffix = a.is_default ? ' · (default)' : ''; return ``; }).join(''); const selected = accounts.find(a => a.id === state._libAccountId) || accounts[0]; const active = !!selected?.is_default; const icon = active ? '' : ''; const defaultTitle = active ? 'Default account' : 'Make this the default account'; return `
` + `default` + `` + `
`; } export function _syncEmailSettingsAccountPicker(page) { const select = page?.querySelector('#email-settings-account-select'); const selected = (state._libAccounts || []).find(a => a && a.id === state._libAccountId); if (select && selected) { [...select.options] .sort((a, b) => { const aAccount = (state._libAccounts || []).find(account => account && account.id === a.value); const bAccount = (state._libAccounts || []).find(account => account && account.id === b.value); return Number(!!bAccount?.is_default) - Number(!!aAccount?.is_default); }) .forEach(option => select.appendChild(option)); select.value = selected.id; [...select.options].forEach(option => { const account = (state._libAccounts || []).find(a => a && a.id === option.value); if (!account) return; const label = account.name || account.from_address || account.imap_user || 'account'; option.textContent = label + (account.is_default ? ' · (default)' : ''); }); } const toggle = page?.querySelector('.email-settings-default'); if (!toggle || !selected) return; const active = !!selected.is_default; toggle.classList.toggle('active', active); toggle.setAttribute('aria-pressed', active ? 'true' : 'false'); toggle.dataset.emailDefaultId = selected.id || ''; toggle.title = active ? 'Default account' : 'Make this the default account'; toggle.querySelector('.cookbook-srv-default-icon').innerHTML = active ? '' : ''; } export function _emailWritingStyleHtml(style) { return `
`; } export async function _showEmailSettingsPage() { const modal = document.getElementById('email-lib-modal'); const page = document.getElementById('email-lib-settings-page'); const btn = document.getElementById('email-lib-settings-btn'); if (!modal || !page) return; modal.classList.add('email-settings-mode'); page.hidden = false; page.scrollTop = 0; page.querySelector('.email-settings-body')?.scrollTo(0, 0); const headerBackBtn = document.getElementById('email-settings-header-back'); if (headerBackBtn) headerBackBtn.style.display = 'inline-flex'; btn?.classList.add('active'); btn?.setAttribute('aria-expanded', 'true'); page.innerHTML = `

${_EMAIL_SETTINGS_ICON}Email Settings

${_emailSettingsLoadingHtml()}
`; let cfg; let writingStyle = ''; try { await _loadAccounts().catch(() => {}); const head = page.querySelector('.email-settings-page-head'); const accountSelectHtml = _emailSettingsAccountSelectHtml(); const oldPicker = page.querySelector('.email-settings-account-picker'); if (oldPicker) oldPicker.remove(); const title = head?.querySelector('.email-settings-title'); if (title && accountSelectHtml) title.insertAdjacentHTML('beforeend', accountSelectHtml); [cfg, writingStyle] = await Promise.all([ _fetchEmailSettingsConfig(), _fetchEmailWritingStyle().catch(() => ''), ]); } catch (err) { page.querySelector('.email-settings-body').innerHTML = `
Could not load settings.
`; return; } page.querySelector('.email-settings-body').innerHTML = _emailWritingStyleHtml(writingStyle) + _emailDisplaySettingsHtml(cfg) + _emailCleanupSettingsHtml() + _emailSettingsFormHtml(cfg); page.querySelector('.email-settings-body')?.scrollTo(0, 0); state._libAutoReplyActive = _isAutoReplyActiveForCurrentAccount(cfg); state._libAutoReplyDraftActive = null; _syncEmailAutoReplyTitle(state._libAutoReplyActive); _renderAccountsStrip(); _syncAutoReplyCalendarEvent(cfg).catch((err) => console.warn('Failed to reconcile auto-reply calendar event:', err)); page.querySelector('#email-settings-account-select')?.addEventListener('change', async (ev) => { state._libAccountId = ev.currentTarget.value || null; state._libAutoReplyDraftActive = null; _publishActiveAccount(); _syncEmailSettingsAccountPicker(page); _renderAccountsStrip(); _resetEmailListForFreshLoad(); _loadEmails({ useCache: true }); _loadFolders({ resetMissing: true }).catch(() => {}); page.querySelector('.email-unsubscribe-inline-panel')?.remove(); const body = page.querySelector('.email-settings-body'); if (body) body.innerHTML = _emailSettingsLoadingHtml(); try { const [nextCfg, nextStyle] = await Promise.all([ _fetchEmailSettingsConfig(), _fetchEmailWritingStyle().catch(() => ''), ]); if (body) body.innerHTML = _emailWritingStyleHtml(nextStyle) + _emailDisplaySettingsHtml(nextCfg) + _emailCleanupSettingsHtml() + _emailSettingsFormHtml(nextCfg); state._libAutoReplyActive = _isAutoReplyActiveForCurrentAccount(nextCfg); state._libAutoReplyDraftActive = null; _syncEmailAutoReplyTitle(state._libAutoReplyActive); _renderAccountsStrip(); _syncAutoReplyCalendarEvent(nextCfg).catch((err) => console.warn('Failed to reconcile auto-reply calendar event:', err)); _bindEmailSettingsPageControls(page); } catch (_) { if (body) body.innerHTML = `
Could not load settings.
`; } }); page.querySelector('.email-settings-default')?.addEventListener('click', async (ev) => { ev.stopPropagation(); const acctId = ev.currentTarget.dataset.emailDefaultId; const account = (state._libAccounts || []).find(a => a && a.id === acctId); if (!acctId || account?.is_default) return; try { const response = await fetch(`${API_BASE}/api/email/accounts/${encodeURIComponent(acctId)}/set-default`, { method: 'POST', credentials: 'same-origin', }); if (!response.ok) throw new Error(`HTTP ${response.status}`); for (const a of state._libAccounts) a.is_default = a.id === acctId; _syncEmailSettingsAccountPicker(page); _renderAccountsStrip(); } catch (err) { console.error('Set default account failed:', err); } }); page.querySelector('.email-settings-default')?.addEventListener('keydown', (ev) => { if (ev.key !== 'Enter' && ev.key !== ' ') return; ev.preventDefault(); ev.currentTarget.click(); }); _bindEmailSettingsPageControls(page); } export function _bindEmailSettingsPageControls(page) { page.querySelector('.email-settings-clean-btn')?.addEventListener('click', (ev) => { // Clean is an explicit fresh scan action; do not reopen stale cached // candidates when the user asks to clean again. _openUnsubscribeReviewModal(ev.currentTarget, { forceRescan: true }); }); const showTagsToggle = page.querySelector('#email-settings-show-tags'); const syncDisplayTagsState = () => { const section = page.querySelector('.email-settings-display-section'); const stateLabel = page.querySelector('.email-settings-display-enabled-state'); const enabled = !!showTagsToggle?.checked; section?.classList.toggle('is-disabled', !enabled); section?.classList.toggle('is-enabled', enabled); if (stateLabel) stateLabel.textContent = enabled ? 'Show' : 'Hide'; }; showTagsToggle?.addEventListener('change', (ev) => { state._libShowTags = !!ev.target.checked; localStorage.setItem('odysseus.email.showTags', state._libShowTags ? '1' : '0'); _renderGrid(); document.dispatchEvent(new CustomEvent('odysseus:email-tags-toggle', { detail: { show: state._libShowTags } })); if (state._libShowTags && !_loadedEmailsHaveVisibleTags()) _notifyNoLoadedEmailTags(); syncDisplayTagsState(); }); syncDisplayTagsState(); const inlineImagesToggle = page.querySelector('#email-settings-view-inline-images'); const syncInlineImagesState = () => { const section = page.querySelector('.email-settings-inline-images-section'); const stateLabel = page.querySelector('.email-settings-inline-images-enabled-state'); const hidden = !!inlineImagesToggle?.checked; section?.classList.toggle('is-disabled', !hidden); section?.classList.toggle('is-enabled', hidden); if (stateLabel) stateLabel.textContent = hidden ? 'Hide' : 'Show'; }; inlineImagesToggle?.addEventListener('change', async (ev) => { const hidden = !!ev.target.checked; const enabled = !hidden; const previous = state._libViewInlineImages; state._libViewInlineImages = enabled; _writeEmailInlineImagesPreference(enabled); syncInlineImagesState(); try { const res = await fetch(emailApiUrl('/api/email/config', { account_id: state._libAccountId || undefined, }), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email_view_inline_images: enabled }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); document.querySelectorAll('.email-card-reader').forEach(reader => _wireEmailInlineImages(reader)); } catch (err) { state._libViewInlineImages = previous; _writeEmailInlineImagesPreference(previous); ev.target.checked = previous === false; syncInlineImagesState(); showToast?.('Failed to save inline image setting'); console.error('Failed to save inline image setting:', err); } }); syncInlineImagesState(); page.querySelector('[data-open-email-tasks]')?.addEventListener('click', _openTasksForEmailTags); const autoReplyStart = page.querySelector('#email-auto-reply-start'); const seedAutoReplyStartDate = () => { if (autoReplyStart && !autoReplyStart.value) autoReplyStart.value = _todayDateInputValue(); }; // Empty native date inputs can open on an arbitrary browser baseline date. // Seed only the draft field when the picker is opened, leaving saved settings // unchanged until the user explicitly presses Save. autoReplyStart?.addEventListener('pointerdown', seedAutoReplyStartDate); autoReplyStart?.addEventListener('focus', seedAutoReplyStartDate); const enabledToggle = page.querySelector('#email-auto-reply-enabled'); const syncEnabledState = () => { const section = page.querySelector('.email-settings-auto-reply-section'); const stateLabel = section?.querySelector('.email-settings-enabled-state'); const enabled = !!enabledToggle?.checked; section?.classList.toggle('is-disabled', !enabled); section?.classList.toggle('is-enabled', enabled); if (stateLabel) stateLabel.textContent = enabled ? 'Enabled' : 'Disabled'; }; const saveAutoReplySettings = async ({ quiet = false } = {}) => { const saveBtn = page.querySelector('.email-settings-save'); const status = page.querySelector('.email-settings-status'); if (saveBtn && !quiet) saveBtn.disabled = true; _setEmailSaveIcon(saveBtn, false); if (status) { status.classList.remove('is-success', 'is-error'); status.textContent = 'Saving…'; } const payload = { email_auto_reply: !!page.querySelector('#email-auto-reply-enabled')?.checked, email_auto_reply_start: page.querySelector('#email-auto-reply-start')?.value || '', email_auto_reply_end: page.querySelector('#email-auto-reply-end')?.value || '', email_auto_reply_subject: page.querySelector('#email-auto-reply-subject')?.value || '', email_auto_reply_message: page.querySelector('#email-auto-reply-message')?.value || '', email_auto_reply_cooldown: page.querySelector('#email-auto-reply-cooldown')?.value || 'period', email_auto_reply_scope: 'account', email_auto_reply_account_id: state._libAccountId || '', email_auto_reply_pause_notifications: !!page.querySelector('#email-auto-reply-pause')?.checked, }; try { const res = await fetch(emailApiUrl('/api/email/config', { account_id: state._libAccountId || undefined, }), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const savedCfg = { enabled: !!payload.email_auto_reply, start: payload.email_auto_reply_start, end: payload.email_auto_reply_end, }; let calendarSynced = true; try { await _syncAutoReplyCalendarEvent(savedCfg); } catch (calendarErr) { calendarSynced = false; console.warn('Failed to sync auto-reply calendar event:', calendarErr); } if (status) { status.classList.add('is-success'); status.textContent = 'Saved'; } _setEmailSaveIcon(saveBtn, true); if (!quiet) showToast?.('Email settings saved'); if (!calendarSynced) showToast?.('Saved, but the calendar event could not be updated'); state._libAutoReplyActive = _isAutoReplyActiveForCurrentAccount(savedCfg); state._libAutoReplyDraftActive = null; _syncEmailAutoReplyTitle(state._libAutoReplyActive); _renderAccountsStrip(); _refreshUnreadBadge({ preserveAutoReplyTitle: true }).catch(() => {}); setTimeout(() => { _setEmailSaveIcon(saveBtn, false); if (status) status.textContent = ''; }, quiet ? 900 : 1400); return true; } catch (err) { if (status) { status.classList.add('is-error'); status.textContent = 'Save failed'; } showToast?.('Failed to save email settings'); return false; } finally { if (saveBtn) saveBtn.disabled = false; } }; enabledToggle?.addEventListener('change', () => { syncEnabledState(); // Invalidate any config read that started before this toggle. Its stale // result must not overwrite the title we are about to render. state._autoReplyRefreshSeq += 1; const active = _isAutoReplyActiveForCurrentAccount({ email_auto_reply: !!enabledToggle.checked, email_auto_reply_start: autoReplyStart?.value || '', email_auto_reply_end: page.querySelector('#email-auto-reply-end')?.value || '', email_auto_reply_scope: 'account', email_auto_reply_account_id: state._libAccountId || '', }); state._libAutoReplyActive = active; state._libAutoReplyDraftActive = active; _syncEmailAutoReplyTitle(active); _renderAccountsStrip(); saveAutoReplySettings({ quiet: true }); }); syncEnabledState(); page.querySelector('.email-settings-save')?.addEventListener('click', async () => { await saveAutoReplySettings(); }); page.querySelector('.email-style-settings-save')?.addEventListener('click', async () => { const saveBtn = page.querySelector('.email-style-settings-save'); const status = page.querySelector('.email-style-settings-status'); saveBtn.disabled = true; _setEmailSaveIcon(saveBtn, false); if (status) { status.classList.remove('is-success', 'is-error'); status.textContent = 'Saving…'; } try { const res = await fetch(emailApiUrl('/api/email/style', { account_id: state._libAccountId || undefined, }), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ style: page.querySelector('#email-writing-style-text')?.value || '' }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); if (status) { status.classList.add('is-success'); status.textContent = 'Saved'; } _setEmailSaveIcon(saveBtn, true); showToast?.('Email writing style saved'); setTimeout(() => { _setEmailSaveIcon(saveBtn, false); if (status) status.textContent = ''; }, 1400); } catch (err) { if (status) { status.classList.add('is-error'); status.textContent = 'Save failed'; } showToast?.('Failed to save email writing style'); } finally { saveBtn.disabled = false; } }); page.querySelector('.email-style-settings-extract')?.addEventListener('click', async () => { const extractBtn = page.querySelector('.email-style-settings-extract'); const saveBtn = page.querySelector('.email-style-settings-save'); const status = page.querySelector('.email-style-settings-status'); const styleEl = page.querySelector('#email-writing-style-text'); extractBtn.disabled = true; if (saveBtn) saveBtn.disabled = true; let wp = null; if (status) { status.innerHTML = ''; try { wp = spinnerModule.createWhirlpool(14); wp.element.style.cssText = 'display:inline-block;vertical-align:-3px;margin-right:6px;'; status.appendChild(wp.element); status.appendChild(document.createTextNode('Analyzing sent emails…')); } catch (_) { status.textContent = 'Analyzing sent emails…'; } } try { const res = await fetch(emailApiUrl('/api/email/extract-style', { account_id: state._libAccountId || undefined, }), { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ sample_count: 15 }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.success || !data.style) throw new Error(data.error || `HTTP ${res.status}`); if (styleEl) styleEl.value = data.style; if (status) status.textContent = 'Extracted'; showToast?.('Email writing style extracted'); setTimeout(() => { if (status) status.textContent = ''; }, 1800); } catch (err) { if (status) status.textContent = err?.message || 'Extract failed'; showToast?.('Failed to extract email writing style'); } finally { if (wp && wp.destroy) { try { wp.destroy(); } catch (_) {} } extractBtn.disabled = false; if (saveBtn) saveBtn.disabled = false; } }); } export function _hideEmailSettingsPage() { const modal = document.getElementById('email-lib-modal'); const page = document.getElementById('email-lib-settings-page'); const btn = document.getElementById('email-lib-settings-btn'); const headerBackBtn = document.getElementById('email-settings-header-back'); modal?.classList.remove('email-settings-mode'); if (page) page.hidden = true; if (headerBackBtn) headerBackBtn.style.display = 'none'; btn?.classList.remove('active'); btn?.setAttribute('aria-expanded', 'false'); }