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>
390 lines
19 KiB
JavaScript
390 lines
19 KiB
JavaScript
/**
|
||
* 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 => ({
|
||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||
})[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 & 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);
|