feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes

Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
  display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
  with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
  server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
  date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
  to any external address)

UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
  (lock, chart-bar, trash) with title tooltips

i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 08:38:52 +01:00
parent 1ea2ed7e74
commit 1c5b58f238
11 changed files with 1252 additions and 14 deletions

View File

@@ -324,12 +324,18 @@
<td>${max}</td>
<td>${m.active == 1 ? '✓' : ''}</td>
<td class="woocow-actions">
<button class="button button-small wc-mb-reset-pw"
data-email="${esc(m.username)}">Reset PW</button>
<button class="button button-small wc-mb-set-quota"
data-email="${esc(m.username)}" data-quota="${quotaMB}">Set Quota</button>
<button class="button button-small wc-mb-del"
data-email="${esc(m.username)}" style="color:#a00">Delete</button>
<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>`;
});
@@ -459,9 +465,236 @@
}
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;
$('#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();
});
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;
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>Active</th><th>Actions</th></tr></thead><tbody>`;
domains.forEach(d => {
html += `<tr>
<td><strong>${esc(d.domain)}</strong></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>
<button class="button button-small wc-dom-dns" data-domain="${esc(d.domain)}">
<span class="dashicons dashicons-admin-site"></span> DNS Records
</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>`);
}
});
});
// 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());
});
$('#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;
}
// Render as a scrollable pre block
const text = entries.map(e => {
if (typeof e === 'string') return e;
if (e.time && e.message) return `[${e.time}] ${e.message}`;
return JSON.stringify(e);
}).join('\n');
$('#wc-log-wrap').html(`
<div class="woocow-log-toolbar">
<strong>${esc(type.charAt(0).toUpperCase() + type.slice(1))} log</strong>
<span style="color:#666;font-size:12px">${entries.length} entries</span>
</div>
<pre class="woocow-log-pre">${esc(text)}</pre>
`);
});
});
}
})(jQuery);