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:
@@ -336,6 +336,93 @@
|
||||
.woocow-muted { color: #aaa; font-size: 13px; }
|
||||
.woocow-error { color: #c0392b; }
|
||||
|
||||
/* ── Icon buttons (admin) ────────────────────────────────────── */
|
||||
.button.woocow-icon-btn {
|
||||
width: 30px !important;
|
||||
min-width: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.button.woocow-icon-btn .dashicons {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
/* ── DNS records table ───────────────────────────────────────── */
|
||||
.woocow-dns-table { margin-top: 8px; }
|
||||
.woocow-dns-host code { font-size: 11px; word-break: break-all; }
|
||||
.woocow-dns-val { max-width: 340px; }
|
||||
.woocow-dns-value {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background: #f6f8fa;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ── Log viewer ──────────────────────────────────────────────── */
|
||||
.woocow-log-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.woocow-log-pre {
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Quarantine (account) ────────────────────────────────────── */
|
||||
.woocow-quarantine-wrap {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background: #fffbf0;
|
||||
}
|
||||
.woocow-quarantine-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.woocow-quarantine-table th,
|
||||
.woocow-quarantine-table td {
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.woocow-quarantine-table th { font-weight: 700; background: #f5f5f5; }
|
||||
.woocow-quarantine-table tr:hover td { background: #fafafa; }
|
||||
.woocow-score-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 7px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Spam filter panel ───────────────────────────────────────── */
|
||||
.woocow-spam-panel { font-size: 13px; }
|
||||
.woocow-spam-panel input[type=range] { vertical-align: middle; }
|
||||
.wc-spam-val { font-weight: 700; min-width: 30px; display: inline-block; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.woocow-domain-header,
|
||||
.woocow-mbox-main,
|
||||
@@ -343,4 +430,6 @@
|
||||
.woocow-quota-bar-outer { width: 80px; }
|
||||
.woocow-alias-fields { flex-direction: column; }
|
||||
.woocow-input { max-width: 100%; }
|
||||
.woocow-quarantine-table { font-size: 11px; }
|
||||
.woocow-quarantine-table th, .woocow-quarantine-table td { padding: 5px 6px; }
|
||||
}
|
||||
|
||||
@@ -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 & 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -31,6 +31,9 @@ class WooCow_Account {
|
||||
'woocow_acct_aliases',
|
||||
'woocow_acct_alias_create',
|
||||
'woocow_acct_alias_delete',
|
||||
'woocow_acct_quarantine',
|
||||
'woocow_acct_quarantine_delete',
|
||||
'woocow_acct_spam_score',
|
||||
];
|
||||
foreach ( $actions as $action ) {
|
||||
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
|
||||
@@ -61,6 +64,32 @@ class WooCow_Account {
|
||||
wp_localize_script( 'woocow-account', 'woocowAcct', [
|
||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'woocow_account' ),
|
||||
'i18n' => [
|
||||
'load_mailboxes' => __( 'Load Mailboxes', 'woocow' ),
|
||||
'refresh' => __( 'Refresh', 'woocow' ),
|
||||
'loading' => __( 'Loading…', 'woocow' ),
|
||||
'no_mailboxes' => __( 'No mailboxes yet. Create one below.', 'woocow' ),
|
||||
'change_password' => __( 'Change Password', 'woocow' ),
|
||||
'aliases_forwarders' => __( 'Aliases & Forwarders', 'woocow' ),
|
||||
'spam_filter' => __( 'Spam Filter', 'woocow' ),
|
||||
'webmail' => __( 'Webmail', 'woocow' ),
|
||||
'add_alias' => __( 'Add Alias / Forwarder', 'woocow' ),
|
||||
'no_aliases' => __( 'No aliases for this domain yet.', 'woocow' ),
|
||||
'add_mailbox' => __( 'Add Mailbox', 'woocow' ),
|
||||
'create_mailbox' => __( 'Create Mailbox', 'woocow' ),
|
||||
'cancel' => __( 'Cancel', 'woocow' ),
|
||||
'save' => __( 'Save', 'woocow' ),
|
||||
'delete' => __( 'Delete', 'woocow' ),
|
||||
'quarantine' => __( 'Quarantine', 'woocow' ),
|
||||
'no_quarantine' => __( 'No quarantined messages for this domain.', 'woocow' ),
|
||||
'q_release_note' => __( 'To release a message to your inbox, use the link in your quarantine notification email or via Webmail.', 'woocow' ),
|
||||
'q_delete_confirm' => __( 'Permanently delete this quarantined message?', 'woocow' ),
|
||||
'spam_threshold' => __( 'Spam Filter Threshold', 'woocow' ),
|
||||
'spam_help' => __( 'Lower = stricter. Default is 5. Emails above this score go to spam/quarantine.', 'woocow' ),
|
||||
'update_password' => __( 'Update Password', 'woocow' ),
|
||||
'passwords_mismatch' => __( 'Passwords do not match or are empty.', 'woocow' ),
|
||||
'unlimited' => __( 'Unlimited', 'woocow' ),
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
@@ -94,7 +123,13 @@ class WooCow_Account {
|
||||
<a href="<?php echo esc_url( $assignment->webmail_url ); ?>" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm">
|
||||
Open Webmail
|
||||
</a>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline woocow-load-mailboxes">Load Mailboxes</button>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline woocow-load-mailboxes"><?php esc_html_e( 'Load Mailboxes', 'woocow' ); ?></button>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-ghost woocow-load-quarantine"><?php esc_html_e( 'Quarantine', 'woocow' ); ?></button>
|
||||
</div>
|
||||
|
||||
<!-- Quarantine panel -->
|
||||
<div class="woocow-quarantine-wrap" style="display:none">
|
||||
<div class="woocow-quarantine-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="woocow-mailboxes-wrap" style="display:none">
|
||||
@@ -416,6 +451,98 @@ class WooCow_Account {
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
// ── AJAX: Quarantine ──────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_acct_quarantine(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->get_quarantine();
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Could not load quarantine.' );
|
||||
}
|
||||
|
||||
// Filter to messages where the recipient belongs to this domain.
|
||||
$messages = array_values( array_filter( (array) $result['data'], function ( $m ) use ( $domain ) {
|
||||
return isset( $m['rcpt'] ) && str_ends_with( $m['rcpt'], '@' . $domain );
|
||||
} ) );
|
||||
|
||||
wp_send_json_success( $messages );
|
||||
}
|
||||
|
||||
public function ajax_woocow_acct_quarantine_delete(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
$qid = absint( $_POST['qid'] ?? 0 );
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->delete_quarantine( $qid );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Failed to delete quarantine message.' );
|
||||
}
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
// ── AJAX: Spam score ──────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_acct_spam_score(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
$email = sanitize_email( $_POST['email'] ?? '' );
|
||||
$spam_score = floatval( $_POST['spam_score'] ?? 5 );
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
if ( ! str_ends_with( $email, '@' . $domain ) ) {
|
||||
wp_send_json_error( 'Email does not belong to your domain.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->request( 'POST', '/api/v1/edit/spam-score/' . rawurlencode( $email ), [
|
||||
'spam_score' => $spam_score,
|
||||
] );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Failed to update spam score.' );
|
||||
}
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
// ── WP password change sync ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,12 @@ class WooCow_Admin {
|
||||
'woocow_admin_mailbox_create',
|
||||
'woocow_admin_mailbox_delete',
|
||||
'woocow_admin_mailbox_edit',
|
||||
'woocow_admin_domain_add',
|
||||
'woocow_admin_domain_dns',
|
||||
'woocow_admin_relayhosts_list',
|
||||
'woocow_admin_relayhost_save',
|
||||
'woocow_admin_relayhost_delete',
|
||||
'woocow_admin_logs',
|
||||
];
|
||||
|
||||
foreach ( $ajax_actions as $action ) {
|
||||
@@ -43,10 +49,13 @@ class WooCow_Admin {
|
||||
'dashicons-email-alt2',
|
||||
56
|
||||
);
|
||||
add_submenu_page( 'woocow', 'Dashboard', 'Dashboard', 'manage_woocommerce', 'woocow', [ $this, 'page_dashboard' ] );
|
||||
add_submenu_page( 'woocow', 'Dashboard', 'Dashboard', 'manage_woocommerce', 'woocow', [ $this, 'page_dashboard' ] );
|
||||
add_submenu_page( 'woocow', 'Servers', 'Servers', 'manage_woocommerce', 'woocow-servers', [ $this, 'page_servers' ] );
|
||||
add_submenu_page( 'woocow', 'Assignments','Assignments', 'manage_woocommerce', 'woocow-assignments', [ $this, 'page_assignments' ] );
|
||||
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' ] );
|
||||
}
|
||||
|
||||
public function enqueue_assets( string $hook ): void {
|
||||
@@ -301,6 +310,181 @@ class WooCow_Admin {
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Domains ────────────────────────────────────────────────────────
|
||||
|
||||
public function page_domains(): void {
|
||||
global $wpdb;
|
||||
$servers = $wpdb->get_results( "SELECT id, name, url FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Domains</h1>
|
||||
<p>Add and manage domains on your Mailcow servers. After adding a domain, DNS records (including DKIM) are generated automatically.</p>
|
||||
|
||||
<div class="woocow-toolbar woocow-flex">
|
||||
<select id="wc-dom-server" class="regular-text">
|
||||
<option value="">— Select server —</option>
|
||||
<?php foreach ( $servers as $s ) : ?>
|
||||
<option value="<?php echo esc_attr( $s->id ); ?>"
|
||||
data-url="<?php echo esc_attr( $s->url ); ?>">
|
||||
<?php echo esc_html( $s->name ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button" id="wc-dom-load" disabled>Load Domains</button>
|
||||
<button class="button button-primary" id="wc-dom-add-btn" style="display:none">+ Add Domain</button>
|
||||
</div>
|
||||
|
||||
<div id="wc-dom-notices"></div>
|
||||
|
||||
<!-- Add Domain form -->
|
||||
<div id="wc-dom-form" class="woocow-card woocow-form" style="display:none">
|
||||
<h3>Add Domain</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="wc-dom-name">Domain</label></th>
|
||||
<td><input type="text" id="wc-dom-name" class="regular-text" placeholder="example.com"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-desc">Description</label></th>
|
||||
<td><input type="text" id="wc-dom-desc" class="regular-text" placeholder="Optional"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-mailboxes">Max Mailboxes</label></th>
|
||||
<td><input type="number" id="wc-dom-mailboxes" value="10" min="1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-aliases">Max Aliases</label></th>
|
||||
<td><input type="number" id="wc-dom-aliases" value="400" min="0"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-quota">Total Quota (MB)</label></th>
|
||||
<td><input type="number" id="wc-dom-quota" value="10240" min="1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-defquota">Default Mailbox Quota (MB)</label></th>
|
||||
<td><input type="number" id="wc-dom-defquota" value="3072" min="1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-dkim-size">DKIM Key Size</label></th>
|
||||
<td>
|
||||
<select id="wc-dom-dkim-size">
|
||||
<option value="2048" selected>2048 (recommended)</option>
|
||||
<option value="1024">1024</option>
|
||||
<option value="4096">4096</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="button button-primary" id="wc-dom-save">Add Domain & Generate DKIM</button>
|
||||
<button class="button" id="wc-dom-cancel">Cancel</button>
|
||||
<span id="wc-dom-form-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Domain list -->
|
||||
<div id="wc-dom-table-wrap"></div>
|
||||
|
||||
<!-- DNS Records panel -->
|
||||
<div id="wc-dom-dns-panel" class="woocow-card" style="display:none">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
||||
<h3 style="margin:0" id="wc-dom-dns-title">DNS Records</h3>
|
||||
<button class="button" id="wc-dom-dns-close">Close</button>
|
||||
</div>
|
||||
<p class="description">Add these records to your DNS provider for <strong id="wc-dom-dns-domain"></strong>.</p>
|
||||
<div id="wc-dom-dns-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Transports ─────────────────────────────────────────────────────
|
||||
|
||||
public function page_transports(): void {
|
||||
global $wpdb;
|
||||
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Sender-Dependent Transports</h1>
|
||||
<p>Configure relay hosts for outbound mail delivery. Each transport can be associated with one or more domains in Mailcow.</p>
|
||||
|
||||
<div class="woocow-toolbar woocow-flex">
|
||||
<select id="wc-tr-server" class="regular-text">
|
||||
<option value="">— Select server —</option>
|
||||
<?php foreach ( $servers as $s ) : ?>
|
||||
<option value="<?php echo esc_attr( $s->id ); ?>"><?php echo esc_html( $s->name ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button" id="wc-tr-load" disabled>Load Transports</button>
|
||||
<button class="button button-primary" id="wc-tr-add-btn" style="display:none">+ Add Transport</button>
|
||||
</div>
|
||||
|
||||
<div id="wc-tr-notices"></div>
|
||||
|
||||
<div id="wc-tr-form" class="woocow-card woocow-form" style="display:none">
|
||||
<h3>Add Transport (Relay Host)</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="wc-tr-hostname">Hostname:Port</label></th>
|
||||
<td><input type="text" id="wc-tr-hostname" class="regular-text" placeholder="smtp.relay.com:587"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-tr-user">SMTP Username</label></th>
|
||||
<td><input type="text" id="wc-tr-user" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-tr-pass">SMTP Password</label></th>
|
||||
<td><input type="password" id="wc-tr-pass" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-tr-active">Active</label></th>
|
||||
<td><input type="checkbox" id="wc-tr-active" checked></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="button button-primary" id="wc-tr-save">Add Transport</button>
|
||||
<button class="button" id="wc-tr-cancel">Cancel</button>
|
||||
<span id="wc-tr-form-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wc-tr-table-wrap"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Logs ────────────────────────────────────────────────────────────
|
||||
|
||||
public function page_logs(): void {
|
||||
global $wpdb;
|
||||
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||
$log_types = [ 'postfix', 'dovecot', 'rspamd', 'ratelimit', 'api', 'acme', 'autodiscover', 'sogo', 'netfilter', 'watchdog' ];
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Server Logs</h1>
|
||||
|
||||
<div class="woocow-toolbar woocow-flex">
|
||||
<select id="wc-log-server" class="regular-text">
|
||||
<option value="">— Select server —</option>
|
||||
<?php foreach ( $servers as $s ) : ?>
|
||||
<option value="<?php echo esc_attr( $s->id ); ?>"><?php echo esc_html( $s->name ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select id="wc-log-type">
|
||||
<?php foreach ( $log_types as $t ) : ?>
|
||||
<option value="<?php echo esc_attr( $t ); ?>"><?php echo esc_html( ucfirst( $t ) ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button" id="wc-log-load" disabled>Load Logs</button>
|
||||
</div>
|
||||
|
||||
<div id="wc-log-wrap">
|
||||
<p class="description">Select a server and log type above.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── AJAX helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
private function verify(): void {
|
||||
@@ -632,4 +816,189 @@ class WooCow_Admin {
|
||||
|
||||
$this->json_ok();
|
||||
}
|
||||
|
||||
// ── AJAX: Domains ─────────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_admin_domain_add(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
$desc = sanitize_text_field( $_POST['description'] ?? '' );
|
||||
$mailboxes = absint( $_POST['mailboxes'] ?? 10 );
|
||||
$aliases = absint( $_POST['aliases'] ?? 400 );
|
||||
$quota = absint( $_POST['quota'] ?? 10240 );
|
||||
$defquota = absint( $_POST['defquota'] ?? 3072 );
|
||||
$dkim_size = absint( $_POST['dkim_size'] ?? 2048 );
|
||||
|
||||
if ( ! $domain ) {
|
||||
$this->json_err( 'Domain name is required.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
|
||||
// Add domain
|
||||
$result = $api->create_domain( [
|
||||
'domain' => $domain,
|
||||
'description' => $desc,
|
||||
'mailboxes' => $mailboxes,
|
||||
'aliases' => $aliases,
|
||||
'quota' => $quota,
|
||||
'defquota' => $defquota,
|
||||
'maxquota' => $quota,
|
||||
'active' => 1,
|
||||
'restart_sogo'=> 1,
|
||||
] );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to add domain.' );
|
||||
}
|
||||
|
||||
// Auto-generate DKIM
|
||||
$api->generate_dkim( $domain, 'dkim', $dkim_size );
|
||||
|
||||
$this->json_ok( [ 'domain' => $domain ] );
|
||||
}
|
||||
|
||||
public function ajax_woocow_admin_domain_dns(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$mail_host = parse_url( $server->url, PHP_URL_HOST );
|
||||
|
||||
// Fetch DKIM
|
||||
$dkim_result = $api->get_dkim( $domain );
|
||||
$dkim_txt = '';
|
||||
$dkim_sel = 'dkim';
|
||||
if ( $dkim_result['success'] && ! empty( $dkim_result['data']['dkim_txt'] ) ) {
|
||||
$dkim_txt = $dkim_result['data']['dkim_txt'];
|
||||
$dkim_sel = $dkim_result['data']['dkim_selector'] ?? 'dkim';
|
||||
}
|
||||
|
||||
$this->json_ok( [
|
||||
'domain' => $domain,
|
||||
'mail_host' => $mail_host,
|
||||
'dkim_sel' => $dkim_sel,
|
||||
'dkim_txt' => $dkim_txt,
|
||||
'records' => [
|
||||
[ 'type' => 'MX', 'host' => $domain, 'value' => $mail_host . '.', 'prio' => '10', 'ttl' => '3600' ],
|
||||
[ 'type' => 'TXT', 'host' => $domain, 'value' => 'v=spf1 mx ~all', 'prio' => '', 'ttl' => '3600', 'note' => 'SPF' ],
|
||||
[ 'type' => 'TXT', 'host' => '_dmarc.' . $domain, 'value' => 'v=DMARC1; p=quarantine; rua=mailto:postmaster@' . $domain, 'prio' => '', 'ttl' => '3600', 'note' => 'DMARC' ],
|
||||
[ 'type' => 'TXT', 'host' => $dkim_sel . '._domainkey.' . $domain, 'value' => $dkim_txt ?: '(generate DKIM first)', 'prio' => '', 'ttl' => '3600', 'note' => 'DKIM' ],
|
||||
[ 'type' => 'CNAME','host' => 'autoconfig.' . $domain, 'value' => $mail_host . '.', 'prio' => '', 'ttl' => '3600', 'note' => 'Autoconfig' ],
|
||||
[ 'type' => 'CNAME','host' => 'autodiscover.' . $domain, 'value' => $mail_host . '.', 'prio' => '', 'ttl' => '3600', 'note' => 'Autodiscover' ],
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
// ── AJAX: Transports ──────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_admin_relayhosts_list(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->get_relayhosts();
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to fetch transports.' );
|
||||
}
|
||||
|
||||
$this->json_ok( $result['data'] ?? [] );
|
||||
}
|
||||
|
||||
public function ajax_woocow_admin_relayhost_save(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$hostname = sanitize_text_field( $_POST['hostname'] ?? '' );
|
||||
$username = sanitize_text_field( $_POST['username'] ?? '' );
|
||||
$password = $_POST['password'] ?? '';
|
||||
$active = absint( $_POST['active'] ?? 1 );
|
||||
|
||||
if ( ! $hostname || ! $username ) {
|
||||
$this->json_err( 'Hostname and username are required.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->create_relayhost( compact( 'hostname', 'username', 'password', 'active' ) );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to add transport.' );
|
||||
}
|
||||
|
||||
$this->json_ok();
|
||||
}
|
||||
|
||||
public function ajax_woocow_admin_relayhost_delete(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$id = absint( $_POST['id'] ?? 0 );
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->delete_relayhost( $id );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to delete transport.' );
|
||||
}
|
||||
|
||||
$this->json_ok();
|
||||
}
|
||||
|
||||
// ── AJAX: Logs ────────────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_admin_logs(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$log_type = sanitize_key( $_POST['log_type'] ?? 'postfix' );
|
||||
|
||||
$allowed = [ 'postfix', 'dovecot', 'rspamd', 'ratelimit', 'api', 'acme', 'autodiscover', 'sogo', 'netfilter', 'watchdog' ];
|
||||
if ( ! in_array( $log_type, $allowed, true ) ) {
|
||||
$this->json_err( 'Invalid log type.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->request( 'GET', '/api/v1/get/logs/' . $log_type );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to fetch logs.' );
|
||||
}
|
||||
|
||||
$this->json_ok( $result['data'] ?? [] );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class WooCow_API {
|
||||
|
||||
// ── Core HTTP ────────────────────────────────────────────────────────────
|
||||
|
||||
private function request( string $method, string $endpoint, array $body = [] ): array {
|
||||
public function request( string $method, string $endpoint, array $body = [] ): array {
|
||||
$args = [
|
||||
'method' => strtoupper( $method ),
|
||||
'timeout' => $this->timeout,
|
||||
@@ -145,6 +145,44 @@ class WooCow_API {
|
||||
return $this->request( 'POST', '/api/v1/delete/domain-admin', [ 'items' => [ $username ] ] );
|
||||
}
|
||||
|
||||
// ── DKIM ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public function get_dkim( string $domain ): array {
|
||||
return $this->request( 'GET', '/api/v1/get/dkim/' . rawurlencode( $domain ) );
|
||||
}
|
||||
|
||||
public function generate_dkim( string $domain, string $selector = 'dkim', int $key_size = 2048 ): array {
|
||||
return $this->request( 'POST', '/api/v1/add/dkim', [
|
||||
'domains' => $domain,
|
||||
'dkim_selector' => $selector,
|
||||
'key_size' => $key_size,
|
||||
] );
|
||||
}
|
||||
|
||||
// ── Relayhosts ───────────────────────────────────────────────────────────
|
||||
|
||||
public function get_relayhosts(): array {
|
||||
return $this->request( 'GET', '/api/v1/get/relayhost/all' );
|
||||
}
|
||||
|
||||
public function create_relayhost( array $data ): array {
|
||||
return $this->request( 'POST', '/api/v1/add/relayhost', $data );
|
||||
}
|
||||
|
||||
public function delete_relayhost( int $id ): array {
|
||||
return $this->request( 'POST', '/api/v1/delete/relayhost', [ 'items' => [ $id ] ] );
|
||||
}
|
||||
|
||||
// ── Quarantine ───────────────────────────────────────────────────────────
|
||||
|
||||
public function get_quarantine(): array {
|
||||
return $this->request( 'GET', '/api/v1/get/quarantine/all' );
|
||||
}
|
||||
|
||||
public function delete_quarantine( int $id ): array {
|
||||
return $this->request( 'POST', '/api/v1/delete/quarantine', [ 'items' => [ $id ] ] );
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function get_webmail_url(): string {
|
||||
|
||||
BIN
languages/woocow-es_ES.mo
Normal file
BIN
languages/woocow-es_ES.mo
Normal file
Binary file not shown.
135
languages/woocow-es_ES.po
Normal file
135
languages/woocow-es_ES.po
Normal file
@@ -0,0 +1,135 @@
|
||||
# Spanish translation for WooCow
|
||||
# Copyright (C) 2025 WooCow
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WooCow 1.0.0\n"
|
||||
"PO-Revision-Date: 2025-01-01 00:00+0000\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Text-Domain: woocow\n"
|
||||
|
||||
#: woocow.php:37
|
||||
msgid "WooCow requires WooCommerce to be installed and active."
|
||||
msgstr "WooCow requiere que WooCommerce esté instalado y activo."
|
||||
|
||||
#: includes/class-woocow-account.php:51
|
||||
#: includes/class-woocow-pw-sync.php:30
|
||||
msgid "Email Hosting"
|
||||
msgstr "Alojamiento de correo electrónico"
|
||||
|
||||
#: includes/class-woocow-account.php:100
|
||||
msgid "Please log in to manage your email hosting."
|
||||
msgstr "Por favor, inicia sesión para gestionar tu alojamiento de correo electrónico."
|
||||
|
||||
#: includes/class-woocow-account.php:108
|
||||
msgid "You have no email domains assigned yet. Please contact support."
|
||||
msgstr "Aún no tienes dominios de correo electrónico asignados. Por favor, contacta con soporte."
|
||||
|
||||
#: includes/class-woocow-account.php:68
|
||||
#: includes/class-woocow-account.php:126
|
||||
msgid "Load Mailboxes"
|
||||
msgstr "Cargar buzones"
|
||||
|
||||
#: includes/class-woocow-account.php:69
|
||||
msgid "Refresh"
|
||||
msgstr "Actualizar"
|
||||
|
||||
#: includes/class-woocow-account.php:70
|
||||
msgid "Loading…"
|
||||
msgstr "Cargando…"
|
||||
|
||||
#: includes/class-woocow-account.php:71
|
||||
msgid "No mailboxes yet. Create one below."
|
||||
msgstr "Aún no hay buzones. Crea uno a continuación."
|
||||
|
||||
#: includes/class-woocow-account.php:72
|
||||
msgid "Change Password"
|
||||
msgstr "Cambiar contraseña"
|
||||
|
||||
#: includes/class-woocow-account.php:73
|
||||
msgid "Aliases & Forwarders"
|
||||
msgstr "Alias y reenvíos"
|
||||
|
||||
#: includes/class-woocow-account.php:74
|
||||
msgid "Spam Filter"
|
||||
msgstr "Filtro de spam"
|
||||
|
||||
#: includes/class-woocow-account.php:75
|
||||
msgid "Webmail"
|
||||
msgstr "Correo web"
|
||||
|
||||
#: includes/class-woocow-account.php:76
|
||||
msgid "Add Alias / Forwarder"
|
||||
msgstr "Añadir alias / reenvío"
|
||||
|
||||
#: includes/class-woocow-account.php:77
|
||||
msgid "No aliases for this domain yet."
|
||||
msgstr "Aún no hay alias para este dominio."
|
||||
|
||||
#: includes/class-woocow-account.php:78
|
||||
msgid "Add Mailbox"
|
||||
msgstr "Añadir buzón"
|
||||
|
||||
#: includes/class-woocow-account.php:79
|
||||
msgid "Create Mailbox"
|
||||
msgstr "Crear buzón"
|
||||
|
||||
#: includes/class-woocow-account.php:80
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: includes/class-woocow-account.php:81
|
||||
msgid "Save"
|
||||
msgstr "Guardar"
|
||||
|
||||
#: includes/class-woocow-account.php:82
|
||||
msgid "Delete"
|
||||
msgstr "Eliminar"
|
||||
|
||||
#: includes/class-woocow-account.php:83
|
||||
#: includes/class-woocow-account.php:127
|
||||
msgid "Quarantine"
|
||||
msgstr "Cuarentena"
|
||||
|
||||
#: includes/class-woocow-account.php:84
|
||||
msgid "No quarantined messages for this domain."
|
||||
msgstr "No hay mensajes en cuarentena para este dominio."
|
||||
|
||||
#: includes/class-woocow-account.php:85
|
||||
msgid "To release a message to your inbox, use the link in your quarantine notification email or via Webmail."
|
||||
msgstr "Para liberar un mensaje a tu bandeja de entrada, usa el enlace en el correo electrónico de notificación de cuarentena o a través del correo web."
|
||||
|
||||
#: includes/class-woocow-account.php:86
|
||||
msgid "Permanently delete this quarantined message?"
|
||||
msgstr "¿Eliminar permanentemente este mensaje en cuarentena?"
|
||||
|
||||
#: includes/class-woocow-account.php:87
|
||||
msgid "Spam Filter Threshold"
|
||||
msgstr "Umbral del filtro de spam"
|
||||
|
||||
#: includes/class-woocow-account.php:88
|
||||
msgid "Lower = stricter. Default is 5. Emails above this score go to spam/quarantine."
|
||||
msgstr "Menor = más estricto. El valor predeterminado es 5. Los correos con una puntuación superior van a spam/cuarentena."
|
||||
|
||||
#: includes/class-woocow-account.php:89
|
||||
msgid "Update Password"
|
||||
msgstr "Actualizar contraseña"
|
||||
|
||||
#: includes/class-woocow-account.php:90
|
||||
msgid "Passwords do not match or are empty."
|
||||
msgstr "Las contraseñas no coinciden o están vacías."
|
||||
|
||||
#: includes/class-woocow-account.php:91
|
||||
msgid "Unlimited"
|
||||
msgstr "Ilimitado"
|
||||
|
||||
#: includes/class-woocow-pw-sync.php:33
|
||||
msgid "Also update password for all my email mailboxes (only applies when changing password above)"
|
||||
msgstr "También actualizar la contraseña para todos mis buzones de correo electrónico (solo aplica al cambiar la contraseña arriba)"
|
||||
|
||||
#: includes/class-woocow-account.php:124
|
||||
msgid "Open Webmail"
|
||||
msgstr "Abrir correo web"
|
||||
BIN
languages/woocow-ro_RO.mo
Normal file
BIN
languages/woocow-ro_RO.mo
Normal file
Binary file not shown.
135
languages/woocow-ro_RO.po
Normal file
135
languages/woocow-ro_RO.po
Normal file
@@ -0,0 +1,135 @@
|
||||
# Romanian translation for WooCow
|
||||
# Copyright (C) 2025 WooCow
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WooCow 1.0.0\n"
|
||||
"PO-Revision-Date: 2025-01-01 00:00+0000\n"
|
||||
"Language: ro_RO\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
|
||||
"X-Text-Domain: woocow\n"
|
||||
|
||||
#: woocow.php:37
|
||||
msgid "WooCow requires WooCommerce to be installed and active."
|
||||
msgstr "WooCow necesită ca WooCommerce să fie instalat și activ."
|
||||
|
||||
#: includes/class-woocow-account.php:51
|
||||
#: includes/class-woocow-pw-sync.php:30
|
||||
msgid "Email Hosting"
|
||||
msgstr "Găzduire email"
|
||||
|
||||
#: includes/class-woocow-account.php:100
|
||||
msgid "Please log in to manage your email hosting."
|
||||
msgstr "Te rugăm să te autentifici pentru a gestiona găzduirea de email."
|
||||
|
||||
#: includes/class-woocow-account.php:108
|
||||
msgid "You have no email domains assigned yet. Please contact support."
|
||||
msgstr "Nu ai niciun domeniu de email atribuit încă. Te rugăm să contactezi suportul."
|
||||
|
||||
#: includes/class-woocow-account.php:68
|
||||
#: includes/class-woocow-account.php:126
|
||||
msgid "Load Mailboxes"
|
||||
msgstr "Încarcă cutiile poștale"
|
||||
|
||||
#: includes/class-woocow-account.php:69
|
||||
msgid "Refresh"
|
||||
msgstr "Reîmprospătează"
|
||||
|
||||
#: includes/class-woocow-account.php:70
|
||||
msgid "Loading…"
|
||||
msgstr "Se încarcă…"
|
||||
|
||||
#: includes/class-woocow-account.php:71
|
||||
msgid "No mailboxes yet. Create one below."
|
||||
msgstr "Nicio cutie poștală încă. Creează una mai jos."
|
||||
|
||||
#: includes/class-woocow-account.php:72
|
||||
msgid "Change Password"
|
||||
msgstr "Schimbă parola"
|
||||
|
||||
#: includes/class-woocow-account.php:73
|
||||
msgid "Aliases & Forwarders"
|
||||
msgstr "Aliasuri și redirecționări"
|
||||
|
||||
#: includes/class-woocow-account.php:74
|
||||
msgid "Spam Filter"
|
||||
msgstr "Filtru spam"
|
||||
|
||||
#: includes/class-woocow-account.php:75
|
||||
msgid "Webmail"
|
||||
msgstr "Webmail"
|
||||
|
||||
#: includes/class-woocow-account.php:76
|
||||
msgid "Add Alias / Forwarder"
|
||||
msgstr "Adaugă alias / redirecționare"
|
||||
|
||||
#: includes/class-woocow-account.php:77
|
||||
msgid "No aliases for this domain yet."
|
||||
msgstr "Niciun alias pentru acest domeniu încă."
|
||||
|
||||
#: includes/class-woocow-account.php:78
|
||||
msgid "Add Mailbox"
|
||||
msgstr "Adaugă cutie poștală"
|
||||
|
||||
#: includes/class-woocow-account.php:79
|
||||
msgid "Create Mailbox"
|
||||
msgstr "Creează cutie poștală"
|
||||
|
||||
#: includes/class-woocow-account.php:80
|
||||
msgid "Cancel"
|
||||
msgstr "Anulează"
|
||||
|
||||
#: includes/class-woocow-account.php:81
|
||||
msgid "Save"
|
||||
msgstr "Salvează"
|
||||
|
||||
#: includes/class-woocow-account.php:82
|
||||
msgid "Delete"
|
||||
msgstr "Șterge"
|
||||
|
||||
#: includes/class-woocow-account.php:83
|
||||
#: includes/class-woocow-account.php:127
|
||||
msgid "Quarantine"
|
||||
msgstr "Carantină"
|
||||
|
||||
#: includes/class-woocow-account.php:84
|
||||
msgid "No quarantined messages for this domain."
|
||||
msgstr "Niciun mesaj în carantină pentru acest domeniu."
|
||||
|
||||
#: includes/class-woocow-account.php:85
|
||||
msgid "To release a message to your inbox, use the link in your quarantine notification email or via Webmail."
|
||||
msgstr "Pentru a elibera un mesaj în căsuța de intrare, folosește linkul din emailul de notificare privind carantina sau prin Webmail."
|
||||
|
||||
#: includes/class-woocow-account.php:86
|
||||
msgid "Permanently delete this quarantined message?"
|
||||
msgstr "Ștergi definitiv acest mesaj din carantină?"
|
||||
|
||||
#: includes/class-woocow-account.php:87
|
||||
msgid "Spam Filter Threshold"
|
||||
msgstr "Prag filtru spam"
|
||||
|
||||
#: includes/class-woocow-account.php:88
|
||||
msgid "Lower = stricter. Default is 5. Emails above this score go to spam/quarantine."
|
||||
msgstr "Mai mic = mai strict. Implicit este 5. Emailurile cu un scor mai mare ajung în spam/carantină."
|
||||
|
||||
#: includes/class-woocow-account.php:89
|
||||
msgid "Update Password"
|
||||
msgstr "Actualizează parola"
|
||||
|
||||
#: includes/class-woocow-account.php:90
|
||||
msgid "Passwords do not match or are empty."
|
||||
msgstr "Parolele nu se potrivesc sau sunt goale."
|
||||
|
||||
#: includes/class-woocow-account.php:91
|
||||
msgid "Unlimited"
|
||||
msgstr "Nelimitat"
|
||||
|
||||
#: includes/class-woocow-pw-sync.php:33
|
||||
msgid "Also update password for all my email mailboxes (only applies when changing password above)"
|
||||
msgstr "Actualizează și parola pentru toate cutiile mele poștale de email (se aplică doar la schimbarea parolei de mai sus)"
|
||||
|
||||
#: includes/class-woocow-account.php:124
|
||||
msgid "Open Webmail"
|
||||
msgstr "Deschide Webmail"
|
||||
@@ -25,6 +25,10 @@ require_once WOOCOW_PLUGIN_DIR . 'includes/class-woocow-pw-sync.php';
|
||||
|
||||
register_activation_hook( __FILE__, [ 'WooCow_Installer', 'install' ] );
|
||||
|
||||
add_action( 'init', function () {
|
||||
load_plugin_textdomain( 'woocow', false, dirname( plugin_basename( WOOCOW_PLUGIN_FILE ) ) . '/languages' );
|
||||
} );
|
||||
|
||||
add_action( 'plugins_loaded', function () {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
add_action( 'admin_notices', function () {
|
||||
|
||||
Reference in New Issue
Block a user