/** * WooCow – Admin JavaScript */ (function ($) { 'use strict'; const ajax = (action, data) => $.post(woocow.ajax_url, { action, nonce: woocow.nonce, ...data }); const notice = ($el, type, msg, autohide = true) => { $el.html(`

${msg}

`); if (autohide) setTimeout(() => $el.find('.notice').fadeOut(), 4000); }; // ── Servers Page ────────────────────────────────────────────────────────── if ($('#wc-servers-table-wrap').length) { let editId = 0; const loadServers = () => { ajax('woocow_servers_list').done(res => { if (!res.success) return; const rows = res.data; if (!rows.length) { $('#wc-servers-table-wrap').html('

No servers yet. Add one above.

'); return; } let html = ``; rows.forEach(s => { const badge = s.active == 1 ? 'Active' : 'Inactive'; html += ``; }); html += '
NameURLStatusAddedActions
${esc(s.name)} ${esc(s.url)} ${badge} ${s.created_at.split(' ')[0]}
'; $('#wc-servers-table-wrap').html(html); $('#wc-servers-loading').hide(); }); }; loadServers(); // Show add form $('#wc-add-server').on('click', () => { editId = 0; $('#wc-server-id').val(''); $('#wc-server-name, #wc-server-url, #wc-server-key').val(''); $('#wc-server-active').prop('checked', true); $('#wc-server-form-title').text('Add Server'); $('#wc-server-form').slideDown(); }); // Edit row $(document).on('click', '.wc-srv-edit', function () { editId = $(this).data('id'); const $row = $(this).closest('tr'); $('#wc-server-id').val(editId); $('#wc-server-name').val($row.data('name')); $('#wc-server-url').val($row.data('url')); $('#wc-server-key').val(''); $('#wc-server-active').prop('checked', true); $('#wc-server-form-title').text('Edit Server'); $('#wc-server-form').slideDown(); $('html, body').animate({ scrollTop: 0 }, 300); }); // Save $('#wc-server-save').on('click', () => { const data = { id: $('#wc-server-id').val(), name: $('#wc-server-name').val().trim(), url: $('#wc-server-url').val().trim(), api_key: $('#wc-server-key').val().trim(), active: $('#wc-server-active').is(':checked') ? 1 : 0, }; if (!data.name || !data.url || (!data.api_key && !editId)) { notice($('#wc-notices'), 'error', 'Please fill in all required fields.'); return; } ajax('woocow_server_save', data).done(res => { if (res.success) { notice($('#wc-notices'), 'success', 'Server saved.'); $('#wc-server-form').slideUp(); loadServers(); } else { notice($('#wc-notices'), 'error', res.data || 'Save failed.'); } }); }); // Test connection $('#wc-server-test').on('click', () => { const $result = $('#wc-server-test-result').text('Testing…'); ajax('woocow_server_test', { id: $('#wc-server-id').val(), url: $('#wc-server-url').val().trim(), api_key: $('#wc-server-key').val().trim(), }).done(res => { if (res.success) { $result.html(`✓ Connected – Mailcow ${esc(res.data.version)}`); } else { $result.html(`✗ ${esc(res.data)}`); } }); }); // Test from row $(document).on('click', '.wc-srv-test', function () { const id = $(this).data('id'); const $td = $(this).closest('td'); $td.append(' Testing…'); ajax('woocow_server_test', { id }).done(res => { $td.find('.wc-inline-test').html( res.success ? ` ✓ v${esc(res.data.version)}` : ` ✗ ${esc(res.data)}` ); }); }); // Delete $(document).on('click', '.wc-srv-del', function () { if (!confirm('Delete this server? All domain assignments for it will also be removed.')) return; ajax('woocow_server_delete', { id: $(this).data('id') }).done(res => { if (res.success) loadServers(); else notice($('#wc-notices'), 'error', res.data); }); }); $('#wc-server-cancel').on('click', () => $('#wc-server-form').slideUp()); } // ── Assignments Page ────────────────────────────────────────────────────── if ($('#wc-assignments-table-wrap').length) { const loadAssignments = () => { ajax('woocow_assignments_list').done(res => { if (!res.success) return; const rows = res.data; if (!rows.length) { $('#wc-assignments-table-wrap').html('

No assignments yet.

'); $('#wc-assignments-loading').hide(); return; } let html = ``; rows.forEach(r => { html += ``; }); html += '
CustomerEmailDomainServerAssignedActions
${esc(r.display_name)} ${esc(r.user_email)} ${esc(r.domain)} ${esc(r.server_name)} ${r.created_at.split(' ')[0]}
'; $('#wc-assignments-table-wrap').html(html); $('#wc-assignments-loading').hide(); }); }; // Load servers into select ajax('woocow_servers_list').done(res => { if (!res.success) return; res.data.filter(s => s.active == 1).forEach(s => { $('#wc-assign-server').append(``); }); }); // Server → load domains $('#wc-assign-server').on('change', function () { const sid = $(this).val(); $('#wc-assign-domain').html(''); if (!sid) { $('#wc-domain-row').hide(); return; } ajax('woocow_server_domains', { server_id: sid }).done(res => { if (!res.success) { alert('Could not load domains: ' + res.data); return; } $('#wc-assign-domain').html(''); res.data.forEach(d => { $('#wc-assign-domain').append(``); }); $('#wc-domain-row').show(); }); }); // Customer autocomplete let searchTimer; $('#wc-cust-search').on('input', function () { clearTimeout(searchTimer); const term = $(this).val().trim(); if (term.length < 2) { $('#wc-cust-results').hide(); return; } searchTimer = setTimeout(() => { ajax('woocow_customers_search', { term }).done(res => { if (!res.success || !res.data.length) { $('#wc-cust-results').hide(); return; } let html = ''; res.data.forEach(c => { html += `
${esc(c.label)}
`; }); $('#wc-cust-results').html(html).show(); }); }, 250); }); $(document).on('click', '.woocow-ac-item', function () { $('#wc-cust-id').val($(this).data('id')); $('#wc-cust-search').val(''); $('#wc-cust-selected').text($(this).data('label')); $('#wc-cust-results').hide(); }); $(document).on('click', function (e) { if (!$(e.target).closest('#wc-cust-results, #wc-cust-search').length) { $('#wc-cust-results').hide(); } }); // Save assignment $('#wc-assign-save').on('click', () => { const customer_id = $('#wc-cust-id').val(); const server_id = $('#wc-assign-server').val(); const domain = $('#wc-assign-domain').val(); if (!customer_id || !server_id || !domain) { notice($('#wc-assign-notice'), 'error', 'Please select a customer, server, and domain.'); return; } ajax('woocow_assignment_save', { customer_id, server_id, domain }).done(res => { if (res.success) { notice($('#wc-assign-notice'), 'success', `Domain ${esc(domain)} assigned.`); $('#wc-cust-id').val(''); $('#wc-cust-selected').text(''); loadAssignments(); } else { notice($('#wc-assign-notice'), 'error', res.data); } }); }); // Delete assignment $(document).on('click', '.wc-assign-del', function () { if (!confirm('Remove this domain assignment?')) return; ajax('woocow_assignment_delete', { id: $(this).data('id') }).done(res => { if (res.success) loadAssignments(); }); }); loadAssignments(); } // ── Mailboxes Page ──────────────────────────────────────────────────────── if ($('#wc-mb-table-wrap').length) { let currentServerId = null; let currentDomain = null; // Server → load domains $('#wc-mb-server').on('change', function () { const sid = $(this).val(); $('#wc-mb-domain').hide().html(''); $('#wc-mb-load').prop('disabled', true); if (!sid) return; ajax('woocow_server_domains', { server_id: sid }).done(res => { if (!res.success) return; res.data.forEach(d => { $('#wc-mb-domain').append(``); }); $('#wc-mb-domain').show(); }); }); $('#wc-mb-domain').on('change', function () { $('#wc-mb-load').prop('disabled', !$(this).val()); }); const loadMailboxes = () => { currentServerId = $('#wc-mb-server').val(); currentDomain = $('#wc-mb-domain').val(); if (!currentServerId || !currentDomain) return; $('#wc-mb-table-wrap').html('

Loading…

'); ajax('woocow_admin_mailboxes', { server_id: currentServerId, domain: currentDomain }).done(res => { if (!res.success) { $('#wc-mb-table-wrap').html(`

${esc(res.data)}

`); return; } const boxes = res.data.mailboxes || []; const webmail = res.data.webmail_url; if (!boxes.length) { $('#wc-mb-table-wrap').html('

No mailboxes found for this domain.

'); } else { let html = ``; boxes.forEach(m => { const pct = m.percent_in_use || 0; const used = formatMB(m.quota_used); const max = formatMB(m.quota); const bar = `
`; const quotaMB = Math.round((m.quota || 0) / 1024 / 1024); html += ``; }); html += '
EmailNameQuota UsedQuota MaxActiveActions
${esc(m.username)} ${esc(m.name)} ${used} ${bar} ${max} ${m.active == 1 ? '✓' : '–'}
'; $('#wc-mb-table-wrap').html(html); } $('#wc-mb-domain-label').text(currentDomain); $('#wc-mb-create').show(); }); }; $('#wc-mb-load').on('click', loadMailboxes); // Create mailbox modal $('#wc-mb-create').on('click', () => { $('#wc-mb-local, #wc-mb-fullname, #wc-mb-pass, #wc-mb-pass2').val(''); $('#wc-mb-quota').val(1024); $('#wc-mb-modal-notice').text(''); $('#wc-mb-modal').show(); }); $('#wc-mb-modal-cancel').on('click', () => $('#wc-mb-modal').hide()); $(document).on('keydown', e => { if (e.key === 'Escape') $('#wc-mb-modal').hide(); }); $('#wc-mb-modal-save').on('click', () => { const data = { server_id: currentServerId, domain: currentDomain, local_part: $('#wc-mb-local').val().trim(), name: $('#wc-mb-fullname').val().trim(), password: $('#wc-mb-pass').val(), password2: $('#wc-mb-pass2').val(), quota: $('#wc-mb-quota').val(), }; $('#wc-mb-modal-notice').text('Creating…'); ajax('woocow_admin_mailbox_create', data).done(res => { if (res.success) { $('#wc-mb-modal').hide(); notice($('#wc-mb-notices'), 'success', `Mailbox ${esc(res.data.email)} created.`); loadMailboxes(); } else { $('#wc-mb-modal-notice').html(`${esc(res.data)}`); } }); }); // Delete mailbox $(document).on('click', '.wc-mb-del', function () { const email = $(this).data('email'); if (!confirm(`Delete mailbox ${email}? This cannot be undone.`)) return; ajax('woocow_admin_mailbox_delete', { server_id: currentServerId, email }).done(res => { if (res.success) loadMailboxes(); else notice($('#wc-mb-notices'), 'error', res.data); }); }); // ── Edit modal: Reset PW ────────────────────────────────────────────── let editEmail = ''; let editType = ''; const openEditModal = (email, type, currentQuota) => { editEmail = email; editType = type; $('#wc-mb-edit-subtitle').text(email); $('#wc-mb-edit-notice').text(''); $('#wc-mb-edit-pw-section, #wc-mb-edit-quota-section').hide(); if (type === 'password') { $('#wc-mb-edit-title').text('Reset Password'); $('#wc-mb-edit-pass, #wc-mb-edit-pass2').val(''); $('#wc-mb-edit-pw-section').show(); setTimeout(() => $('#wc-mb-edit-pass').trigger('focus'), 100); } else { $('#wc-mb-edit-title').text('Set Quota'); $('#wc-mb-edit-quota').val(currentQuota || 1024); $('#wc-mb-edit-quota-section').show(); setTimeout(() => $('#wc-mb-edit-quota').trigger('focus'), 100); } $('#wc-mb-edit-modal').show(); }; $(document).on('click', '.wc-mb-reset-pw', function () { openEditModal($(this).data('email'), 'password', null); }); $(document).on('click', '.wc-mb-set-quota', function () { openEditModal($(this).data('email'), 'quota', $(this).data('quota')); }); $('#wc-mb-edit-cancel').on('click', () => $('#wc-mb-edit-modal').hide()); $(document).on('keydown', e => { if (e.key === 'Escape') $('#wc-mb-edit-modal').hide(); }); $('#wc-mb-edit-save').on('click', () => { const $note = $('#wc-mb-edit-notice').text('Saving…'); const data = { server_id: currentServerId, email: editEmail, type: editType }; if (editType === 'password') { data.password = $('#wc-mb-edit-pass').val(); data.password2 = $('#wc-mb-edit-pass2').val(); if (!data.password) { $note.html('Password cannot be empty.'); return; } if (data.password !== data.password2) { $note.html('Passwords do not match.'); return; } } else { data.quota = $('#wc-mb-edit-quota').val(); if (!data.quota || data.quota < 1) { $note.html('Enter a valid quota.'); return; } } ajax('woocow_admin_mailbox_edit', data).done(res => { if (res.success) { $('#wc-mb-edit-modal').hide(); const msg = editType === 'password' ? 'Password updated.' : 'Quota updated.'; notice($('#wc-mb-notices'), 'success', `${esc(editEmail)} — ${msg}`); if (editType === 'quota') loadMailboxes(); // refresh to show new quota } else { $note.html(`${esc(res.data)}`); } }); }); } // ── Utilities ───────────────────────────────────────────────────────────── function esc(str) { return String(str).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]); } function formatMB(bytes) { if (bytes === 0 || bytes === '0') return '∞'; if (!bytes) return '0 MB'; const mb = bytes / 1024 / 1024; return mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB'; } // ── Domains Page ────────────────────────────────────────────────────────── if ($('#wc-dom-server').length) { let domServerId = null; let domServerUrl = null; let domainsData = {}; // domain name → full data object $('#wc-dom-server').on('change', function () { domServerId = $(this).val(); domServerUrl = $(this).find('option:selected').data('url'); $('#wc-dom-load').prop('disabled', !domServerId); $('#wc-dom-add-btn, #wc-dom-table-wrap').hide(); domainsData = {}; }); const loadDomains = () => { ajax('woocow_server_domains', { server_id: domServerId }).done(res => { if (!res.success) { notice($('#wc-dom-notices'), 'error', res.data); return; } const domains = res.data; domainsData = {}; domains.forEach(d => { domainsData[d.domain] = d; }); if (!domains.length) { $('#wc-dom-table-wrap').html('

No domains on this server yet.

').show(); } else { let html = ``; domains.forEach(d => { const mboxes = `${d.mboxes_in}/${d.mailboxes || '∞'}`; const qUsed = formatMB(d.quota_used * 1024 * 1024); const qTotal = d.quota ? formatMB(d.quota * 1024 * 1024) : '∞'; const active = d.active == 1 ? 'Active' : 'Inactive'; html += ``; }); html += '
Domain Mailboxes Quota Used Active Actions
${esc(d.domain)}${d.description ? `
${esc(d.description)}` : ''}
${mboxes} ${qUsed} / ${qTotal} ${active}
'; $('#wc-dom-table-wrap').html(html).show(); } $('#wc-dom-add-btn').show(); }); }; $('#wc-dom-load').on('click', loadDomains); $('#wc-dom-add-btn').on('click', () => { $('#wc-dom-name, #wc-dom-desc').val(''); $('#wc-dom-form-notice').text(''); $('#wc-dom-form').slideDown(); }); $('#wc-dom-cancel').on('click', () => $('#wc-dom-form').slideUp()); $('#wc-dom-save').on('click', () => { const $note = $('#wc-dom-form-notice').text('Adding domain…'); const data = { server_id: domServerId, domain: $('#wc-dom-name').val().trim(), description: $('#wc-dom-desc').val().trim(), mailboxes: $('#wc-dom-mailboxes').val(), aliases: $('#wc-dom-aliases').val(), quota: $('#wc-dom-quota').val(), defquota: $('#wc-dom-defquota').val(), dkim_size: $('#wc-dom-dkim-size').val(), }; if (!data.domain) { $note.html('Domain name required.'); return; } ajax('woocow_admin_domain_add', data).done(res => { if (res.success) { $note.html('✓ Domain added with DKIM generated!'); $('#wc-dom-form').slideUp(); loadDomains(); } else { $note.html(`${esc(res.data)}`); } }); }); // ── Edit Domain modal ───────────────────────────────────────────────── $(document).on('click', '.wc-dom-edit', function () { const domain = $(this).data('domain'); const d = domainsData[domain]; if (!d) return; $('#wc-dom-edit-domain').val(domain); $('#wc-dom-edit-name').text(domain); $('#wc-dom-edit-desc').val(d.description || ''); $('#wc-dom-edit-mboxes').val(d.mailboxes || 10); $('#wc-dom-edit-aliases').val(d.aliases || 400); $('#wc-dom-edit-quota').val(d.quota || 10240); $('#wc-dom-edit-defquota').val(d.defquota || 3072); $('#wc-dom-edit-rl-value').val(d.rl_value || 0); $('#wc-dom-edit-rl-frame').val(d.rl_frame || 's'); $('#wc-dom-edit-active').prop('checked', d.active == 1); $('#wc-dom-edit-notice').text(''); // Load relayhosts into transport dropdown const $sel = $('#wc-dom-edit-relayhost').html(''); ajax('woocow_admin_relayhosts_list', { server_id: domServerId }).done(rh => { if (rh.success && Array.isArray(rh.data)) { rh.data.forEach(r => { $sel.append(``); }); $sel.val(d.relayhost || '0'); } }); $('#wc-dom-edit-modal').show(); }); $('#wc-dom-edit-cancel').on('click', () => $('#wc-dom-edit-modal').hide()); $('#wc-dom-edit-save').on('click', () => { const $note = $('#wc-dom-edit-notice').text('Saving…'); const domain = $('#wc-dom-edit-domain').val(); ajax('woocow_admin_domain_edit', { server_id: domServerId, domain, description: $('#wc-dom-edit-desc').val(), mailboxes: $('#wc-dom-edit-mboxes').val(), aliases: $('#wc-dom-edit-aliases').val(), quota: $('#wc-dom-edit-quota').val(), defquota: $('#wc-dom-edit-defquota').val(), rl_value: $('#wc-dom-edit-rl-value').val(), rl_frame: $('#wc-dom-edit-rl-frame').val(), relayhost: $('#wc-dom-edit-relayhost').val(), active: $('#wc-dom-edit-active').is(':checked') ? 1 : 0, }).done(res => { if (res.success) { $('#wc-dom-edit-modal').hide(); notice($('#wc-dom-notices'), 'success', `Domain ${esc(domain)} updated.`); loadDomains(); } else { $note.html(`${esc(res.data)}`); } }); }); // ── Delete Domain ───────────────────────────────────────────────────── $(document).on('click', '.wc-dom-del', function () { const domain = $(this).data('domain'); if (!confirm(`Delete domain ${domain}? This will also delete all mailboxes, aliases, and data on the Mailcow server. This cannot be undone!`)) return; ajax('woocow_admin_domain_delete', { server_id: domServerId, domain }).done(res => { if (res.success) { notice($('#wc-dom-notices'), 'success', `Domain ${esc(domain)} deleted.`); loadDomains(); } else { notice($('#wc-dom-notices'), 'error', res.data); } }); }); // DNS Records panel $(document).on('click', '.wc-dom-dns', function () { const domain = $(this).data('domain'); $('#wc-dom-dns-domain').text(domain); $('#wc-dom-dns-content').html('

Loading…

'); $('#wc-dom-dns-panel').show(); $('html,body').animate({ scrollTop: $('#wc-dom-dns-panel').offset().top - 40 }, 300); ajax('woocow_admin_domain_dns', { server_id: domServerId, domain }).done(res => { if (!res.success) { $('#wc-dom-dns-content').html(`

${esc(res.data)}

`); return; } const d = res.data; let html = ``; d.records.forEach(r => { html += ``; }); html += '
TypeHost / NameValuePriorityTTLNote
${esc(r.type)} ${esc(r.host)} ${esc(r.value)} ${esc(r.prio)} ${esc(r.ttl)} ${esc(r.note || '')}
'; if (!d.dkim_txt) { html += `

⚠ DKIM key not yet generated for this domain. Add the domain first, then view DNS records again.

`; } $('#wc-dom-dns-content').html(html); }); }); $(document).on('click', '.wc-copy-dns', function () { const val = $(this).data('val'); navigator.clipboard.writeText(val).then(() => { $(this).text('Copied!'); setTimeout(() => $(this).text('Copy'), 1500); }); }); $('#wc-dom-dns-close').on('click', () => $('#wc-dom-dns-panel').hide()); } // ── Transports Page ─────────────────────────────────────────────────────── if ($('#wc-tr-server').length) { let trServerId = null; $('#wc-tr-server').on('change', function () { trServerId = $(this).val(); $('#wc-tr-load').prop('disabled', !trServerId); }); const loadTransports = () => { ajax('woocow_admin_relayhosts_list', { server_id: trServerId }).done(res => { if (!res.success) { notice($('#wc-tr-notices'), 'error', res.data); return; } const rows = res.data; if (!Array.isArray(rows) || !rows.length) { $('#wc-tr-table-wrap').html('

No transports configured on this server.

'); } else { let html = ``; rows.forEach(r => { html += ``; }); html += '
Hostname:PortUsernameUsed by DomainsActiveActions
${esc(r.hostname)} ${esc(r.username)} ${esc(r.used_by_domains || '—')} ${r.active == 1 ? '✓' : '–'}
'; $('#wc-tr-table-wrap').html(html); } $('#wc-tr-add-btn').show(); }); }; $('#wc-tr-load').on('click', loadTransports); $('#wc-tr-add-btn').on('click', () => { $('#wc-tr-hostname,#wc-tr-user,#wc-tr-pass').val(''); $('#wc-tr-form').slideDown(); }); $('#wc-tr-cancel').on('click', () => $('#wc-tr-form').slideUp()); $('#wc-tr-save').on('click', () => { const $note = $('#wc-tr-form-notice').text('Saving…'); ajax('woocow_admin_relayhost_save', { server_id: trServerId, hostname: $('#wc-tr-hostname').val().trim(), username: $('#wc-tr-user').val().trim(), password: $('#wc-tr-pass').val(), active: $('#wc-tr-active').is(':checked') ? 1 : 0, }).done(res => { if (res.success) { $note.html('✓ Transport added.'); $('#wc-tr-form').slideUp(); loadTransports(); } else $note.html(`${esc(res.data)}`); }); }); $(document).on('click', '.wc-tr-del', function () { if (!confirm('Delete this transport?')) return; ajax('woocow_admin_relayhost_delete', { server_id: trServerId, id: $(this).data('id') }).done(res => { if (res.success) loadTransports(); else notice($('#wc-tr-notices'), 'error', res.data); }); }); } // ── Logs Page ───────────────────────────────────────────────────────────── if ($('#wc-log-server').length) { $('#wc-log-server').on('change', function () { $('#wc-log-load').prop('disabled', !$(this).val()); }); // ── Rspamd history renderer ─────────────────────────────────────────── const rspamdActionBadge = (action) => { const map = { 'no action': ['woocow-badge-green', 'No Action'], 'add header': ['woocow-badge-blue', 'Add Header'], 'rewrite subject': ['woocow-badge-orange', 'Rewrite Subject'], 'greylist': ['woocow-badge-orange', 'Greylist'], 'soft reject': ['woocow-badge-orange', 'Soft Reject'], 'reject': ['woocow-badge-red', 'Reject'], }; const [cls, label] = map[(action || '').toLowerCase()] || ['woocow-badge-grey', esc(action || '—')]; return `${label}`; }; const rspamdScoreClass = (score, req) => { const pct = (score || 0) / (req || 25); if (pct < 0.3) return 'woocow-score-clean'; if (pct < 0.7) return 'woocow-score-low'; return 'woocow-score-high'; }; const rspamdDetail = (m) => { let html = '
'; // Meta grid const size = m.size ? ((m.size / 1024).toFixed(1) + ' KB') : '—'; const proc = m.time_real ? (m.time_real.toFixed(3) + 's') : '—'; html += `
${esc(m.sender_smtp || '—')}
${esc(m.sender_mime || '—')}
${esc(m.ip || '—')}
${size}
${proc}
${esc(m['message-id'] || '—')}
`; // Thresholds if (m.thresholds) { const parts = Object.entries(m.thresholds) .sort((a, b) => a[1] - b[1]) .map(([k, v]) => `${esc(k)}: ${v}`) .join(' · '); html += `

Thresholds: ${parts}

`; } // Symbols table if (m.symbols) { const syms = Object.values(m.symbols); const triggered = syms.filter(s => (s.metric_score || 0) !== 0) .sort((a, b) => Math.abs(b.metric_score) - Math.abs(a.metric_score)); const info = syms.filter(s => (s.metric_score || 0) === 0) .sort((a, b) => (a.name || '').localeCompare(b.name || '')); html += ``; [...triggered, ...info].forEach(s => { const ms = s.metric_score || 0; const icon = ms > 0 ? '' : ms < 0 ? '' : ''; const cls = ms > 0 ? 'woocow-sym-score-pos' : ms < 0 ? 'woocow-sym-score-neg' : 'woocow-sym-score-zero'; const sign = ms > 0 ? '+' : ''; const opts = (s.options || []).join(', '); const zero = ms === 0 ? ' sym-zero' : ''; html += ``; }); html += '
Symbol Score Description Options
${icon} ${esc(s.name)} ${sign}${ms.toFixed(3)} ${esc(s.description || '')} ${esc(opts)}
'; } html += '
'; return html; }; const renderRspamdLog = (entries) => { let html = `
Rspamd History ${entries.length} messages (click row for details)
`; entries.forEach((m, i) => { const dt = m.unix_time ? new Date(m.unix_time * 1000).toLocaleString() : '—'; const from = m.sender_mime || m.sender_smtp || '—'; const to = (m.rcpt_mime || m.rcpt_smtp || []).join(', ') || '—'; const subj = m.subject || '—'; const req = m.required_score || 25; const scr = (m.score || 0).toFixed(2); const cls = rspamdScoreClass(m.score, req); html += ``; }); html += '
Time From To Subject Score Action
${esc(dt)} ${esc(from)} ${esc(to)} ${esc(subj)} ${scr}/${req} ${rspamdActionBadge(m.action)}
'; $('#wc-log-wrap').html(html); }; $(document).on('click', '.wc-rspamd-toggle', function () { const i = $(this).data('idx'); const $det = $(`#woocow-rspamd-${i}`); const $ico = $(this).find('.dashicons'); if ($det.is(':visible')) { $det.hide(); $ico.removeClass('dashicons-arrow-up-alt2').addClass('dashicons-arrow-down-alt2'); } else { $det.show(); $ico.removeClass('dashicons-arrow-down-alt2').addClass('dashicons-arrow-up-alt2'); } }); // ── Ratelimited log renderer ────────────────────────────────────────── const renderRatelimitLog = (entries) => { let html = `
Rate-Limited Messages ${entries.length} entries
`; entries.forEach(e => { const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—'; const sender = e.header_from || e.from || '—'; const subject = e.header_subject || '—'; html += ``; }); html += '
Time Sender Recipient Subject IP Rate Limit Rule Queue ID
${esc(dt)} ${esc(sender)} ${esc(e.rcpt || '—')} ${esc(subject)} ${esc(e.ip || '—')} ${esc(e.rl_name || '—')} ${esc(e.qid || '—')}
'; $('#wc-log-wrap').html(html); }; // ── Syslog-style renderer (postfix, dovecot, sogo, netfilter, acme) ── const priorityBadge = (priority) => { const p = (priority || 'info').toLowerCase(); const styles = { debug: 'background:#f0f0f0;color:#888', info: 'background:#e9ecef;color:#495057', notice: 'background:#cce5ff;color:#004085', warn: 'background:#fff3cd;color:#856404', warning: 'background:#fff3cd;color:#856404', err: 'background:#f8d7da;color:#721c24', error: 'background:#f8d7da;color:#721c24', crit: 'background:#721c24;color:#fff', critical: 'background:#721c24;color:#fff', alert: 'background:#721c24;color:#fff', emerg: 'background:#1a1a1a;color:#fff', }; const style = styles[p] || 'background:#e9ecef;color:#495057'; const label = p.charAt(0).toUpperCase() + p.slice(1); return `${label}`; }; const renderSyslogTable = (entries, label) => { const hasProgram = entries.some(e => e.program); const hasPriority = entries.some(e => e.priority); let html = `
${esc(label)} ${entries.length} entries
${hasPriority ? '' : ''} ${hasProgram ? '' : ''} `; entries.forEach(e => { const dt = e.time ? new Date(parseInt(e.time) * 1000).toLocaleString() : '—'; html += ` ${hasPriority ? `` : ''} ${hasProgram ? `` : ''} `; }); html += '
TimePriorityProcessMessage
${esc(dt)}${priorityBadge(e.priority)}${esc(e.program || '—')}${esc(e.message || '—')}
'; $('#wc-log-wrap').html(html); }; // ── API access log renderer ─────────────────────────────────────────── const renderApiLog = (entries) => { const methodBadge = (method) => { const colors = { GET:'#27ae60', POST:'#2271b1', PUT:'#e67e22', DELETE:'#c0392b', PATCH:'#8e44ad' }; const bg = colors[(method || '').toUpperCase()] || '#555'; return `${esc(method || '?')}`; }; let html = `
API Access Log ${entries.length} requests
`; entries.forEach(e => { const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—'; html += ``; }); html += '
Time Method Endpoint Remote IP Data
${esc(dt)} ${methodBadge(e.method)} ${esc(e.uri || '—')} ${esc(e.remote || '—')} ${esc(e.data || '—')}
'; $('#wc-log-wrap').html(html); }; // ── Autodiscover log renderer ───────────────────────────────────────── const renderAutodiscoverLog = (entries) => { const svcBadge = (svc) => { const map = { activesync: ['woocow-badge-blue', 'ActiveSync'], caldav: ['woocow-badge-green', 'CalDAV'], carddav: ['woocow-badge-green', 'CardDAV'], imap: ['woocow-badge-grey', 'IMAP'], smtp: ['woocow-badge-grey', 'SMTP'], }; const [cls, label] = map[(svc || '').toLowerCase()] || ['woocow-badge-grey', esc(svc || '—')]; return `${label}`; }; let html = `
Autodiscover Log ${entries.length} requests
`; entries.forEach(e => { const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—'; html += ``; }); html += '
Time User Service User Agent
${esc(dt)} ${esc(e.user || '—')} ${svcBadge(e.service)} ${esc(e.ua || '—')}
'; $('#wc-log-wrap').html(html); }; // ── Watchdog log renderer ───────────────────────────────────────────── const renderWatchdogLog = (entries) => { const healthBadge = (lvl) => { const n = parseInt(lvl) || 0; if (n >= 100) return 'Healthy'; if (n >= 50) return 'Degraded'; return 'Critical'; }; let html = `
Watchdog Log ${entries.length} entries
`; entries.forEach(e => { const dt = e.time ? new Date(parseInt(e.time) * 1000).toLocaleString() : '—'; const diff = parseInt(e.hpdiff) || 0; const diffHtml = diff > 0 ? `+${diff}` : diff < 0 ? `${diff}` : `±0`; html += ``; }); html += '
Time Service Status Processes Change
${esc(dt)} ${esc(e.service || '—')} ${healthBadge(e.lvl)} ${esc(e.hpnow || '0')}/${esc(e.hptotal || '0')} ${diffHtml}
'; $('#wc-log-wrap').html(html); }; // ── Log load ────────────────────────────────────────────────────────── $('#wc-log-load').on('click', () => { const sid = $('#wc-log-server').val(); const type = $('#wc-log-type').val(); $('#wc-log-wrap').html('

Loading…

'); ajax('woocow_admin_logs', { server_id: sid, log_type: type }).done(res => { if (!res.success) { $('#wc-log-wrap').html(`

${esc(res.data)}

`); return; } const entries = Array.isArray(res.data) ? res.data : Object.values(res.data); if (!entries.length) { $('#wc-log-wrap').html('

No log entries.

'); return; } const typeLabel = $('#wc-log-type option:selected').text(); if (type === 'rspamd-history') { renderRspamdLog(entries); return; } if (type === 'ratelimited') { renderRatelimitLog(entries); return; } if (type === 'api') { renderApiLog(entries); return; } if (type === 'autodiscover') { renderAutodiscoverLog(entries); return; } if (type === 'watchdog') { renderWatchdogLog(entries); return; } // postfix, dovecot, sogo, netfilter, acme → syslog table renderSyslogTable(entries, typeLabel + ' Log'); }); }); } // ── Admin Quarantine Page ───────────────────────────────────────────────── if ($('#wc-quar-server').length) { let quarServerId = null; $('#wc-quar-server').on('change', function () { quarServerId = $(this).val(); $('#wc-quar-load').prop('disabled', !quarServerId); }); const loadQuarantine = () => { $('#wc-quar-wrap').html('

Loading…

'); ajax('woocow_admin_quarantine', { server_id: quarServerId }).done(res => { if (!res.success) { $('#wc-quar-wrap').html(`

${esc(res.data)}

`); return; } const msgs = Array.isArray(res.data) ? res.data : []; if (!msgs.length) { $('#wc-quar-wrap').html('

No quarantined messages.

'); return; } let html = ``; msgs.forEach(m => { const date = m.created ? new Date(m.created * 1000).toLocaleString() : '—'; const domain = (m.rcpt || '').split('@')[1] || ''; html += ``; }); html += '
DateSenderRecipientSubjectScoreActions
${esc(date)} ${esc(m.sender)} ${esc(m.rcpt)} ${esc(m.subject)} ${esc(m.score)}
'; html += `

${msgs.length} quarantined message(s). Note: To release a message to inbox, use the link in the quarantine notification email or Webmail.

`; $('#wc-quar-wrap').html(html); }); }; $('#wc-quar-load').on('click', loadQuarantine); $(document).on('click', '.wc-quar-del', function () { if (!confirm('Permanently delete this quarantined message?')) return; const qid = $(this).data('qid'); ajax('woocow_admin_quarantine_delete', { server_id: quarServerId, qid }).done(res => { if (res.success) loadQuarantine(); else notice($('#wc-quar-notices'), 'error', res.data); }); }); $(document).on('click', '.wc-quar-block', function () { const sender = $(this).data('sender'); const domain = $(this).data('domain'); if (!domain) { notice($('#wc-quar-notices'), 'error', 'Could not determine recipient domain.'); return; } if (!confirm(`Add ${sender} to the blacklist for domain ${domain}?`)) return; ajax('woocow_admin_quarantine_block', { server_id: quarServerId, domain, object_from: sender, }).done(res => { if (res.success) notice($('#wc-quar-notices'), 'success', `Sender ${esc(sender)} blacklisted for ${esc(domain)}.`); else notice($('#wc-quar-notices'), 'error', res.data); }); }); } })(jQuery);