feat: domain edit/delete, fix logs, add admin quarantine with block
- Add ajax_woocow_admin_domain_edit and _delete PHP handlers
- Domain table: richer columns (mailboxes used/limit, quota), icon buttons
- Edit domain modal: pre-populates fields, loads relayhosts for transport select
- Fix logs: correct Mailcow API slugs (rspamd-history, ratelimited) and add /{count} suffix to endpoint
- Add admin Quarantine submenu: view all quarantined messages, delete, blacklist sender via domain policy
- Add domain policy methods to API class (add/delete/get_bl)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -476,12 +476,14 @@
|
|||||||
if ($('#wc-dom-server').length) {
|
if ($('#wc-dom-server').length) {
|
||||||
let domServerId = null;
|
let domServerId = null;
|
||||||
let domServerUrl = null;
|
let domServerUrl = null;
|
||||||
|
let domainsData = {}; // domain name → full data object
|
||||||
|
|
||||||
$('#wc-dom-server').on('change', function () {
|
$('#wc-dom-server').on('change', function () {
|
||||||
domServerId = $(this).val();
|
domServerId = $(this).val();
|
||||||
domServerUrl = $(this).find('option:selected').data('url');
|
domServerUrl = $(this).find('option:selected').data('url');
|
||||||
$('#wc-dom-load').prop('disabled', !domServerId);
|
$('#wc-dom-load').prop('disabled', !domServerId);
|
||||||
$('#wc-dom-add-btn, #wc-dom-table-wrap').hide();
|
$('#wc-dom-add-btn, #wc-dom-table-wrap').hide();
|
||||||
|
domainsData = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadDomains = () => {
|
const loadDomains = () => {
|
||||||
@@ -491,18 +493,44 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const domains = res.data;
|
const domains = res.data;
|
||||||
|
domainsData = {};
|
||||||
|
domains.forEach(d => { domainsData[d.domain] = d; });
|
||||||
|
|
||||||
if (!domains.length) {
|
if (!domains.length) {
|
||||||
$('#wc-dom-table-wrap').html('<p>No domains on this server yet.</p>').show();
|
$('#wc-dom-table-wrap').html('<p>No domains on this server yet.</p>').show();
|
||||||
} else {
|
} else {
|
||||||
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
|
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
|
||||||
<thead><tr><th>Domain</th><th>Active</th><th>Actions</th></tr></thead><tbody>`;
|
<thead><tr>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Mailboxes</th>
|
||||||
|
<th>Quota Used</th>
|
||||||
|
<th>Active</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr></thead><tbody>`;
|
||||||
domains.forEach(d => {
|
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>
|
html += `<tr>
|
||||||
<td><strong>${esc(d.domain)}</strong></td>
|
<td><strong>${esc(d.domain)}</strong>${d.description ? `<br><small class="description">${esc(d.description)}</small>` : ''}</td>
|
||||||
<td>${d.active == 1 ? '<span class="woocow-badge woocow-badge-green">Active</span>' : '<span class="woocow-badge woocow-badge-grey">Inactive</span>'}</td>
|
<td>${mboxes}</td>
|
||||||
<td>
|
<td>${qUsed} / ${qTotal}</td>
|
||||||
<button class="button button-small wc-dom-dns" data-domain="${esc(d.domain)}">
|
<td>${active}</td>
|
||||||
<span class="dashicons dashicons-admin-site"></span> DNS Records
|
<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>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
@@ -548,6 +576,82 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── 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
|
// DNS Records panel
|
||||||
$(document).on('click', '.wc-dom-dns', function () {
|
$(document).on('click', '.wc-dom-dns', function () {
|
||||||
const domain = $(this).data('domain');
|
const domain = $(this).data('domain');
|
||||||
@@ -697,4 +801,84 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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);
|
})(jQuery);
|
||||||
|
|||||||
@@ -26,10 +26,15 @@ class WooCow_Admin {
|
|||||||
'woocow_admin_mailbox_edit',
|
'woocow_admin_mailbox_edit',
|
||||||
'woocow_admin_domain_add',
|
'woocow_admin_domain_add',
|
||||||
'woocow_admin_domain_dns',
|
'woocow_admin_domain_dns',
|
||||||
|
'woocow_admin_domain_edit',
|
||||||
|
'woocow_admin_domain_delete',
|
||||||
'woocow_admin_relayhosts_list',
|
'woocow_admin_relayhosts_list',
|
||||||
'woocow_admin_relayhost_save',
|
'woocow_admin_relayhost_save',
|
||||||
'woocow_admin_relayhost_delete',
|
'woocow_admin_relayhost_delete',
|
||||||
'woocow_admin_logs',
|
'woocow_admin_logs',
|
||||||
|
'woocow_admin_quarantine',
|
||||||
|
'woocow_admin_quarantine_delete',
|
||||||
|
'woocow_admin_quarantine_block',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ( $ajax_actions as $action ) {
|
foreach ( $ajax_actions as $action ) {
|
||||||
@@ -55,7 +60,8 @@ class WooCow_Admin {
|
|||||||
add_submenu_page( 'woocow', 'Mailboxes', 'Mailboxes', 'manage_woocommerce', 'woocow-mailboxes', [ $this, 'page_mailboxes' ] );
|
add_submenu_page( 'woocow', 'Mailboxes', 'Mailboxes', 'manage_woocommerce', 'woocow-mailboxes', [ $this, 'page_mailboxes' ] );
|
||||||
add_submenu_page( 'woocow', 'Domains', 'Domains', 'manage_woocommerce', 'woocow-domains', [ $this, 'page_domains' ] );
|
add_submenu_page( 'woocow', 'Domains', 'Domains', 'manage_woocommerce', 'woocow-domains', [ $this, 'page_domains' ] );
|
||||||
add_submenu_page( 'woocow', 'Transports','Transports', 'manage_woocommerce', 'woocow-transports', [ $this, 'page_transports' ] );
|
add_submenu_page( 'woocow', 'Transports','Transports', 'manage_woocommerce', 'woocow-transports', [ $this, 'page_transports' ] );
|
||||||
add_submenu_page( 'woocow', 'Logs', 'Logs', 'manage_woocommerce', 'woocow-logs', [ $this, 'page_logs' ] );
|
add_submenu_page( 'woocow', 'Logs', 'Logs', 'manage_woocommerce', 'woocow-logs', [ $this, 'page_logs' ] );
|
||||||
|
add_submenu_page( 'woocow', 'Quarantine', 'Quarantine', 'manage_woocommerce', 'woocow-quarantine', [ $this, 'page_quarantine' ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue_assets( string $hook ): void {
|
public function enqueue_assets( string $hook ): void {
|
||||||
@@ -385,6 +391,71 @@ class WooCow_Admin {
|
|||||||
<!-- Domain list -->
|
<!-- Domain list -->
|
||||||
<div id="wc-dom-table-wrap"></div>
|
<div id="wc-dom-table-wrap"></div>
|
||||||
|
|
||||||
|
<!-- Edit Domain Modal -->
|
||||||
|
<div id="wc-dom-edit-modal" class="woocow-modal" style="display:none">
|
||||||
|
<div class="woocow-modal-box" style="max-width:600px">
|
||||||
|
<h3>Edit Domain: <span id="wc-dom-edit-name"></span></h3>
|
||||||
|
<input type="hidden" id="wc-dom-edit-domain">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th><label for="wc-dom-edit-desc">Description</label></th>
|
||||||
|
<td><input type="text" id="wc-dom-edit-desc" class="regular-text"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="wc-dom-edit-mboxes">Max Mailboxes</label></th>
|
||||||
|
<td><input type="number" id="wc-dom-edit-mboxes" class="small-text" min="1"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="wc-dom-edit-aliases">Max Aliases</label></th>
|
||||||
|
<td><input type="number" id="wc-dom-edit-aliases" class="small-text" min="0"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="wc-dom-edit-quota">Total Quota (MB)</label></th>
|
||||||
|
<td><input type="number" id="wc-dom-edit-quota" class="small-text" min="1">
|
||||||
|
<p class="description">Sum of all mailbox quotas for this domain.</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="wc-dom-edit-defquota">Default Mailbox Quota (MB)</label></th>
|
||||||
|
<td><input type="number" id="wc-dom-edit-defquota" class="small-text" min="1"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label>Rate Limit</label></th>
|
||||||
|
<td>
|
||||||
|
<div class="woocow-flex-inline">
|
||||||
|
<input type="number" id="wc-dom-edit-rl-value" class="small-text" min="0" placeholder="0 = off">
|
||||||
|
<span>msgs per</span>
|
||||||
|
<select id="wc-dom-edit-rl-frame">
|
||||||
|
<option value="s">Second</option>
|
||||||
|
<option value="m">Minute</option>
|
||||||
|
<option value="h">Hour</option>
|
||||||
|
<option value="d">Day</option>
|
||||||
|
</select>
|
||||||
|
<span class="description">(0 = disabled)</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="wc-dom-edit-relayhost">Outbound Transport</label></th>
|
||||||
|
<td>
|
||||||
|
<select id="wc-dom-edit-relayhost" class="regular-text">
|
||||||
|
<option value="0">— Direct delivery (no relay) —</option>
|
||||||
|
</select>
|
||||||
|
<p class="description">Route outbound mail for this domain through a sender-dependent transport.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="wc-dom-edit-active">Active</label></th>
|
||||||
|
<td><input type="checkbox" id="wc-dom-edit-active"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="woocow-modal-actions">
|
||||||
|
<button class="button button-primary" id="wc-dom-edit-save">Save Changes</button>
|
||||||
|
<button class="button" id="wc-dom-edit-cancel">Cancel</button>
|
||||||
|
<span id="wc-dom-edit-notice"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- DNS Records panel -->
|
<!-- DNS Records panel -->
|
||||||
<div id="wc-dom-dns-panel" class="woocow-card" style="display:none">
|
<div id="wc-dom-dns-panel" class="woocow-card" style="display:none">
|
||||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
||||||
@@ -458,7 +529,18 @@ class WooCow_Admin {
|
|||||||
public function page_logs(): void {
|
public function page_logs(): void {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||||
$log_types = [ 'postfix', 'dovecot', 'rspamd', 'ratelimit', 'api', 'acme', 'autodiscover', 'sogo', 'netfilter', 'watchdog' ];
|
$log_types = [
|
||||||
|
'postfix' => 'Postfix',
|
||||||
|
'dovecot' => 'Dovecot',
|
||||||
|
'rspamd-history' => 'Rspamd',
|
||||||
|
'ratelimited' => 'Rate Limit',
|
||||||
|
'api' => 'API',
|
||||||
|
'acme' => 'ACME',
|
||||||
|
'autodiscover' => 'Autodiscover',
|
||||||
|
'sogo' => 'SOGo',
|
||||||
|
'netfilter' => 'Netfilter',
|
||||||
|
'watchdog' => 'Watchdog',
|
||||||
|
];
|
||||||
?>
|
?>
|
||||||
<div class="wrap woocow-wrap">
|
<div class="wrap woocow-wrap">
|
||||||
<h1>WooCow – Server Logs</h1>
|
<h1>WooCow – Server Logs</h1>
|
||||||
@@ -471,8 +553,8 @@ class WooCow_Admin {
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<select id="wc-log-type">
|
<select id="wc-log-type">
|
||||||
<?php foreach ( $log_types as $t ) : ?>
|
<?php foreach ( $log_types as $slug => $label ) : ?>
|
||||||
<option value="<?php echo esc_attr( $t ); ?>"><?php echo esc_html( ucfirst( $t ) ); ?></option>
|
<option value="<?php echo esc_attr( $slug ); ?>"><?php echo esc_html( $label ); ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<button class="button" id="wc-log-load" disabled>Load Logs</button>
|
<button class="button" id="wc-log-load" disabled>Load Logs</button>
|
||||||
@@ -485,6 +567,34 @@ class WooCow_Admin {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Page: Quarantine ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function page_quarantine(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||||
|
?>
|
||||||
|
<div class="wrap woocow-wrap">
|
||||||
|
<h1>WooCow – Quarantine</h1>
|
||||||
|
<p>View and manage quarantined messages across all servers. You can permanently delete messages or blacklist senders by domain.</p>
|
||||||
|
|
||||||
|
<div class="woocow-toolbar woocow-flex">
|
||||||
|
<select id="wc-quar-server" class="regular-text">
|
||||||
|
<option value="">— Select server —</option>
|
||||||
|
<?php foreach ( $servers as $s ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $s->id ); ?>"><?php echo esc_html( $s->name ); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<button class="button" id="wc-quar-load" disabled>Load Quarantine</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wc-quar-notices"></div>
|
||||||
|
<div id="wc-quar-wrap">
|
||||||
|
<p class="description">Select a server above to view quarantined messages.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
// ── AJAX helpers ─────────────────────────────────────────────────────────
|
// ── AJAX helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private function verify(): void {
|
private function verify(): void {
|
||||||
@@ -595,7 +705,26 @@ class WooCow_Admin {
|
|||||||
if ( ! $result['success'] ) {
|
if ( ! $result['success'] ) {
|
||||||
$this->json_err( $result['error'] ?? 'Failed to fetch domains.' );
|
$this->json_err( $result['error'] ?? 'Failed to fetch domains.' );
|
||||||
}
|
}
|
||||||
$domains = array_map( fn( $d ) => [ 'domain' => $d['domain_name'], 'active' => $d['active'] ], (array) $result['data'] );
|
$domains = array_map( function ( $d ) {
|
||||||
|
$rl = is_array( $d['rl'] ?? false ) ? $d['rl'] : [];
|
||||||
|
return [
|
||||||
|
'domain' => $d['domain_name'],
|
||||||
|
'active' => $d['active'],
|
||||||
|
'description' => $d['description'] ?? '',
|
||||||
|
'mailboxes' => $d['max_num_mboxes_for_domain'] ?? 0,
|
||||||
|
'mboxes_in' => $d['mboxes_in_domain'] ?? 0,
|
||||||
|
'aliases' => $d['max_num_aliases_for_domain'] ?? 0,
|
||||||
|
'aliases_in' => $d['aliases_in_domain'] ?? 0,
|
||||||
|
'quota' => (int) round( ( $d['max_quota_for_domain'] ?? 0 ) / 1024 / 1024 ),
|
||||||
|
'defquota' => (int) round( ( $d['def_new_mailbox_quota'] ?? 0 ) / 1024 / 1024 ),
|
||||||
|
'quota_used' => (int) round( ( $d['quota_used_in_domain'] ?? 0 ) / 1024 / 1024 ),
|
||||||
|
'relayhost' => $d['relayhost'] ?? '0',
|
||||||
|
'rl_value' => $rl['value'] ?? 0,
|
||||||
|
'rl_frame' => $rl['frame'] ?? 's',
|
||||||
|
'gal' => $d['gal'] ?? '0',
|
||||||
|
'backupmx' => $d['backupmx'] ?? '0',
|
||||||
|
];
|
||||||
|
}, (array) $result['data'] );
|
||||||
$this->json_ok( $domains );
|
$this->json_ok( $domains );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,6 +1033,90 @@ class WooCow_Admin {
|
|||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ajax_woocow_admin_domain_edit(): void {
|
||||||
|
$this->verify();
|
||||||
|
|
||||||
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||||
|
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||||
|
|
||||||
|
if ( ! $domain ) {
|
||||||
|
$this->json_err( 'Domain name is required.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = $this->get_server( $server_id );
|
||||||
|
if ( ! $server ) {
|
||||||
|
$this->json_err( 'Server not found.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$attr = [];
|
||||||
|
|
||||||
|
if ( isset( $_POST['description'] ) ) {
|
||||||
|
$attr['description'] = sanitize_text_field( $_POST['description'] );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['mailboxes'] ) ) {
|
||||||
|
$attr['mailboxes'] = absint( $_POST['mailboxes'] );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['aliases'] ) ) {
|
||||||
|
$attr['aliases'] = absint( $_POST['aliases'] );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['quota'] ) ) {
|
||||||
|
$attr['quota'] = absint( $_POST['quota'] );
|
||||||
|
$attr['maxquota'] = absint( $_POST['quota'] );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['defquota'] ) ) {
|
||||||
|
$attr['defquota'] = absint( $_POST['defquota'] );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['rl_value'] ) ) {
|
||||||
|
$attr['rl_value'] = absint( $_POST['rl_value'] );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['rl_frame'] ) ) {
|
||||||
|
$frame = sanitize_text_field( $_POST['rl_frame'] );
|
||||||
|
if ( in_array( $frame, [ 's', 'm', 'h', 'd' ], true ) ) {
|
||||||
|
$attr['rl_frame'] = $frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['relayhost'] ) ) {
|
||||||
|
$attr['relayhost'] = absint( $_POST['relayhost'] );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['active'] ) ) {
|
||||||
|
$attr['active'] = absint( $_POST['active'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = WooCow_API::from_server( $server );
|
||||||
|
$result = $api->edit_domain( [ $domain ], $attr );
|
||||||
|
|
||||||
|
if ( ! $result['success'] ) {
|
||||||
|
$this->json_err( $result['error'] ?? 'Failed to update domain.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->json_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ajax_woocow_admin_domain_delete(): void {
|
||||||
|
$this->verify();
|
||||||
|
|
||||||
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||||
|
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||||
|
|
||||||
|
if ( ! $domain ) {
|
||||||
|
$this->json_err( 'Domain name is required.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = $this->get_server( $server_id );
|
||||||
|
if ( ! $server ) {
|
||||||
|
$this->json_err( 'Server not found.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = WooCow_API::from_server( $server );
|
||||||
|
$result = $api->delete_domain( $domain );
|
||||||
|
|
||||||
|
if ( ! $result['success'] ) {
|
||||||
|
$this->json_err( $result['error'] ?? 'Failed to delete domain.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->json_ok();
|
||||||
|
}
|
||||||
|
|
||||||
// ── AJAX: Transports ──────────────────────────────────────────────────────
|
// ── AJAX: Transports ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
public function ajax_woocow_admin_relayhosts_list(): void {
|
public function ajax_woocow_admin_relayhosts_list(): void {
|
||||||
@@ -982,7 +1195,7 @@ class WooCow_Admin {
|
|||||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||||
$log_type = sanitize_key( $_POST['log_type'] ?? 'postfix' );
|
$log_type = sanitize_key( $_POST['log_type'] ?? 'postfix' );
|
||||||
|
|
||||||
$allowed = [ 'postfix', 'dovecot', 'rspamd', 'ratelimit', 'api', 'acme', 'autodiscover', 'sogo', 'netfilter', 'watchdog' ];
|
$allowed = [ 'postfix', 'dovecot', 'rspamd-history', 'ratelimited', 'api', 'acme', 'autodiscover', 'sogo', 'netfilter', 'watchdog' ];
|
||||||
if ( ! in_array( $log_type, $allowed, true ) ) {
|
if ( ! in_array( $log_type, $allowed, true ) ) {
|
||||||
$this->json_err( 'Invalid log type.' );
|
$this->json_err( 'Invalid log type.' );
|
||||||
}
|
}
|
||||||
@@ -993,7 +1206,7 @@ class WooCow_Admin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$api = WooCow_API::from_server( $server );
|
$api = WooCow_API::from_server( $server );
|
||||||
$result = $api->request( 'GET', '/api/v1/get/logs/' . $log_type );
|
$result = $api->request( 'GET', '/api/v1/get/logs/' . $log_type . '/100' );
|
||||||
|
|
||||||
if ( ! $result['success'] ) {
|
if ( ! $result['success'] ) {
|
||||||
$this->json_err( $result['error'] ?? 'Failed to fetch logs.' );
|
$this->json_err( $result['error'] ?? 'Failed to fetch logs.' );
|
||||||
@@ -1001,4 +1214,76 @@ class WooCow_Admin {
|
|||||||
|
|
||||||
$this->json_ok( $result['data'] ?? [] );
|
$this->json_ok( $result['data'] ?? [] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── AJAX: Admin Quarantine ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function ajax_woocow_admin_quarantine(): void {
|
||||||
|
$this->verify();
|
||||||
|
|
||||||
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||||
|
$server = $this->get_server( $server_id );
|
||||||
|
if ( ! $server ) {
|
||||||
|
$this->json_err( 'Server not found.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = WooCow_API::from_server( $server );
|
||||||
|
$result = $api->get_quarantine();
|
||||||
|
|
||||||
|
if ( ! $result['success'] ) {
|
||||||
|
$this->json_err( $result['error'] ?? 'Failed to fetch quarantine.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->json_ok( $result['data'] ?? [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ajax_woocow_admin_quarantine_delete(): void {
|
||||||
|
$this->verify();
|
||||||
|
|
||||||
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||||
|
$qid = absint( $_POST['qid'] ?? 0 );
|
||||||
|
|
||||||
|
if ( ! $qid ) {
|
||||||
|
$this->json_err( 'Message ID is required.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = $this->get_server( $server_id );
|
||||||
|
if ( ! $server ) {
|
||||||
|
$this->json_err( 'Server not found.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = WooCow_API::from_server( $server );
|
||||||
|
$result = $api->delete_quarantine( $qid );
|
||||||
|
|
||||||
|
if ( ! $result['success'] ) {
|
||||||
|
$this->json_err( $result['error'] ?? 'Failed to delete quarantine message.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->json_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ajax_woocow_admin_quarantine_block(): void {
|
||||||
|
$this->verify();
|
||||||
|
|
||||||
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||||
|
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||||
|
$object_from = sanitize_text_field( $_POST['object_from'] ?? '' );
|
||||||
|
|
||||||
|
if ( ! $domain || ! $object_from ) {
|
||||||
|
$this->json_err( 'Domain and sender address are required.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = $this->get_server( $server_id );
|
||||||
|
if ( ! $server ) {
|
||||||
|
$this->json_err( 'Server not found.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = WooCow_API::from_server( $server );
|
||||||
|
$result = $api->add_domain_policy( $domain, $object_from, 'bl' );
|
||||||
|
|
||||||
|
if ( ! $result['success'] ) {
|
||||||
|
$this->json_err( $result['error'] ?? 'Failed to blacklist sender.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->json_ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,24 @@ class WooCow_API {
|
|||||||
return $this->request( 'POST', '/api/v1/delete/quarantine', [ 'items' => [ $id ] ] );
|
return $this->request( 'POST', '/api/v1/delete/quarantine', [ 'items' => [ $id ] ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Domain Policies ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function get_domain_policy_bl( string $domain ): array {
|
||||||
|
return $this->request( 'GET', '/api/v1/get/policy_bl_domain/' . rawurlencode( $domain ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_domain_policy( string $domain, string $object_from, string $list = 'bl' ): array {
|
||||||
|
return $this->request( 'POST', '/api/v1/add/domain-policy', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'object_from' => $object_from,
|
||||||
|
'object_list' => $list,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete_domain_policy( array $prefids ): array {
|
||||||
|
return $this->request( 'POST', '/api/v1/delete/domain-policy', [ 'items' => $prefids ] );
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public function get_webmail_url(): string {
|
public function get_webmail_url(): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user