Files
WooCow/assets/js/woocow-admin.js
Malin 3cc73dc9ec feat: rich log viewers for all remaining log types
- Syslog-style table (postfix, dovecot, sogo, netfilter, acme):
  Time | Priority badge (colour-coded debug→info→notice→warn→error→crit)
  | Process | Message — columns hidden when unused by that log type
- API access log: Time | Method badge (GET=green, POST=blue,
  PUT=orange, DELETE=red) | Endpoint | Remote IP | Data
- Autodiscover: Time | User | Service badge (ActiveSync, CalDAV,
  CardDAV, IMAP, SMTP) | User Agent
- Watchdog: Time | Service | Health badge (Healthy/Degraded/Critical
  based on lvl) | Processes (now/total) | Change (+/-/±0 coloured)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 09:52:17 +01:00

1227 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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(`<div class="notice notice-${type} is-dismissible"><p>${msg}</p></div>`);
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('<p>No servers yet. Add one above.</p>');
return;
}
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th>Name</th><th>URL</th><th>Status</th><th>Added</th><th>Actions</th>
</tr></thead><tbody>`;
rows.forEach(s => {
const badge = s.active == 1
? '<span class="woocow-badge woocow-badge-green">Active</span>'
: '<span class="woocow-badge woocow-badge-grey">Inactive</span>';
html += `<tr data-id="${s.id}" data-name="${esc(s.name)}" data-url="${esc(s.url)}">
<td><strong>${esc(s.name)}</strong></td>
<td><a href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.url)}</a></td>
<td>${badge}</td>
<td>${s.created_at.split(' ')[0]}</td>
<td class="woocow-actions">
<button class="button button-small wc-srv-edit" data-id="${s.id}">Edit</button>
<button class="button button-small wc-srv-test" data-id="${s.id}">Test</button>
<button class="button button-small wc-srv-del" data-id="${s.id}" style="color:#a00">Delete</button>
</td>
</tr>`;
});
html += '</tbody></table>';
$('#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(`<span style="color:green">✓ Connected Mailcow ${esc(res.data.version)}</span>`);
} else {
$result.html(`<span style="color:red">✗ ${esc(res.data)}</span>`);
}
});
});
// Test from row
$(document).on('click', '.wc-srv-test', function () {
const id = $(this).data('id');
const $td = $(this).closest('td');
$td.append('<span class="wc-inline-test"> Testing…</span>');
ajax('woocow_server_test', { id }).done(res => {
$td.find('.wc-inline-test').html(
res.success
? `<span style="color:green"> ✓ v${esc(res.data.version)}</span>`
: `<span style="color:red"> ✗ ${esc(res.data)}</span>`
);
});
});
// 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('<p>No assignments yet.</p>');
$('#wc-assignments-loading').hide();
return;
}
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th>Customer</th><th>Email</th><th>Domain</th><th>Server</th><th>Assigned</th><th>Actions</th>
</tr></thead><tbody>`;
rows.forEach(r => {
html += `<tr>
<td>${esc(r.display_name)}</td>
<td>${esc(r.user_email)}</td>
<td><strong>${esc(r.domain)}</strong></td>
<td>${esc(r.server_name)}</td>
<td>${r.created_at.split(' ')[0]}</td>
<td><button class="button button-small wc-assign-del" data-id="${r.id}" style="color:#a00">Remove</button></td>
</tr>`;
});
html += '</tbody></table>';
$('#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(`<option value="${s.id}">${esc(s.name)}</option>`);
});
});
// Server → load domains
$('#wc-assign-server').on('change', function () {
const sid = $(this).val();
$('#wc-assign-domain').html('<option value="">— Loading —</option>');
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('<option value="">— Select domain —</option>');
res.data.forEach(d => {
$('#wc-assign-domain').append(`<option value="${esc(d.domain)}">${esc(d.domain)}</option>`);
});
$('#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 += `<div class="woocow-ac-item" data-id="${c.id}" data-label="${esc(c.label)}">${esc(c.label)}</div>`;
});
$('#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 <strong>${esc(domain)}</strong> 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('<option value="">— Select domain —</option>');
$('#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(`<option value="${esc(d.domain)}">${esc(d.domain)}</option>`);
});
$('#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('<p>Loading…</p>');
ajax('woocow_admin_mailboxes', { server_id: currentServerId, domain: currentDomain }).done(res => {
if (!res.success) {
$('#wc-mb-table-wrap').html(`<div class="notice notice-error"><p>${esc(res.data)}</p></div>`);
return;
}
const boxes = res.data.mailboxes || [];
const webmail = res.data.webmail_url;
if (!boxes.length) {
$('#wc-mb-table-wrap').html('<p>No mailboxes found for this domain.</p>');
} else {
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th>Email</th><th>Name</th><th>Quota Used</th><th>Quota Max</th><th>Active</th><th>Actions</th>
</tr></thead><tbody>`;
boxes.forEach(m => {
const pct = m.percent_in_use || 0;
const used = formatMB(m.quota_used);
const max = formatMB(m.quota);
const bar = `<div class="woocow-quota-bar"><div style="width:${pct}%"></div></div>`;
const quotaMB = Math.round((m.quota || 0) / 1024 / 1024);
html += `<tr>
<td><a href="${esc(webmail)}" target="_blank">${esc(m.username)}</a></td>
<td>${esc(m.name)}</td>
<td>${used} ${bar}</td>
<td>${max}</td>
<td>${m.active == 1 ? '✓' : ''}</td>
<td class="woocow-actions">
<button class="button button-small woocow-icon-btn wc-mb-reset-pw"
title="Reset Password" data-email="${esc(m.username)}">
<span class="dashicons dashicons-lock"></span>
</button>
<button class="button button-small woocow-icon-btn wc-mb-set-quota"
title="Set Quota" data-email="${esc(m.username)}" data-quota="${quotaMB}">
<span class="dashicons dashicons-chart-bar"></span>
</button>
<button class="button button-small woocow-icon-btn wc-mb-del"
title="Delete" data-email="${esc(m.username)}" style="color:#a00">
<span class="dashicons dashicons-trash"></span>
</button>
</td>
</tr>`;
});
html += '</tbody></table>';
$('#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 <strong>${esc(res.data.email)}</strong> created.`);
loadMailboxes();
} else {
$('#wc-mb-modal-notice').html(`<span style="color:red">${esc(res.data)}</span>`);
}
});
});
// 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('<span style="color:red">Password cannot be empty.</span>'); return; }
if (data.password !== data.password2) { $note.html('<span style="color:red">Passwords do not match.</span>'); return; }
} else {
data.quota = $('#wc-mb-edit-quota').val();
if (!data.quota || data.quota < 1) { $note.html('<span style="color:red">Enter a valid quota.</span>'); 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', `<strong>${esc(editEmail)}</strong> — ${msg}`);
if (editType === 'quota') loadMailboxes(); // refresh to show new quota
} else {
$note.html(`<span style="color:red">${esc(res.data)}</span>`);
}
});
});
}
// ── Utilities ─────────────────────────────────────────────────────────────
function esc(str) {
return String(str).replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[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('<p>No domains on this server yet.</p>').show();
} else {
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th>Domain</th>
<th>Mailboxes</th>
<th>Quota Used</th>
<th>Active</th>
<th>Actions</th>
</tr></thead><tbody>`;
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
? '<span class="woocow-badge woocow-badge-green">Active</span>'
: '<span class="woocow-badge woocow-badge-grey">Inactive</span>';
html += `<tr>
<td><strong>${esc(d.domain)}</strong>${d.description ? `<br><small class="description">${esc(d.description)}</small>` : ''}</td>
<td>${mboxes}</td>
<td>${qUsed} / ${qTotal}</td>
<td>${active}</td>
<td class="woocow-actions">
<button class="button button-small woocow-icon-btn wc-dom-dns"
title="DNS Records" data-domain="${esc(d.domain)}">
<span class="dashicons dashicons-admin-site"></span>
</button>
<button class="button button-small woocow-icon-btn wc-dom-edit"
title="Edit" data-domain="${esc(d.domain)}">
<span class="dashicons dashicons-edit"></span>
</button>
<button class="button button-small woocow-icon-btn wc-dom-del"
title="Delete" data-domain="${esc(d.domain)}" style="color:#a00">
<span class="dashicons dashicons-trash"></span>
</button>
</td>
</tr>`;
});
html += '</tbody></table>';
$('#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('<span style="color:red">Domain name required.</span>'); return; }
ajax('woocow_admin_domain_add', data).done(res => {
if (res.success) {
$note.html('<span style="color:green">✓ Domain added with DKIM generated!</span>');
$('#wc-dom-form').slideUp();
loadDomains();
} else {
$note.html(`<span style="color:red">${esc(res.data)}</span>`);
}
});
});
// ── 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('<option value="0">— Direct delivery (no relay) —</option>');
ajax('woocow_admin_relayhosts_list', { server_id: domServerId }).done(rh => {
if (rh.success && Array.isArray(rh.data)) {
rh.data.forEach(r => {
$sel.append(`<option value="${r.id}">${esc(r.hostname)}</option>`);
});
$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 <strong>${esc(domain)}</strong> updated.`);
loadDomains();
} else {
$note.html(`<span style="color:red">${esc(res.data)}</span>`);
}
});
});
// ── 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 <strong>${esc(domain)}</strong> 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('<p>Loading…</p>');
$('#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(`<p style="color:red">${esc(res.data)}</p>`);
return;
}
const d = res.data;
let html = `<table class="wp-list-table widefat fixed striped woocow-table woocow-dns-table">
<thead><tr><th>Type</th><th>Host / Name</th><th>Value</th><th>Priority</th><th>TTL</th><th>Note</th><th></th></tr></thead><tbody>`;
d.records.forEach(r => {
html += `<tr>
<td><code>${esc(r.type)}</code></td>
<td class="woocow-dns-host"><code>${esc(r.host)}</code></td>
<td class="woocow-dns-val"><code class="woocow-dns-value">${esc(r.value)}</code></td>
<td>${esc(r.prio)}</td>
<td>${esc(r.ttl)}</td>
<td><em>${esc(r.note || '')}</em></td>
<td><button class="button button-small wc-copy-dns" data-val="${esc(r.value)}">Copy</button></td>
</tr>`;
});
html += '</tbody></table>';
if (!d.dkim_txt) {
html += `<p class="description" style="color:orange">⚠ DKIM key not yet generated for this domain. Add the domain first, then view DNS records again.</p>`;
}
$('#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('<p>No transports configured on this server.</p>');
} else {
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr><th>Hostname:Port</th><th>Username</th><th>Used by Domains</th><th>Active</th><th>Actions</th></tr></thead><tbody>`;
rows.forEach(r => {
html += `<tr>
<td><code>${esc(r.hostname)}</code></td>
<td>${esc(r.username)}</td>
<td>${esc(r.used_by_domains || '—')}</td>
<td>${r.active == 1 ? '✓' : ''}</td>
<td><button class="button button-small woocow-icon-btn wc-tr-del" title="Delete" data-id="${r.id}" style="color:#a00">
<span class="dashicons dashicons-trash"></span>
</button></td>
</tr>`;
});
html += '</tbody></table>';
$('#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('<span style="color:green">✓ Transport added.</span>'); $('#wc-tr-form').slideUp(); loadTransports(); }
else $note.html(`<span style="color:red">${esc(res.data)}</span>`);
});
});
$(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 `<span class="woocow-badge ${cls}">${label}</span>`;
};
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 = '<div class="woocow-rspamd-symbols">';
// Meta grid
const size = m.size ? ((m.size / 1024).toFixed(1) + ' KB') : '—';
const proc = m.time_real ? (m.time_real.toFixed(3) + 's') : '—';
html += `<div class="woocow-rspamd-meta">
<div class="woocow-rspamd-meta-item"><label>Sender SMTP</label>${esc(m.sender_smtp || '—')}</div>
<div class="woocow-rspamd-meta-item"><label>Sender MIME</label>${esc(m.sender_mime || '—')}</div>
<div class="woocow-rspamd-meta-item"><label>IP</label>${esc(m.ip || '—')}</div>
<div class="woocow-rspamd-meta-item"><label>Size</label>${size}</div>
<div class="woocow-rspamd-meta-item"><label>Process Time</label>${proc}</div>
<div class="woocow-rspamd-meta-item"><label>Message-ID</label><span style="word-break:break-all">${esc(m['message-id'] || '—')}</span></div>
</div>`;
// Thresholds
if (m.thresholds) {
const parts = Object.entries(m.thresholds)
.sort((a, b) => a[1] - b[1])
.map(([k, v]) => `<strong>${esc(k)}:</strong> ${v}`)
.join('&ensp;·&ensp;');
html += `<p class="woocow-rspamd-thresholds">Thresholds: ${parts}</p>`;
}
// 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 += `<table class="woocow-rspamd-sym-table">
<thead><tr>
<th style="width:24px"></th>
<th style="width:30%">Symbol</th>
<th style="width:64px">Score</th>
<th>Description</th>
<th>Options</th>
</tr></thead><tbody>`;
[...triggered, ...info].forEach(s => {
const ms = s.metric_score || 0;
const icon = ms > 0
? '<span class="dashicons dashicons-warning" style="font-size:14px;color:#c0392b"></span>'
: ms < 0
? '<span class="dashicons dashicons-yes-alt" style="font-size:14px;color:#27ae60"></span>'
: '<span class="dashicons dashicons-minus" style="font-size:14px;color:#bbb"></span>';
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 += `<tr class="${zero}">
<td>${icon}</td>
<td class="woocow-sym-name"><code>${esc(s.name)}</code></td>
<td><span class="${cls}">${sign}${ms.toFixed(3)}</span></td>
<td>${esc(s.description || '')}</td>
<td style="color:#888">${esc(opts)}</td>
</tr>`;
});
html += '</tbody></table>';
}
html += '</div>';
return html;
};
const renderRspamdLog = (entries) => {
let html = `<div class="woocow-log-toolbar">
<strong>Rspamd History</strong>
<span style="color:#666;font-size:12px">${entries.length} messages (click row for details)</span>
</div>
<table class="wp-list-table widefat fixed striped woocow-rspamd-table woocow-table">
<thead><tr>
<th style="width:130px">Time</th>
<th>From</th>
<th>To</th>
<th>Subject</th>
<th style="width:90px">Score</th>
<th style="width:110px">Action</th>
<th style="width:34px"></th>
</tr></thead><tbody>`;
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 += `<tr>
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
<td style="font-size:11px">${esc(from)}</td>
<td style="font-size:11px">${esc(to)}</td>
<td style="font-size:12px">${esc(subj)}</td>
<td><span class="woocow-score ${cls}">${scr}<span style="opacity:.6;font-weight:400">/${req}</span></span></td>
<td>${rspamdActionBadge(m.action)}</td>
<td><button class="button button-small woocow-icon-btn wc-rspamd-toggle" data-idx="${i}" title="Details">
<span class="dashicons dashicons-arrow-down-alt2"></span>
</button></td>
</tr>
<tr class="woocow-rspamd-detail" id="woocow-rspamd-${i}" style="display:none">
<td colspan="7">${rspamdDetail(m)}</td>
</tr>`;
});
html += '</tbody></table>';
$('#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 = `<div class="woocow-log-toolbar">
<strong>Rate-Limited Messages</strong>
<span style="color:#666;font-size:12px">${entries.length} entries</span>
</div>
<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th style="width:130px">Time</th>
<th>Sender</th>
<th>Recipient</th>
<th>Subject</th>
<th style="width:110px">IP</th>
<th style="width:120px">Rate Limit Rule</th>
<th style="width:110px">Queue ID</th>
</tr></thead><tbody>`;
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 += `<tr>
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
<td style="font-size:11px">${esc(sender)}</td>
<td style="font-size:11px">${esc(e.rcpt || '—')}</td>
<td style="font-size:12px">${esc(subject)}</td>
<td><code style="font-size:11px">${esc(e.ip || '—')}</code></td>
<td>
<span class="woocow-badge woocow-badge-orange" title="${esc(e.rl_info || '')}">${esc(e.rl_name || '—')}</span>
</td>
<td><code style="font-size:11px">${esc(e.qid || '—')}</code></td>
</tr>`;
});
html += '</tbody></table>';
$('#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 `<span class="woocow-badge" style="${style}">${label}</span>`;
};
const renderSyslogTable = (entries, label) => {
const hasProgram = entries.some(e => e.program);
const hasPriority = entries.some(e => e.priority);
let html = `<div class="woocow-log-toolbar">
<strong>${esc(label)}</strong>
<span style="color:#666;font-size:12px">${entries.length} entries</span>
</div>
<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th style="width:140px">Time</th>
${hasPriority ? '<th style="width:80px">Priority</th>' : ''}
${hasProgram ? '<th style="width:150px">Process</th>' : ''}
<th>Message</th>
</tr></thead><tbody>`;
entries.forEach(e => {
const dt = e.time ? new Date(parseInt(e.time) * 1000).toLocaleString() : '—';
html += `<tr>
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
${hasPriority ? `<td>${priorityBadge(e.priority)}</td>` : ''}
${hasProgram ? `<td><code style="font-size:11px">${esc(e.program || '—')}</code></td>` : ''}
<td style="font-size:12px;word-break:break-word">${esc(e.message || '—')}</td>
</tr>`;
});
html += '</tbody></table>';
$('#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 `<span style="display:inline-block;padding:1px 7px;border-radius:3px;background:${bg};color:#fff;font-size:11px;font-weight:700;font-family:monospace">${esc(method || '?')}</span>`;
};
let html = `<div class="woocow-log-toolbar">
<strong>API Access Log</strong>
<span style="color:#666;font-size:12px">${entries.length} requests</span>
</div>
<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th style="width:140px">Time</th>
<th style="width:72px">Method</th>
<th>Endpoint</th>
<th style="width:110px">Remote IP</th>
<th style="width:160px">Data</th>
</tr></thead><tbody>`;
entries.forEach(e => {
const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—';
html += `<tr>
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
<td>${methodBadge(e.method)}</td>
<td><code style="font-size:11px">${esc(e.uri || '—')}</code></td>
<td><code style="font-size:11px">${esc(e.remote || '—')}</code></td>
<td style="font-size:11px;color:#888;word-break:break-all">${esc(e.data || '—')}</td>
</tr>`;
});
html += '</tbody></table>';
$('#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 `<span class="woocow-badge ${cls}">${label}</span>`;
};
let html = `<div class="woocow-log-toolbar">
<strong>Autodiscover Log</strong>
<span style="color:#666;font-size:12px">${entries.length} requests</span>
</div>
<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th style="width:140px">Time</th>
<th>User</th>
<th style="width:120px">Service</th>
<th>User Agent</th>
</tr></thead><tbody>`;
entries.forEach(e => {
const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—';
html += `<tr>
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
<td style="font-size:12px">${esc(e.user || '—')}</td>
<td>${svcBadge(e.service)}</td>
<td style="font-size:11px;color:#666">${esc(e.ua || '—')}</td>
</tr>`;
});
html += '</tbody></table>';
$('#wc-log-wrap').html(html);
};
// ── Watchdog log renderer ─────────────────────────────────────────────
const renderWatchdogLog = (entries) => {
const healthBadge = (lvl) => {
const n = parseInt(lvl) || 0;
if (n >= 100) return '<span class="woocow-badge woocow-badge-green">Healthy</span>';
if (n >= 50) return '<span class="woocow-badge woocow-badge-orange">Degraded</span>';
return '<span class="woocow-badge woocow-badge-red">Critical</span>';
};
let html = `<div class="woocow-log-toolbar">
<strong>Watchdog Log</strong>
<span style="color:#666;font-size:12px">${entries.length} entries</span>
</div>
<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th style="width:140px">Time</th>
<th style="width:140px">Service</th>
<th style="width:90px">Status</th>
<th style="width:100px">Processes</th>
<th style="width:70px">Change</th>
</tr></thead><tbody>`;
entries.forEach(e => {
const dt = e.time ? new Date(parseInt(e.time) * 1000).toLocaleString() : '—';
const diff = parseInt(e.hpdiff) || 0;
const diffHtml = diff > 0
? `<span style="color:#27ae60;font-weight:700">+${diff}</span>`
: diff < 0
? `<span style="color:#c0392b;font-weight:700">${diff}</span>`
: `<span style="color:#bbb">±0</span>`;
html += `<tr>
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
<td><strong>${esc(e.service || '—')}</strong></td>
<td>${healthBadge(e.lvl)}</td>
<td style="font-family:monospace">${esc(e.hpnow || '0')}/${esc(e.hptotal || '0')}</td>
<td>${diffHtml}</td>
</tr>`;
});
html += '</tbody></table>';
$('#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('<p>Loading…</p>');
ajax('woocow_admin_logs', { server_id: sid, log_type: type }).done(res => {
if (!res.success) {
$('#wc-log-wrap').html(`<p style="color:red">${esc(res.data)}</p>`);
return;
}
const entries = Array.isArray(res.data) ? res.data : Object.values(res.data);
if (!entries.length) {
$('#wc-log-wrap').html('<p>No log entries.</p>');
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('<p>Loading…</p>');
ajax('woocow_admin_quarantine', { server_id: quarServerId }).done(res => {
if (!res.success) {
$('#wc-quar-wrap').html(`<div class="notice notice-error"><p>${esc(res.data)}</p></div>`);
return;
}
const msgs = Array.isArray(res.data) ? res.data : [];
if (!msgs.length) {
$('#wc-quar-wrap').html('<p>No quarantined messages.</p>');
return;
}
let html = `<table class="wp-list-table widefat fixed striped woocow-quarantine-table">
<thead><tr>
<th>Date</th><th>Sender</th><th>Recipient</th><th>Subject</th><th>Score</th><th>Actions</th>
</tr></thead><tbody>`;
msgs.forEach(m => {
const date = m.created ? new Date(m.created * 1000).toLocaleString() : '—';
const domain = (m.rcpt || '').split('@')[1] || '';
html += `<tr>
<td>${esc(date)}</td>
<td><code>${esc(m.sender)}</code></td>
<td>${esc(m.rcpt)}</td>
<td>${esc(m.subject)}</td>
<td>${esc(m.score)}</td>
<td class="woocow-actions">
<button class="button button-small woocow-icon-btn wc-quar-del"
title="Delete" data-qid="${m.id}" style="color:#a00">
<span class="dashicons dashicons-trash"></span>
</button>
<button class="button button-small woocow-icon-btn wc-quar-block"
title="Blacklist sender" data-sender="${esc(m.sender)}" data-domain="${esc(domain)}">
<span class="dashicons dashicons-shield-alt"></span>
</button>
</td>
</tr>`;
});
html += '</tbody></table>';
html += `<p class="description">${msgs.length} quarantined message(s). <em>Note: To release a message to inbox, use the link in the quarantine notification email or Webmail.</em></p>`;
$('#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 <strong>${esc(sender)}</strong> blacklisted for <strong>${esc(domain)}</strong>.`);
else notice($('#wc-quar-notices'), 'error', res.data);
});
});
}
})(jQuery);