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

@@ -57,9 +57,10 @@
let html = '';
boxes.forEach(m => {
const pct = parseFloat(m.percent_in_use || 0);
const unlimited = (m.quota === 0 || m.quota === '0');
const pct = unlimited ? 0 : parseFloat(m.percent_in_use || 0);
const used = formatMB(m.quota_used);
const max = formatMB(m.quota);
const max = unlimited ? '∞' : formatMB(m.quota);
const col = pct > 85 ? '#e74c3c' : pct > 60 ? '#f39c12' : '#27ae60';
html += `<div class="woocow-mailbox-row" data-email="${esc(m.username)}">
@@ -79,14 +80,19 @@
<div class="woocow-mbox-actions">
<button class="woocow-btn woocow-btn-sm wc-change-pw"
data-server="${esc(sid)}" data-domain="${esc(domain)}" data-email="${esc(m.username)}">
Change Password
🔑 Change Password
</button>
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-toggle-aliases"
data-server="${esc(sid)}" data-domain="${esc(domain)}">
Aliases
Aliases &amp; Forwarders
</button>
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-spam-score-btn"
data-server="${esc(sid)}" data-domain="${esc(domain)}"
data-email="${esc(m.username)}" data-score="${esc(m.spam_score || 5)}">
🛡 Spam Filter
</button>
<a href="${esc(webmail)}" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm woocow-btn-ghost">
Webmail
Webmail
</a>
</div>
<div class="woocow-aliases-wrap" style="display:none">
@@ -278,4 +284,106 @@
});
});
// ── Quarantine ────────────────────────────────────────────────────────────
$(document).on('click', '.woocow-load-quarantine', function () {
const $panel = $(this).closest('.woocow-domain-panel');
const $wrap = $panel.find('.woocow-quarantine-wrap');
const $list = $panel.find('.woocow-quarantine-list');
const sid = $panel.data('server-id');
const domain = $panel.data('domain');
if ($wrap.is(':visible')) { $wrap.slideUp(); return; }
$list.html('<p class="woocow-loading">Loading quarantine…</p>');
$wrap.slideDown();
ajax('woocow_acct_quarantine', { server_id: sid, domain }).done(res => {
if (!res.success) { $list.html(`<p class="woocow-error">${esc(res.data)}</p>`); return; }
const msgs = res.data;
if (!msgs.length) { $list.html('<p class="woocow-muted">No quarantined messages for this domain.</p>'); return; }
let html = `<table class="woocow-quarantine-table">
<thead><tr><th>From</th><th>To</th><th>Subject</th><th>Score</th><th>Date</th><th></th></tr></thead><tbody>`;
msgs.forEach(m => {
const date = new Date(m.created * 1000).toLocaleString();
const score = parseFloat(m.score).toFixed(1);
const virus = m.virus_flag == 1 ? ' 🦠' : '';
html += `<tr>
<td>${esc(m.sender)}</td>
<td>${esc(m.rcpt)}</td>
<td>${esc(m.subject)}${virus}</td>
<td><span class="woocow-score-badge" style="background:${score > 10 ? '#e74c3c' : score > 5 ? '#f39c12' : '#95a5a6'}">${score}</span></td>
<td style="white-space:nowrap;font-size:12px">${esc(date)}</td>
<td><button class="woocow-btn woocow-btn-danger woocow-btn-xs wc-q-del"
data-id="${esc(m.id)}" data-server="${esc(sid)}" data-domain="${esc(domain)}">Delete</button></td>
</tr>`;
});
html += '</tbody></table>';
html += '<p class="woocow-muted" style="margin-top:8px">To release a message to your inbox, use the link in your quarantine notification email or via Webmail.</p>';
$list.html(html);
});
});
$(document).on('click', '.wc-q-del', function () {
if (!confirm('Permanently delete this quarantined message?')) return;
const $row = $(this).closest('tr');
ajax('woocow_acct_quarantine_delete', {
server_id: $(this).data('server'),
domain: $(this).data('domain'),
qid: $(this).data('id'),
}).done(res => {
if (res.success) $row.fadeOut(300, function () { $(this).remove(); });
else alert('Delete failed: ' + res.data);
});
});
// ── Spam Score ────────────────────────────────────────────────────────────
$(document).on('click', '.wc-spam-score-btn', function () {
const $row = $(this).closest('.woocow-mailbox-row');
const sid = $(this).data('server');
const domain = $(this).data('domain');
const email = $(this).data('email');
const score = $(this).data('score');
const $existing = $row.find('.woocow-spam-panel');
if ($existing.length) { $existing.slideToggle(); return; }
$row.append(`
<div class="woocow-spam-panel" style="margin-top:12px;padding-top:12px;border-top:1px dashed #e0e0e0">
<strong>Spam Filter Threshold</strong>
<p class="woocow-muted">Lower = stricter. Default is 5. Emails above this score go to spam/quarantine.</p>
<div class="woocow-flex-inline" style="gap:10px;margin-top:8px">
<input type="range" class="wc-spam-slider" min="1" max="15" step="0.5" value="${esc(score)}"
style="width:200px">
<span class="wc-spam-val">${parseFloat(score).toFixed(1)}</span>
<button class="woocow-btn woocow-btn-primary woocow-btn-sm wc-spam-save"
data-server="${esc(sid)}" data-domain="${esc(domain)}" data-email="${esc(email)}">Save</button>
<span class="wc-spam-notice"></span>
</div>
</div>
`);
$row.find('.wc-spam-slider').on('input', function () {
$row.find('.wc-spam-val').text(parseFloat($(this).val()).toFixed(1));
});
});
$(document).on('click', '.wc-spam-save', function () {
const $panel = $(this).closest('.woocow-spam-panel');
const $note = $panel.find('.wc-spam-notice');
const score = $panel.find('.wc-spam-slider').val();
$note.text('Saving…');
ajax('woocow_acct_spam_score', {
server_id: $(this).data('server'),
domain: $(this).data('domain'),
email: $(this).data('email'),
spam_score: score,
}).done(res => {
if (res.success) $note.html('<span style="color:green">✓ Saved</span>');
else $note.html(`<span class="woocow-error">${esc(res.data)}</span>`);
});
});
})(jQuery);