const app = document.getElementById('app'); const state = { user: null, domains: [], selectedDomain: null, mailboxes: [], audit: [], message: '', error: '' }; async function api(path, options = {}) { const res = await fetch(path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, ...options }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || `HTTP ${res.status}`); } return res.json(); } function bytes(n) { n = Number(n || 0); if (n <= 0) return '0 B'; const u = ['B','KB','MB','GB','TB']; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`; } function percent(n) { if (n === null || n === undefined || Number.isNaN(Number(n))) return null; const value = Number(n); if (value < 1 && value > 0) return value.toFixed(1); return value.toFixed(0); } function usageInfo(m) { const used = Number(m.used_bytes || 0); const limit = m.quota_bytes === null || m.quota_bytes === undefined ? null : Number(m.quota_bytes); const calculated = limit && limit > 0 ? (used / limit) * 100 : null; const rawPct = m.quota_percent === null || m.quota_percent === undefined ? calculated : Number(m.quota_percent); const pct = rawPct === null || Number.isNaN(rawPct) ? null : Math.max(0, Math.min(100, rawPct)); return { used, limit, pct, label: limit ? `${bytes(used)} / ${bytes(limit)} (${percent(pct)}%)` : `${bytes(used)} / unlimited`, }; } function esc(s) { return String(s ?? '').replace(/[&<>'"]/g, c => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c])); } async function init() { try { state.user = await api('/api/auth/me'); await loadDomains(true); } catch { state.user = null; } render(); } async function loadDomains(resync = false) { state.domains = await api(`/api/domains${resync ? '?resync=true' : ''}`); if (!state.selectedDomain && state.domains.length) state.selectedDomain = state.domains[0].domain; if (state.selectedDomain) await loadMailboxes(true); } async function loadMailboxes(refreshQuota = false) { if (!state.selectedDomain) { state.mailboxes = []; return; } state.mailboxes = await api(`/api/mailboxes?domain=${encodeURIComponent(state.selectedDomain)}${refreshQuota ? '&refreshQuota=true' : ''}`); } async function loadAudit() { state.audit = await api('/api/audit'); renderAuditModal(); } function renderUsage(m) { const usage = usageInfo(m); const barWidth = usage.pct === null ? 0 : usage.pct; return `
Disk Usage: ${esc(usage.label)}
${m.usage_scanned_at ? `quota checked ${new Date(m.usage_scanned_at).toLocaleString()}` : 'quota not checked yet'} ${m.message_count !== null && m.message_count !== undefined ? ` · ${Number(m.message_count)} messages` : ''}
`; } function render() { if (!state.user) return renderLogin(); app.innerHTML = `
MailAdmin
${esc(state.user.email)} · ${esc(state.user.role)}
${state.error ? `
${esc(state.error)}
` : ''} ${state.message ? `
${esc(state.message)}
` : ''}

Domains on this node

Domains are discovered dynamically from DMS accounts.
${state.domains.map(d => `
${esc(d.domain)}
${d.active_mailboxes || 0} inboxes · ${bytes(d.used_bytes)}
${esc(d.current_node)} ${esc(d.status)}
`).join('') || '
No domains found yet.
'}

${esc(state.selectedDomain || 'Mailboxes')}

Create/delete mailboxes, reset passwords, edit rules. Quotas are refreshed when you open a domain.
${state.mailboxes.map(m => ` `).join('') || ''}
EmailStatusUsageUpdatedActions
${esc(m.email_address)}
${esc(m.node_name)}
${esc(m.status)} ${renderUsage(m)} ${new Date(m.updated_at).toLocaleString()}
No mailboxes for this domain.
`; document.getElementById('logoutBtn').onclick = async () => { await api('/api/auth/logout', { method:'POST' }); state.user = null; render(); }; document.getElementById('resyncBtn').onclick = guard(async () => { await api('/api/domains/resync', { method:'POST' }); await loadDomains(false); state.message = 'DMS sync completed.'; render(); }); document.getElementById('auditBtn').onclick = guard(loadAudit); document.getElementById('newMailboxBtn').onclick = renderCreateMailboxModal; document.querySelectorAll('[data-domain]').forEach(el => el.onclick = guard(async () => { state.selectedDomain = el.dataset.domain; await loadMailboxes(true); render(); })); document.querySelectorAll('[data-delete]').forEach(el => el.onclick = () => renderDeleteModal(el.dataset.delete)); document.querySelectorAll('[data-password]').forEach(el => el.onclick = () => renderPasswordModal(el.dataset.password)); document.querySelectorAll('[data-rules]').forEach(el => el.onclick = () => renderRulesModal(el.dataset.rules)); document.querySelectorAll('[data-blocks]').forEach(el => el.onclick = () => renderBlocklistModal(el.dataset.blocks)); } function renderLogin() { app.innerHTML = `

MailAdmin Login

${state.error ? `
${esc(state.error)}
` : ''}




`; document.getElementById('loginForm').onsubmit = guard(async e => { e.preventDefault(); const f = new FormData(e.target); state.user = await api('/api/auth/login', { method:'POST', body: JSON.stringify({ email: f.get('email'), password: f.get('password') }) }); await loadDomains(true); render(); }); } function modal(html) { const div = document.createElement('div'); div.className = 'modal-backdrop'; div.innerHTML = ``; document.body.appendChild(div); div.querySelector('[data-close]').onclick = () => div.remove(); div.onclick = e => { if (e.target === div) div.remove(); }; return div; } function renderCreateMailboxModal() { const domain = state.selectedDomain || ''; const d = modal(`

New mailbox

`); const emailInput = d.querySelector('#createEmail'); const passwordInput = d.querySelector('#createPassword'); const submitButton = d.querySelector('#createMailboxSubmit'); emailInput.focus(); // Cursor before the @domain, so you can directly type "test". try { emailInput.setSelectionRange(0, 0); } catch { // Some browsers do not allow selection on email inputs. } const createMailbox = guard(async () => { const email = String(emailInput.value || '').trim().toLowerCase(); const password = String(passwordInput.value || ''); if (!email || !email.includes('@')) { throw new Error('Please enter a valid email address.'); } if (!email.endsWith(`@${domain}`)) { throw new Error(`Mailbox must belong to ${domain}.`); } if (password.length < 8) { throw new Error('Password must have at least 8 characters.'); } submitButton.disabled = true; submitButton.textContent = 'Creating...'; await api('/api/mailboxes', { method: 'POST', body: JSON.stringify({ email, password, }), }); d.remove(); state.message = `Mailbox created: ${email}`; await loadDomains(false); await loadMailboxes(true); render(); }); submitButton.onclick = createMailbox; passwordInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); createMailbox(); } }); emailInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); passwordInput.focus(); } }); } function renderDeleteModal(email) { const d = modal(`

Delete mailbox

Delete ${esc(email)} from DMS?

`); d.querySelector('#confirmDelete').onclick = guard(async () => { await api(`/api/mailboxes/${encodeURIComponent(email)}`, { method:'DELETE' }); d.remove(); await loadMailboxes(true); render(); }); } function renderPasswordModal(email) { const d = modal(`

Reset password



`); d.querySelector('#pwForm').onsubmit = guard(async e => { e.preventDefault(); const f = new FormData(e.target); await api(`/api/mailboxes/${encodeURIComponent(email)}/password`, { method:'POST', body: JSON.stringify({ password:f.get('password') }) }); d.remove(); state.message = `Password updated for ${email}.`; render(); }); } async function renderRulesModal(email) { const rules = await api(`/api/mailboxes/${encodeURIComponent(email)}/rules`); const d = modal(`

Rules for ${esc(email)}







`); d.querySelector('#rulesForm').onsubmit = guard(async e => { e.preventDefault(); const f = new FormData(e.target); await api(`/api/mailboxes/${encodeURIComponent(email)}/rules`, { method:'PUT', body: JSON.stringify({ ooo_active: !!f.get('ooo_active'), ooo_message:f.get('ooo_message'), forwards:String(f.get('forwards')||'').split(/\n|,/).map(x=>x.trim()).filter(Boolean) }) }); d.remove(); }); } async function renderBlocklistModal(email) { const block = await api(`/api/mailboxes/${encodeURIComponent(email)}/blocklist`); const d = modal(`

Blocklist for ${esc(email)}



`); d.querySelector('#blockForm').onsubmit = guard(async e => { e.preventDefault(); const f = new FormData(e.target); await api(`/api/mailboxes/${encodeURIComponent(email)}/blocklist`, { method:'PUT', body: JSON.stringify({ blocked_patterns:String(f.get('blocked_patterns')||'').split('\n').map(x=>x.trim()).filter(Boolean) }) }); d.remove(); }); } function renderAuditModal() { modal(`

Audit Log

${state.audit.map(a => ``).join('')}
TimeActorActionTarget
${new Date(a.created_at).toLocaleString()}${esc(a.actor_email)}${esc(a.action)}${esc(a.target_id)}
`); } function guard(fn) { return async function(...args) { try { state.error=''; state.message=''; await fn.apply(this,args); } catch(e) { state.error = e.message; render(); } }; } init();