Files
WooCow/assets/js/woocow-account.js
Malin 1c5b58f238 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>
2026-02-27 08:38:52 +01:00

390 lines
19 KiB
JavaScript
Raw Permalink 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 My Account frontend JavaScript
*/
(function ($) {
'use strict';
if (!$('#woocow-account').length) return;
const ajax = (action, data) =>
$.post(woocowAcct.ajax_url, { action, nonce: woocowAcct.nonce, ...data });
const notice = (msg, type = 'success') => {
const $n = $('#woocow-acct-notices');
$n.html(`<div class="woocommerce-${type === 'success' ? 'message' : 'error'}">${msg}</div>`);
setTimeout(() => $n.find('> div').fadeOut(400, function () { $(this).remove(); }), 5000);
};
function esc(str) {
return String(str).replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[m]);
}
function formatMB(bytes) {
if (!bytes) return '0 MB';
const mb = bytes / 1024 / 1024;
return mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB';
}
// ── Load Mailboxes ────────────────────────────────────────────────────────
$(document).on('click', '.woocow-load-mailboxes', function () {
const $panel = $(this).closest('.woocow-domain-panel');
const sid = $panel.data('server-id');
const domain = $panel.data('domain');
const $wrap = $panel.find('.woocow-mailboxes-wrap');
const $list = $panel.find('.woocow-mailboxes-list');
$(this).prop('disabled', true).text('Loading…');
$wrap.show();
$list.html('<p class="woocow-loading">Fetching mailboxes…</p>');
ajax('woocow_acct_mailboxes', { server_id: sid, domain }).done(res => {
$(this).prop('disabled', false).text('Refresh');
if (!res.success) {
$list.html(`<p class="woocow-error">${esc(res.data)}</p>`);
return;
}
const boxes = res.data.mailboxes || [];
const webmail = res.data.webmail_url;
if (!boxes.length) {
$list.html('<p class="woocow-muted">No mailboxes yet. Create one below.</p>');
return;
}
let html = '';
boxes.forEach(m => {
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 = unlimited ? '∞' : formatMB(m.quota);
const col = pct > 85 ? '#e74c3c' : pct > 60 ? '#f39c12' : '#27ae60';
html += `<div class="woocow-mailbox-row" data-email="${esc(m.username)}">
<div class="woocow-mbox-main">
<div class="woocow-mbox-address">
<span class="woocow-mbox-icon">✉</span>
<strong>${esc(m.username)}</strong>
${m.name ? `<span class="woocow-mbox-name">(${esc(m.name)})</span>` : ''}
</div>
<div class="woocow-quota-wrap">
<div class="woocow-quota-bar-outer">
<div class="woocow-quota-bar-inner" style="width:${pct}%;background:${col}"></div>
</div>
<span class="woocow-quota-text">${used} / ${max} (${pct}%)</span>
</div>
</div>
<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
</button>
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-toggle-aliases"
data-server="${esc(sid)}" data-domain="${esc(domain)}">
✉ 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
</a>
</div>
<div class="woocow-aliases-wrap" style="display:none">
<div class="woocow-aliases-list"></div>
<div class="woocow-alias-create-form" style="display:none">
<div class="woocow-alias-fields">
<input type="email" class="wc-alias-addr woocow-input" placeholder="alias@${esc(domain)}">
<span class="woocow-arrow">→</span>
<input type="email" class="wc-alias-goto woocow-input" placeholder="destination@example.com" value="${esc(m.username)}">
<button class="woocow-btn woocow-btn-primary woocow-btn-sm wc-alias-save"
data-server="${esc(sid)}" data-domain="${esc(domain)}">Add</button>
<button class="woocow-btn woocow-btn-sm wc-alias-cancel">✕</button>
</div>
</div>
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-alias-add-btn">+ Add Alias</button>
</div>
</div>`;
});
$list.html(html);
});
});
// ── Aliases ───────────────────────────────────────────────────────────────
$(document).on('click', '.wc-toggle-aliases', function () {
const $row = $(this).closest('.woocow-mailbox-row');
const $wrap = $row.find('.woocow-aliases-wrap');
const sid = $(this).data('server');
const domain = $(this).data('domain');
if ($wrap.is(':visible')) {
$wrap.slideUp();
return;
}
const $list = $row.find('.woocow-aliases-list').html('<p class="woocow-muted">Loading aliases…</p>');
$wrap.slideDown();
ajax('woocow_acct_aliases', { server_id: sid, domain }).done(res => {
if (!res.success) {
$list.html(`<p class="woocow-error">${esc(res.data)}</p>`);
return;
}
const aliases = res.data;
if (!aliases.length) {
$list.html('<p class="woocow-muted">No aliases for this domain yet.</p>');
return;
}
let html = '<ul class="woocow-alias-list">';
aliases.forEach(a => {
html += `<li>
<span class="woocow-alias-addr">${esc(a.address)}</span>
<span class="woocow-arrow">→</span>
<span class="woocow-alias-goto">${esc(a.goto)}</span>
<button class="woocow-btn woocow-btn-danger woocow-btn-xs wc-alias-del"
data-id="${esc(a.id)}" data-server="${esc(sid)}" data-domain="${esc(domain)}">✕</button>
</li>`;
});
html += '</ul>';
$list.html(html);
});
});
$(document).on('click', '.wc-alias-add-btn', function () {
$(this).closest('.woocow-aliases-wrap').find('.woocow-alias-create-form').slideToggle();
});
$(document).on('click', '.wc-alias-cancel', function () {
$(this).closest('.woocow-alias-create-form').slideUp();
});
$(document).on('click', '.wc-alias-save', function () {
const $form = $(this).closest('.woocow-alias-create-form');
const sid = $(this).data('server');
const domain = $(this).data('domain');
const addr = $form.find('.wc-alias-addr').val().trim();
const goto_ = $form.find('.wc-alias-goto').val().trim();
if (!addr || !goto_) { alert('Both alias and destination are required.'); return; }
ajax('woocow_acct_alias_create', { server_id: sid, domain, address: addr, goto: goto_ }).done(res => {
if (res.success) {
// Refresh alias list
$(this).closest('.woocow-mailbox-row').find('.wc-toggle-aliases').trigger('click');
setTimeout(() => { $(this).closest('.woocow-mailbox-row').find('.wc-toggle-aliases').trigger('click'); }, 300);
$form.slideUp();
} else {
alert('Error: ' + res.data);
}
});
});
$(document).on('click', '.wc-alias-del', function () {
if (!confirm('Delete this alias?')) return;
const $li = $(this).closest('li');
const sid = $(this).data('server');
const domain = $(this).data('domain');
const id = $(this).data('id');
ajax('woocow_acct_alias_delete', { server_id: sid, domain, alias_id: id }).done(res => {
if (res.success) $li.fadeOut(300, function () { $(this).remove(); });
else alert('Delete failed: ' + res.data);
});
});
// ── Create Mailbox ────────────────────────────────────────────────────────
$(document).on('click', '.woocow-create-mbox-btn', function () {
const $panel = $(this).closest('.woocow-domain-panel');
$panel.find('.woocow-create-mbox-form').slideToggle();
});
$(document).on('click', '.wc-mbox-cancel', function () {
$(this).closest('.woocow-create-mbox-form').slideUp();
});
$(document).on('click', '.wc-mbox-submit', function () {
const $panel = $(this).closest('.woocow-domain-panel');
const sid = $panel.data('server-id');
const domain = $panel.data('domain');
const $form = $(this).closest('.woocow-create-mbox-form');
const $note = $form.find('.wc-mbox-notice');
const local = $form.find('.wc-mbox-local').val().trim();
const name = $form.find('.wc-mbox-name').val().trim();
const pass = $form.find('.wc-mbox-pass').val();
const pass2 = $form.find('.wc-mbox-pass2').val();
const quota = $form.find('.wc-mbox-quota').val();
if (!local || !pass) { $note.html('<span class="woocow-error">Username and password required.</span>'); return; }
if (pass !== pass2) { $note.html('<span class="woocow-error">Passwords do not match.</span>'); return; }
$note.text('Creating…');
ajax('woocow_acct_mailbox_create', { server_id: sid, domain, local_part: local, name, password: pass, password2: pass2, quota }).done(res => {
if (res.success) {
$note.html('<span style="color:green">✓ Mailbox created!</span>');
$form.slideUp();
// Refresh mailbox list
$panel.find('.woocow-load-mailboxes').trigger('click');
} else {
$note.html(`<span class="woocow-error">${esc(res.data)}</span>`);
}
});
});
// ── Change Password Modal ─────────────────────────────────────────────────
$(document).on('click', '.wc-change-pw', function () {
const sid = $(this).data('server');
const domain = $(this).data('domain');
const email = $(this).data('email');
$('#woocow-pw-server-id').val(sid);
$('#woocow-pw-mailbox').val(email);
$('#woocow-pw-email').text(email);
$('#woocow-pw-new, #woocow-pw-new2').val('');
$('#woocow-pw-notice').text('');
$('#woocow-pw-modal').fadeIn(200);
// Store domain on modal for verification
$('#woocow-pw-modal').data('domain', domain);
});
$('#woocow-pw-cancel').on('click', () => $('#woocow-pw-modal').fadeOut(200));
$(document).on('click', '#woocow-pw-modal', function (e) {
if ($(e.target).is('#woocow-pw-modal')) $(this).fadeOut(200);
});
$('#woocow-pw-save').on('click', function () {
const sid = $('#woocow-pw-server-id').val();
const email = $('#woocow-pw-mailbox').val();
const domain = $('#woocow-pw-modal').data('domain');
const pass = $('#woocow-pw-new').val();
const pass2 = $('#woocow-pw-new2').val();
const $note = $('#woocow-pw-notice');
if (!pass || pass !== pass2) {
$note.html('<span class="woocow-error">Passwords do not match or are empty.</span>');
return;
}
$note.text('Updating…');
ajax('woocow_acct_mailbox_password', { server_id: sid, domain, email, password: pass, password2: pass2 }).done(res => {
if (res.success) {
$note.html('<span style="color:green">✓ Password updated!</span>');
setTimeout(() => $('#woocow-pw-modal').fadeOut(200), 1500);
} else {
$note.html(`<span class="woocow-error">${esc(res.data)}</span>`);
}
});
});
// ── 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);