From dbe4abccf74703975f58975d1936deaa5ea5c045 Mon Sep 17 00:00:00 2001 From: Malin Date: Fri, 27 Feb 2026 08:53:11 +0100 Subject: [PATCH] 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 --- assets/js/woocow-admin.js | 196 ++++++++++++++++++++- includes/class-woocow-admin.php | 299 +++++++++++++++++++++++++++++++- includes/class-woocow-api.php | 18 ++ 3 files changed, 500 insertions(+), 13 deletions(-) diff --git a/assets/js/woocow-admin.js b/assets/js/woocow-admin.js index 719c7a5..c58f29a 100644 --- a/assets/js/woocow-admin.js +++ b/assets/js/woocow-admin.js @@ -476,12 +476,14 @@ 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 = () => { @@ -491,18 +493,44 @@ return; } const domains = res.data; + domainsData = {}; + domains.forEach(d => { domainsData[d.domain] = d; }); + if (!domains.length) { $('#wc-dom-table-wrap').html('

No domains on this server yet.

').show(); } else { let html = ` - `; + + + + + + + `; domains.forEach(d => { + const mboxes = `${d.mboxes_in}/${d.mailboxes || '∞'}`; + const qUsed = formatMB(d.quota_used * 1024 * 1024); + const qTotal = d.quota ? formatMB(d.quota * 1024 * 1024) : '∞'; + const active = d.active == 1 + ? 'Active' + : 'Inactive'; html += ` - - - + + + + `; @@ -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(''); + 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'); @@ -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('

Loading…

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

${esc(res.data)}

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

No quarantined messages.

'); + return; + } + let html = `
DomainActiveActions
DomainMailboxesQuota UsedActiveActions
${esc(d.domain)}${d.active == 1 ? 'Active' : 'Inactive'} - ${esc(d.domain)}${d.description ? `
${esc(d.description)}` : ''}
${mboxes}${qUsed} / ${qTotal}${active} + + +
+ + + `; + msgs.forEach(m => { + const date = m.created ? new Date(m.created * 1000).toLocaleString() : '—'; + const domain = (m.rcpt || '').split('@')[1] || ''; + html += ` + + + + + + + `; + }); + html += '
DateSenderRecipientSubjectScoreActions
${esc(date)}${esc(m.sender)}${esc(m.rcpt)}${esc(m.subject)}${esc(m.score)} + + +
'; + html += `

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

`; + $('#wc-quar-wrap').html(html); + }); + }; + + $('#wc-quar-load').on('click', loadQuarantine); + + $(document).on('click', '.wc-quar-del', function () { + if (!confirm('Permanently delete this quarantined message?')) return; + const qid = $(this).data('qid'); + ajax('woocow_admin_quarantine_delete', { server_id: quarServerId, qid }).done(res => { + if (res.success) loadQuarantine(); + else notice($('#wc-quar-notices'), 'error', res.data); + }); + }); + + $(document).on('click', '.wc-quar-block', function () { + const sender = $(this).data('sender'); + const domain = $(this).data('domain'); + if (!domain) { notice($('#wc-quar-notices'), 'error', 'Could not determine recipient domain.'); return; } + if (!confirm(`Add ${sender} to the blacklist for domain ${domain}?`)) return; + ajax('woocow_admin_quarantine_block', { + server_id: quarServerId, + domain, + object_from: sender, + }).done(res => { + if (res.success) notice($('#wc-quar-notices'), 'success', `Sender ${esc(sender)} blacklisted for ${esc(domain)}.`); + else notice($('#wc-quar-notices'), 'error', res.data); + }); + }); + } + })(jQuery); diff --git a/includes/class-woocow-admin.php b/includes/class-woocow-admin.php index 0cd6894..c9bcc09 100644 --- a/includes/class-woocow-admin.php +++ b/includes/class-woocow-admin.php @@ -26,10 +26,15 @@ class WooCow_Admin { 'woocow_admin_mailbox_edit', 'woocow_admin_domain_add', 'woocow_admin_domain_dns', + 'woocow_admin_domain_edit', + 'woocow_admin_domain_delete', 'woocow_admin_relayhosts_list', 'woocow_admin_relayhost_save', 'woocow_admin_relayhost_delete', 'woocow_admin_logs', + 'woocow_admin_quarantine', + 'woocow_admin_quarantine_delete', + 'woocow_admin_quarantine_block', ]; 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', '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', '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 { @@ -385,6 +391,71 @@ class WooCow_Admin {
+ + +