`;
}).join('');
}
// ── Accounts ───────────────────────────────────────────────────────────────
async function loadAccounts() {
const data = await api('GET','/accounts');
if (!data) return;
S.accounts = data;
renderAccountsSettingsList();
populateComposeFrom();
loadSignatures();
}
// ── Signatures (compose prefill) ────────────────────────────────────────────
async function loadSignatures() {
const data = await api('GET','/signatures');
if (data) S.signatures = data;
}
// Returns the HTML for an account's default-for-new or default-for-reply signature, or ''.
function getSignatureHTML(accountId, forReply) {
const acc = S.accounts.find(a => a.id === accountId);
if (!acc) return '';
const sigId = forReply ? acc.default_signature_reply_id : acc.default_signature_new_id;
if (!sigId) return '';
const sig = S.signatures.find(s => s.id === sigId);
return sig ? sig.content_html : '';
}
// Renders the (possibly empty) signature block with a stable id so it can be swapped
// live if the From-account changes mid-compose, without touching the rest of the body.
function signatureBlockHTML(accountId, forReply) {
const html = getSignatureHTML(accountId, forReply);
// A block-level "
" renders as exactly one blank line in every browser;
// a bare
doesn't — its rendered height depends on what precedes/follows it
// (e.g. it collapses to nothing right before another block element), which is exactly
// the inconsistency this was written to avoid.
return `
${html ? '
' + html : ''}
`;
}
// Bound to #compose-from's change event — swaps just the signature block.
function onComposeFromChange() {
const accountId = parseInt(document.getElementById('compose-from')?.value || 0);
const forReply = S.composeMode !== 'new';
const block = document.getElementById('sig-block');
if (block) block.outerHTML = signatureBlockHTML(accountId, forReply);
}
function connectOAuth(p) {
if (p === 'outlook_personal') {
location.href = '/auth/outlook-personal/connect';
} else {
location.href = '/auth/' + p + '/connect';
}
}
function openAddAccountModal() {
['imap-email','imap-name','imap-password','imap-host','smtp-host','imap-caldav-url','imap-carddav-url'].forEach(id=>{ const el=document.getElementById(id); if(el) el.value=''; });
document.getElementById('imap-port').value='993';
document.getElementById('smtp-port').value='587';
document.getElementById('use-jmap').checked=false;
toggleJMAPFields();
const r=document.getElementById('test-result'); if(r){r.style.display='none';r.className='test-result';}
openModal('add-account-modal');
}
// Toggles the Add Account modal between IMAP/SMTP fields and a single JMAP
// server URL field (JMAP has no separate SMTP host/port or IMAP port — one
// base URL covers both mail access and sending).
function toggleJMAPFields() {
const jmap=document.getElementById('use-jmap').checked;
document.getElementById('imap-port-field').style.display=jmap?'none':'';
document.getElementById('smtp-fields').style.display=jmap?'none':'';
document.getElementById('imap-hint').style.display=jmap?'none':'';
const label=document.getElementById('imap-host-label'), host=document.getElementById('imap-host');
label.textContent=jmap?'JMAP Server URL':'IMAP Host';
host.placeholder=jmap?'https://mail.example.com:8443':'imap.example.com';
}
async function testNewConnection() {
const btn=document.getElementById('test-btn'), result=document.getElementById('test-result');
const jmap=document.getElementById('use-jmap').checked;
const body={email:document.getElementById('imap-email').value.trim(),password:document.getElementById('imap-password').value,
imap_host:document.getElementById('imap-host').value.trim()};
if (jmap) { body.provider='jmap'; }
else {
body.imap_port=parseInt(document.getElementById('imap-port').value)||993;
body.smtp_host=document.getElementById('smtp-host').value.trim();
body.smtp_port=parseInt(document.getElementById('smtp-port').value)||587;
}
if (!body.email||!body.password||!body.imap_host){result.textContent=(jmap?'Email, password and JMAP server URL required.':'Email, password and IMAP host required.');result.className='test-result err';result.style.display='block';return;}
btn.innerHTML='Testing...';btn.disabled=true;
const r=await api('POST','/accounts/test',body,20000);
btn.textContent='Test Connection';btn.disabled=false;
result.textContent=(r?.ok)?'✓ Connection successful!':((r?.error?.message||r?.error)||'Connection failed');
result.className='test-result '+((r?.ok)?'ok':'err'); result.style.display='block';
}
async function addIMAPAccount() {
const btn=document.getElementById('save-acct-btn');
const jmap=document.getElementById('use-jmap').checked;
const body={email:document.getElementById('imap-email').value.trim(),display_name:document.getElementById('imap-name').value.trim(),
password:document.getElementById('imap-password').value,imap_host:document.getElementById('imap-host').value.trim()};
if (jmap) { body.provider='jmap'; }
else {
body.imap_port=parseInt(document.getElementById('imap-port').value)||993;
body.smtp_host=document.getElementById('smtp-host').value.trim();
body.smtp_port=parseInt(document.getElementById('smtp-port').value)||587;
}
body.caldav_url=document.getElementById('imap-caldav-url').value.trim();
body.carddav_url=document.getElementById('imap-carddav-url').value.trim();
if (!body.email||!body.password||!body.imap_host){toast((jmap?'Email, password and JMAP server URL required':'Email, password and IMAP host required'),'error');return;}
btn.disabled=true;btn.textContent='Connecting...';
const r=await api('POST','/accounts',body);
btn.disabled=false;btn.textContent='Connect';
if (r?.ok){
toast('Account added — syncing…','success');
closeModal('add-account-modal');
await loadAccounts();
pollForNewFolders();
} else toast(r?.error||'Failed to add account','error');
}
// Repeatedly reloads folders/accounts until the newly-added account's folders show up
// (initial IMAP sync takes a few seconds), instead of guessing a fixed delay. Also
// refreshes messages/unread counts so the sidebar updates without a manual page reload.
function pollForNewFolders() {
let tries = 0;
const poll = setInterval(async () => {
tries++;
await loadAccounts();
await loadFolders();
await loadMessages();
const hasFolders = S.accounts.some(a => S.folders.some(f => f.account_id === a.id));
if (hasFolders || tries >= 12) {
clearInterval(poll);
if (hasFolders) toast('Account ready!', 'success');
}
}, 2500);
}
async function detectMailSettings() {
const email=document.getElementById('imap-email').value.trim();
if (!email||!email.includes('@')){toast('Enter your email address first','error');return;}
const btn=document.getElementById('detect-btn');
btn.innerHTML='Detecting…';btn.disabled=true;
const r=await api('POST','/accounts/detect',{email});
btn.textContent='Auto-detect';btn.disabled=false;
if (!r){toast('Detection failed','error');return;}
document.getElementById('imap-host').value=r.imap_host||'';
document.getElementById('imap-port').value=r.imap_port||993;
document.getElementById('smtp-host').value=r.smtp_host||'';
document.getElementById('smtp-port').value=r.smtp_port||587;
if(r.detected) toast(`Detected ${r.imap_host} / ${r.smtp_host}`,'success');
else toast('No servers found — filled with defaults based on domain','info');
}
async function syncNow(id, e) {
if (e) e.stopPropagation();
toast('Syncing…','info');
const r = await api('POST','/accounts/'+id+'/sync');
if (r?.ok) { toast('Synced '+(r.synced||0)+' messages','success'); loadAccounts(); loadFolders(); loadMessages(); }
else toast(r?.error||'Sync failed','error');
}
// ── Edit Account modal ─────────────────────────────────────────────────────
// Opened from the Settings modal's Accounts tab; stacks above it (see
// #edit-account-modal z-index in CSS) rather than closing Settings, so
// Cancel/Save land the user back in Settings automatically.
async function openEditAccount(id) {
const r=await api('GET','/accounts/'+id);
if (!r) return;
document.getElementById('edit-account-id').value=id;
document.getElementById('edit-account-email').textContent=r.email_address;
document.getElementById('edit-name').value=r.display_name||'';
const isOAuth = r.provider==='gmail' || r.provider==='outlook' || r.provider==='outlook_personal';
// Show/hide credential section and test button based on provider type
document.getElementById('edit-creds-section').style.display = isOAuth ? 'none' : '';
document.getElementById('edit-test-btn').style.display = isOAuth ? 'none' : '';
const oauthSection = document.getElementById('edit-oauth-section');
if (oauthSection) oauthSection.style.display = isOAuth ? '' : 'none';
if (isOAuth) {
const providerLabel = r.provider==='gmail' ? 'Google' : r.provider==='outlook_personal' ? 'Microsoft (Personal)' : 'Microsoft';
const lbl = document.getElementById('edit-oauth-provider-label');
const lblBtn = document.getElementById('edit-oauth-provider-label-btn');
const expWarn = document.getElementById('edit-oauth-expired-warning');
if (lbl) lbl.textContent = providerLabel;
if (lblBtn) lblBtn.textContent = providerLabel;
if (expWarn) expWarn.style.display = r.token_expired ? '' : 'none';
const reconnectBtn = document.getElementById('edit-oauth-reconnect-btn');
if (reconnectBtn) reconnectBtn.onclick = () => {
closeModal('edit-account-modal');
connectOAuth(r.provider);
};
}
const isJMAP = r.provider==='jmap';
if (!isOAuth) {
document.getElementById('edit-password').value='';
document.getElementById('edit-imap-host').value=r.imap_host||'';
document.getElementById('edit-imap-port').value=r.imap_port||993;
document.getElementById('edit-smtp-host').value=r.smtp_host||'';
document.getElementById('edit-smtp-port').value=r.smtp_port||587;
document.getElementById('edit-imap-port-field').style.display=isJMAP?'none':'';
document.getElementById('edit-smtp-fields').style.display=isJMAP?'none':'';
document.getElementById('edit-imap-host-label').textContent=isJMAP?'JMAP Server URL':'IMAP Host';
}
document.getElementById('edit-caldav-url').value=r.caldav_url||'';
document.getElementById('edit-carddav-url').value=r.carddav_url||'';
document.getElementById('edit-sync-days').value=r.sync_days||30;
const sel = document.getElementById('edit-sync-mode');
if (r.sync_mode==='all' || !r.sync_days) {
sel.value='all';
} else {
const presetMap={30:'preset-30',90:'preset-90',180:'preset-180',365:'preset-365',730:'preset-730',1825:'preset-1825'};
sel.value = presetMap[r.sync_days] || 'days';
}
toggleSyncDaysField();
const errEl=document.getElementById('edit-last-error'), connEl=document.getElementById('edit-conn-result');
connEl.style.display='none';
errEl.style.display=r.last_error?'block':'none';
if (r.last_error) errEl.textContent='Last sync error: '+r.last_error;
const hiddenEl = document.getElementById('edit-hidden-folders');
const hidden = S.folders.filter(f=>f.account_id===id && f.is_hidden);
if (!hidden.length) {
hiddenEl.innerHTML='No hidden folders.';
} else {
hiddenEl.innerHTML = hidden.map(f=>`
${esc(f.name)}
`).join('');
}
openModal('edit-account-modal');
}
async function unhideFolder(folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
const r = await api('PUT','/folders/'+folderId+'/visibility',{is_hidden:false, sync_enabled:true});
if (r?.ok) {
toast('Folder restored to sidebar','success');
await loadFolders();
// Refresh hidden list in modal
const accId = parseInt(document.getElementById('edit-account-id').value);
if (accId) {
const hiddenEl = document.getElementById('edit-hidden-folders');
const hidden = S.folders.filter(f=>f.account_id===accId && f.is_hidden);
if (!hidden.length) hiddenEl.innerHTML='No hidden folders.';
else hiddenEl.innerHTML = hidden.map(f=>`
` : '';
// Default folders (inbox/sent/drafts/trash/spam) are provider-managed — deleting them
// would break sync. Only user-created "custom" folders can be deleted.
const deleteEntry = f.folder_type==='custom'
? `
🗑 Delete folder
` : '';
showCtxMenu(e, `
📁 New folder
↻ Sync this folder
${syncLabel}
${enableAllEntry}
✓ Mark all as read
⬇ Export folder
›
as .zip (.eml files)
as .mbox
${moveEntry}
${emptyEntry}
👁 Hide from sidebar
${deleteEntry}`);
}
function showAccountMenu(e, accountId) {
e.preventDefault(); e.stopPropagation();
showCtxMenu(e, `
📁 New folder
`);
}
function createFolderPrompt(accountId) {
inlinePrompt('New folder name:', async (name) => {
const r = await api('POST', '/accounts/'+accountId+'/folders', { name });
if (r?.ok) {
S.folders.push(r.folder);
toast('Folder created', 'success');
renderFolders();
} else toast(r?.error || 'Create failed', 'error');
});
}
async function syncFolderNow(folderId) {
toast('Syncing folder…','info');
const r=await api('POST','/folders/'+folderId+'/sync');
if (r?.ok) { toast('Synced '+(r.synced||0)+' messages','success'); loadFolders(); loadMessages(); }
else toast(r?.error||'Sync failed','error');
}
function exportFolder(folderId, format) {
window.open('/api/folders/'+folderId+'/export?format='+format, '_blank');
}
async function markFolderAllRead(folderId) {
const r=await api('POST','/folders/'+folderId+'/mark-all-read');
if(r?.ok){
toast(`Marked ${r.marked||0} message(s) as read`,'success');
loadFolders();
loadMessages();
} else toast(r?.error||'Failed','error');
}
async function toggleFolderSync(folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
const newSync = !f.sync_enabled;
const r = await api('PUT','/folders/'+folderId+'/visibility',{is_hidden:f.is_hidden, sync_enabled:newSync});
if (r?.ok) {
f.sync_enabled = newSync;
toast(newSync?'Folder sync enabled':'Folder sync disabled', 'success');
renderFolders();
} else toast('Update failed','error');
}
async function enableAllFolderSync(accountId) {
const r = await api('POST','/accounts/'+accountId+'/enable-all-sync');
if (r?.ok) {
// Update local state
S.folders.forEach(f=>{ if(f.account_id===accountId) f.sync_enabled=true; });
toast(`Sync enabled for ${r.enabled||0} folder${r.enabled===1?'':'s'}`, 'success');
renderFolders();
} else toast('Failed to enable sync', 'error');
}
async function confirmEmptyFolder(folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
const label = f.folder_type==='trash' ? 'Trash' : 'Spam';
inlineConfirm(
`Permanently delete all messages in ${label}? This cannot be undone.`,
async () => {
const r = await api('POST','/folders/'+folderId+'/empty');
if (r?.ok) {
toast(`Emptied ${label} (${r.deleted||0} messages)`, 'success');
// Remove locally
S.messages = S.messages.filter(m=>m.folder_id!==folderId);
if (S.currentMessage && S.currentFolder===folderId) resetDetail();
await loadFolders();
if (S.currentFolder===folderId) renderMessageList();
} else toast('Failed to empty folder','error');
}
);
}
async function confirmHideFolder(folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
inlineConfirm(
`Hide "${f.name}" from sidebar? You can unhide it from account settings.`,
async () => {
const r = await api('PUT','/folders/'+folderId+'/visibility',{is_hidden:true, sync_enabled:false});
if (r?.ok) { toast('Folder hidden','success'); await loadFolders(); }
else toast('Update failed','error');
}
);
}
async function confirmDeleteFolder(folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
const countRes = await api('GET','/folders/'+folderId+'/count');
const count = countRes?.count ?? '?';
inlineConfirm(
`Delete folder "${f.name}"? This will permanently delete all ${count} message${count===1?'':'s'} inside it. This cannot be undone.`,
async () => {
const r = await api('DELETE','/folders/'+folderId);
if (r?.ok) {
toast('Folder deleted','success');
S.folders = S.folders.filter(x=>x.id!==folderId);
if (S.currentFolder===folderId) selectFolder('unified','Unified Inbox');
renderFolders(); loadMessages();
} else toast(r?.error||'Delete failed','error');
}
);
}
async function moveFolderContents(fromId, toId) {
const from = S.folders.find(f=>f.id===fromId);
const to = S.folders.find(f=>f.id===toId);
if (!from||!to) return;
inlineConfirm(
`Move all messages from "${from.name}" into "${to.name}"?`,
async () => {
const r = await api('POST','/folders/'+fromId+'/move-to/'+toId);
if (r?.ok) { toast(`Moved ${r.moved||0} messages`,'success'); loadFolders(); loadMessages(); }
else toast(r?.error||'Move failed','error');
}
);
}
function updateUnreadBadge() {
const total=S.folders.filter(f=>f.folder_type==='inbox').reduce((s,f)=>s+(f.unread_count||0),0);
const badge=document.getElementById('unread-total');
badge.textContent=total; badge.style.display=total>0?'':'none';
}
// ── Labels ─────────────────────────────────────────────────────────────────
// Local to gowebmail only — not synced to Gmail/Outlook/IMAP (see server-side comment on
// models.Label for why a reliable cross-provider equivalent doesn't exist).
const LABEL_PALETTE = ['#e5484d','#f5a623','#f7ce46','#3dd68c','#12b886','#2bb3d6',
'#5b8def','#7c5cfc','#c25ee0','#ec4899','#8a8f98','#64748b'];
async function loadLabels() {
const data = await api('GET','/labels');
S.labels = data || [];
renderLabelsDropdown();
}
// Renders the Labels dropdown (panel-header, next to Filter): click a label to view its
// messages, or use the inline ✎/🗑 to rename/recolor or delete right from the list.
function renderLabelsDropdown() {
const el = document.getElementById('labels-dropdown-menu');
if (!el) return;
const rows = S.labels.map(l => `
${esc(l.name)}
`).join('');
el.innerHTML = rows + `
+ New label
`;
}
function toggleLabelsDropdown(e) {
e.stopPropagation();
const menu = document.getElementById('labels-dropdown-menu');
if (!menu) return;
const isOpen = menu.style.display !== 'none';
menu.style.display = isOpen ? 'none' : 'block';
document.getElementById('labels-dropdown-btn')?.setAttribute('aria-expanded', String(!isOpen));
if (!isOpen) setTimeout(() => document.addEventListener('click', closeLabelsDropdown, { once: true }), 0);
}
function closeLabelsDropdown() {
const menu = document.getElementById('labels-dropdown-menu');
if (menu) menu.style.display = 'none';
document.getElementById('labels-dropdown-btn')?.setAttribute('aria-expanded', 'false');
}
function toggleThreadPanel(e) {
if (e) e.stopPropagation();
const panel = document.getElementById('thread-panel');
if (!panel) return;
const isOpen = panel.style.display !== 'none';
panel.style.display = isOpen ? 'none' : 'block';
if (!isOpen) setTimeout(() => document.addEventListener('click', closeThreadPanel, { once: true }), 0);
}
function closeThreadPanel() {
const panel = document.getElementById('thread-panel');
if (panel) panel.style.display = 'none';
}
function selectLabel(labelId, name) {
closeLabelsDropdown();
selectFolder('label:'+labelId, name);
}
function openLabelEditor(labelId) {
const label = labelId ? S.labels.find(l=>l.id===labelId) : null;
document.getElementById('label-editor-title').textContent = label ? 'Edit Label' : 'New Label';
document.getElementById('label-editor-id').value = labelId || '';
document.getElementById('label-editor-name').value = label ? label.name : '';
const chosen = label ? label.color : LABEL_PALETTE[0];
document.getElementById('label-editor-swatches').innerHTML = LABEL_PALETTE.map(c =>
``
).join('');
document.getElementById('label-editor-custom-color').value = chosen;
openModal('label-editor-modal');
setTimeout(()=>document.getElementById('label-editor-name').focus(), 50);
}
function pickLabelColor(color) {
document.getElementById('label-editor-custom-color').value = color;
document.querySelectorAll('#label-editor-swatches .label-swatch').forEach(s =>
s.classList.toggle('selected', s.dataset.color.toLowerCase() === color.toLowerCase()));
}
async function saveLabelEditor() {
const id = document.getElementById('label-editor-id').value;
const name = document.getElementById('label-editor-name').value.trim();
const color = document.getElementById('label-editor-custom-color').value;
if (!name) { toast('Label name required','error'); return; }
const r = id
? await api('PUT','/labels/'+id,{name,color})
: await api('POST','/labels',{name,color});
if (r?.ok || r?.id) {
closeModal('label-editor-modal');
toast(id?'Label updated':'Label created','success');
await loadLabels();
await loadMessages(); // refresh label chips/dots on visible messages
} else toast(r?.error || 'Failed to save label','error');
}
function deleteLabelConfirm(labelId) {
const label = S.labels.find(l=>l.id===labelId);
inlineConfirm(`Delete label "${label?.name||''}"? It will be removed from all messages.`, async () => {
const r = await api('DELETE','/labels/'+labelId);
if (r?.ok) {
toast('Label deleted','success');
if (S.currentFolder === 'label:'+labelId) selectFolder('unified','Unified Inbox');
await loadLabels();
await loadMessages();
} else toast('Delete failed','error');
});
}
// Assigns/unassigns a label on a message and updates local state (list row + open detail)
// without a full reload.
async function toggleMessageLabel(msgId, labelId) {
const msg = S.messages.find(m=>m.id===msgId) || (S.currentMessage?.id===msgId ? S.currentMessage : null);
const has = msg?.labels?.some(l=>l.id===labelId);
const r = has
? await api('DELETE','/messages/'+msgId+'/labels/'+labelId)
: await api('POST','/messages/'+msgId+'/labels/'+labelId);
if (!r?.ok) { toast('Failed to update label','error'); return; }
const label = S.labels.find(l=>l.id===labelId);
[S.messages.find(m=>m.id===msgId), S.currentMessage?.id===msgId?S.currentMessage:null].forEach(m => {
if (!m) return;
m.labels = m.labels || [];
if (has) m.labels = m.labels.filter(l=>l.id!==labelId);
else if (label) m.labels.push(label);
});
renderMessageList();
if (S.currentMessage?.id===msgId) renderMessageDetail(S.currentMessage,false);
}
function toggleLabelPicker(e, msgId) {
e.stopPropagation();
const msg = S.currentMessage?.id===msgId ? S.currentMessage : S.messages.find(m=>m.id===msgId);
const items = S.labels.map(l => {
const on = msg?.labels?.some(x=>x.id===l.id);
return `
${on?'✓ ':'○ '}${esc(l.name)}
`;
}).join('') || '
No labels yet
';
showCtxMenu(e, items);
}
// ── Messages ───────────────────────────────────────────────────────────────
function selectFolder(folderId, folderName) {
S.currentFolder=folderId; S.currentFolderName=folderName||S.currentFolderName;
S.currentPage=1; S.messages=[]; S.searchQuery='';
document.getElementById('search-input').value='';
if (hasActiveSearchFilters()) clearSearchFiltersQuiet();
const sfp=document.getElementById('search-filters-panel'); if (sfp) sfp.style.display='none';
document.getElementById('panel-title').textContent=folderName||S.currentFolderName;
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
const navEl=folderId==='unified'?document.getElementById('nav-unified')
:folderId==='starred'?document.getElementById('nav-starred')
:folderId==='snoozed'?document.getElementById('nav-snoozed')
:document.getElementById('nav-f'+folderId);
if (navEl) navEl.classList.add('active');
mobCloseNav();
mobSetView('list');
loadMessages();
}
const handleSearch=debounce(q=>{
S.searchQuery=q.trim(); S.currentPage=1;
document.getElementById('panel-title').textContent=q.trim()?'Search: '+q.trim():S.currentFolderName;
loadMessages();
},350);
// ── Advanced search filters (scope, attachment, date range, size) ───────────
// Lives at the end of (position:fixed), same reasoning as #compose-dropdown —
// its old spot inside .message-list-panel got clipped/overflowed in reading-pane
// "bottom" mode (fixed-height list panel). Positioned via JS under the search bar.
function toggleSearchFilters(e) {
if (e) e.stopPropagation();
const panel = document.getElementById('search-filters-panel');
const bar = document.querySelector('.search-bar');
if (!panel || !bar) return;
const isOpen = panel.style.display !== 'none';
document.removeEventListener('click', closeSearchFiltersOutside);
if (isOpen) { panel.style.display = 'none'; return; }
populateMailboxScopeOptions();
const r = bar.getBoundingClientRect();
panel.style.display = 'block';
panel.style.width = r.width + 'px';
panel.style.left = r.left + 'px';
panel.style.top = Math.min(r.bottom + 4, window.innerHeight - 60) + 'px';
setTimeout(() => document.addEventListener('click', closeSearchFiltersOutside), 0);
}
function closeSearchFiltersOutside(e) {
const panel = document.getElementById('search-filters-panel');
const btn = document.getElementById('search-filters-btn');
if (!panel || panel.contains(e.target) || (btn && btn.contains(e.target))) return;
panel.style.display = 'none';
document.removeEventListener('click', closeSearchFiltersOutside);
}
// Offers "this account" / "this folder" only when a real folder is currently selected
// (not Unified Inbox / Starred, which don't map to a single account or folder).
function populateMailboxScopeOptions() {
const sel = document.getElementById('sf-mailbox-scope');
if (!sel) return;
const prev = sel.value;
sel.innerHTML = '';
const cur = S.folders.find(f => String(f.id) === String(S.currentFolder));
if (cur) {
const acc = S.accounts.find(a => a.id === cur.account_id);
const accLabel = acc ? (acc.display_name || acc.email_address) : 'this account';
sel.innerHTML += ``;
sel.innerHTML += ``;
}
if ([...sel.options].some(o => o.value === prev)) sel.value = prev;
}
function applySearchFilters() {
const f = S.searchFilters;
f.scope = document.getElementById('sf-scope').value;
f.hasAttachment = document.getElementById('sf-attachment').value;
f.dateFrom = document.getElementById('sf-date-from').value;
f.dateTo = document.getElementById('sf-date-to').value;
const olderDays = document.getElementById('sf-older-days').value;
if (olderDays) {
const cutoff = new Date(Date.now() - olderDays * 86400000);
f.dateTo = cutoff.toISOString().slice(0, 10);
document.getElementById('sf-date-to').value = f.dateTo;
}
f.minSizeKB = document.getElementById('sf-min-size').value;
f.maxSizeKB = document.getElementById('sf-max-size').value;
const mb = document.getElementById('sf-mailbox-scope').value; // "" | "account:" | "folder:"
f.accountId = mb.startsWith('account:') ? mb.slice(8) : '';
f.folderId = mb.startsWith('folder:') ? mb.slice(7) : '';
document.getElementById('search-filters-btn').classList.toggle('active', hasActiveSearchFilters());
S.currentPage = 1;
loadMessages();
}
// Resets filter state/UI without triggering a reload — for callers (like selectFolder)
// that are about to call loadMessages() themselves right after.
function clearSearchFiltersQuiet() {
S.searchFilters = { scope:'', hasAttachment:'', dateFrom:'', dateTo:'', minSizeKB:'', maxSizeKB:'', accountId:'', folderId:'' };
['sf-attachment','sf-date-from','sf-date-to','sf-older-days','sf-min-size','sf-max-size','sf-mailbox-scope']
.forEach(id => { const el=document.getElementById(id); if (el) el.value=''; });
const scopeEl=document.getElementById('sf-scope'); if (scopeEl) scopeEl.value='all'; // "" isn't a valid