/**
* 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(`
`);
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 = `
| Name | URL | Status | Added | Actions |
`;
rows.forEach(s => {
const badge = s.active == 1
? 'Active'
: 'Inactive';
html += `
| ${esc(s.name)} |
${esc(s.url)} |
${badge} |
${s.created_at.split(' ')[0]} |
|
`;
});
html += '
';
$('#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 = `
| Customer | Email | Domain | Server | Assigned | Actions |
`;
rows.forEach(r => {
html += `
| ${esc(r.display_name)} |
${esc(r.user_email)} |
${esc(r.domain)} |
${esc(r.server_name)} |
${r.created_at.split(' ')[0]} |
|
`;
});
html += '
';
$('#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(``);
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 = `
| Email | Name | Quota Used | Quota Max | Active | Actions |
`;
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 += `
| ${esc(m.username)} |
${esc(m.name)} |
${used} ${bar} |
${max} |
${m.active == 1 ? '✓' : '–'} |
|
`;
});
html += '
';
$('#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 = `
| Domain |
Mailboxes |
Quota Used |
Active |
Actions |
`;
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 += `
${esc(d.domain)}${d.description ? ` ${esc(d.description)}` : ''} |
${mboxes} |
${qUsed} / ${qTotal} |
${active} |
|
`;
});
html += '
';
$('#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 = `
| Type | Host / Name | Value | Priority | TTL | Note | |
`;
d.records.forEach(r => {
html += `
${esc(r.type)} |
${esc(r.host)} |
${esc(r.value)} |
${esc(r.prio)} |
${esc(r.ttl)} |
${esc(r.note || '')} |
|
`;
});
html += '
';
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 = `
| Hostname:Port | Username | Used by Domains | Active | Actions |
`;
rows.forEach(r => {
html += `
${esc(r.hostname)} |
${esc(r.username)} |
${esc(r.used_by_domains || '—')} |
${r.active == 1 ? '✓' : '–'} |
|
`;
});
html += '
';
$('#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 += `
`;
// 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 += `
|
Symbol |
Score |
Description |
Options |
`;
[...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 += `
| ${icon} |
${esc(s.name)} |
${sign}${ms.toFixed(3)} |
${esc(s.description || '')} |
${esc(opts)} |
`;
});
html += '
';
}
html += '
';
return html;
};
const renderRspamdLog = (entries) => {
let html = `
Rspamd History
${entries.length} messages (click row for details)
| Time |
From |
To |
Subject |
Score |
Action |
|
`;
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 += `
| ${esc(dt)} |
${esc(from)} |
${esc(to)} |
${esc(subj)} |
${scr}/${req} |
${rspamdActionBadge(m.action)} |
|
| ${rspamdDetail(m)} |
`;
});
html += '
';
$('#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
| Time |
Sender |
Recipient |
Subject |
IP |
Rate Limit Rule |
Queue ID |
`;
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 += `
| ${esc(dt)} |
${esc(sender)} |
${esc(e.rcpt || '—')} |
${esc(subject)} |
${esc(e.ip || '—')} |
${esc(e.rl_name || '—')}
|
${esc(e.qid || '—')} |
`;
});
html += '
';
$('#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
| Time |
${hasPriority ? 'Priority | ' : ''}
${hasProgram ? 'Process | ' : ''}
Message |
`;
entries.forEach(e => {
const dt = e.time ? new Date(parseInt(e.time) * 1000).toLocaleString() : '—';
html += `
| ${esc(dt)} |
${hasPriority ? `${priorityBadge(e.priority)} | ` : ''}
${hasProgram ? `${esc(e.program || '—')} | ` : ''}
${esc(e.message || '—')} |
`;
});
html += '
';
$('#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
| Time |
Method |
Endpoint |
Remote IP |
Data |
`;
entries.forEach(e => {
const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—';
html += `
| ${esc(dt)} |
${methodBadge(e.method)} |
${esc(e.uri || '—')} |
${esc(e.remote || '—')} |
${esc(e.data || '—')} |
`;
});
html += '
';
$('#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
| Time |
User |
Service |
User Agent |
`;
entries.forEach(e => {
const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—';
html += `
| ${esc(dt)} |
${esc(e.user || '—')} |
${svcBadge(e.service)} |
${esc(e.ua || '—')} |
`;
});
html += '
';
$('#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
| Time |
Service |
Status |
Processes |
Change |
`;
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 += `
| ${esc(dt)} |
${esc(e.service || '—')} |
${healthBadge(e.lvl)} |
${esc(e.hpnow || '0')}/${esc(e.hptotal || '0')} |
${diffHtml} |
`;
});
html += '
';
$('#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(``);
return;
}
const msgs = Array.isArray(res.data) ? res.data : [];
if (!msgs.length) {
$('#wc-quar-wrap').html('No quarantined messages.
');
return;
}
let html = `
| Date | Sender | Recipient | Subject | Score | Actions |
`;
msgs.forEach(m => {
const date = m.created ? new Date(m.created * 1000).toLocaleString() : '—';
const domain = (m.rcpt || '').split('@')[1] || '';
html += `
| ${esc(date)} |
${esc(m.sender)} |
${esc(m.rcpt)} |
${esc(m.subject)} |
${esc(m.score)} |
|
`;
});
html += '
';
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);