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

@@ -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; }
}

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);

View File

@@ -324,12 +324,18 @@
<td>${max}</td>
<td>${m.active == 1 ? '✓' : ''}</td>
<td class="woocow-actions">
<button class="button button-small wc-mb-reset-pw"
data-email="${esc(m.username)}">Reset PW</button>
<button class="button button-small wc-mb-set-quota"
data-email="${esc(m.username)}" data-quota="${quotaMB}">Set Quota</button>
<button class="button button-small wc-mb-del"
data-email="${esc(m.username)}" style="color:#a00">Delete</button>
<button class="button button-small woocow-icon-btn wc-mb-reset-pw"
title="Reset Password" data-email="${esc(m.username)}">
<span class="dashicons dashicons-lock"></span>
</button>
<button class="button button-small woocow-icon-btn wc-mb-set-quota"
title="Set Quota" data-email="${esc(m.username)}" data-quota="${quotaMB}">
<span class="dashicons dashicons-chart-bar"></span>
</button>
<button class="button button-small woocow-icon-btn wc-mb-del"
title="Delete" data-email="${esc(m.username)}" style="color:#a00">
<span class="dashicons dashicons-trash"></span>
</button>
</td>
</tr>`;
});
@@ -459,9 +465,236 @@
}
function formatMB(bytes) {
if (bytes === 0 || bytes === '0') return '∞';
if (!bytes) return '0 MB';
const mb = bytes / 1024 / 1024;
return mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB';
}
// ── Domains Page ──────────────────────────────────────────────────────────
if ($('#wc-dom-server').length) {
let domServerId = null;
let domServerUrl = null;
$('#wc-dom-server').on('change', function () {
domServerId = $(this).val();
domServerUrl = $(this).find('option:selected').data('url');
$('#wc-dom-load').prop('disabled', !domServerId);
$('#wc-dom-add-btn, #wc-dom-table-wrap').hide();
});
const loadDomains = () => {
ajax('woocow_server_domains', { server_id: domServerId }).done(res => {
if (!res.success) {
notice($('#wc-dom-notices'), 'error', res.data);
return;
}
const domains = res.data;
if (!domains.length) {
$('#wc-dom-table-wrap').html('<p>No domains on this server yet.</p>').show();
} else {
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr><th>Domain</th><th>Active</th><th>Actions</th></tr></thead><tbody>`;
domains.forEach(d => {
html += `<tr>
<td><strong>${esc(d.domain)}</strong></td>
<td>${d.active == 1 ? '<span class="woocow-badge woocow-badge-green">Active</span>' : '<span class="woocow-badge woocow-badge-grey">Inactive</span>'}</td>
<td>
<button class="button button-small wc-dom-dns" data-domain="${esc(d.domain)}">
<span class="dashicons dashicons-admin-site"></span> DNS Records
</button>
</td>
</tr>`;
});
html += '</tbody></table>';
$('#wc-dom-table-wrap').html(html).show();
}
$('#wc-dom-add-btn').show();
});
};
$('#wc-dom-load').on('click', loadDomains);
$('#wc-dom-add-btn').on('click', () => {
$('#wc-dom-name, #wc-dom-desc').val('');
$('#wc-dom-form-notice').text('');
$('#wc-dom-form').slideDown();
});
$('#wc-dom-cancel').on('click', () => $('#wc-dom-form').slideUp());
$('#wc-dom-save').on('click', () => {
const $note = $('#wc-dom-form-notice').text('Adding domain…');
const data = {
server_id: domServerId,
domain: $('#wc-dom-name').val().trim(),
description: $('#wc-dom-desc').val().trim(),
mailboxes: $('#wc-dom-mailboxes').val(),
aliases: $('#wc-dom-aliases').val(),
quota: $('#wc-dom-quota').val(),
defquota: $('#wc-dom-defquota').val(),
dkim_size: $('#wc-dom-dkim-size').val(),
};
if (!data.domain) { $note.html('<span style="color:red">Domain name required.</span>'); return; }
ajax('woocow_admin_domain_add', data).done(res => {
if (res.success) {
$note.html('<span style="color:green">✓ Domain added with DKIM generated!</span>');
$('#wc-dom-form').slideUp();
loadDomains();
} else {
$note.html(`<span style="color:red">${esc(res.data)}</span>`);
}
});
});
// DNS Records panel
$(document).on('click', '.wc-dom-dns', function () {
const domain = $(this).data('domain');
$('#wc-dom-dns-domain').text(domain);
$('#wc-dom-dns-content').html('<p>Loading…</p>');
$('#wc-dom-dns-panel').show();
$('html,body').animate({ scrollTop: $('#wc-dom-dns-panel').offset().top - 40 }, 300);
ajax('woocow_admin_domain_dns', { server_id: domServerId, domain }).done(res => {
if (!res.success) {
$('#wc-dom-dns-content').html(`<p style="color:red">${esc(res.data)}</p>`);
return;
}
const d = res.data;
let html = `<table class="wp-list-table widefat fixed striped woocow-table woocow-dns-table">
<thead><tr><th>Type</th><th>Host / Name</th><th>Value</th><th>Priority</th><th>TTL</th><th>Note</th><th></th></tr></thead><tbody>`;
d.records.forEach(r => {
html += `<tr>
<td><code>${esc(r.type)}</code></td>
<td class="woocow-dns-host"><code>${esc(r.host)}</code></td>
<td class="woocow-dns-val"><code class="woocow-dns-value">${esc(r.value)}</code></td>
<td>${esc(r.prio)}</td>
<td>${esc(r.ttl)}</td>
<td><em>${esc(r.note || '')}</em></td>
<td><button class="button button-small wc-copy-dns" data-val="${esc(r.value)}">Copy</button></td>
</tr>`;
});
html += '</tbody></table>';
if (!d.dkim_txt) {
html += `<p class="description" style="color:orange">⚠ DKIM key not yet generated for this domain. Add the domain first, then view DNS records again.</p>`;
}
$('#wc-dom-dns-content').html(html);
});
});
$(document).on('click', '.wc-copy-dns', function () {
const val = $(this).data('val');
navigator.clipboard.writeText(val).then(() => {
$(this).text('Copied!');
setTimeout(() => $(this).text('Copy'), 1500);
});
});
$('#wc-dom-dns-close').on('click', () => $('#wc-dom-dns-panel').hide());
}
// ── Transports Page ───────────────────────────────────────────────────────
if ($('#wc-tr-server').length) {
let trServerId = null;
$('#wc-tr-server').on('change', function () {
trServerId = $(this).val();
$('#wc-tr-load').prop('disabled', !trServerId);
});
const loadTransports = () => {
ajax('woocow_admin_relayhosts_list', { server_id: trServerId }).done(res => {
if (!res.success) { notice($('#wc-tr-notices'), 'error', res.data); return; }
const rows = res.data;
if (!Array.isArray(rows) || !rows.length) {
$('#wc-tr-table-wrap').html('<p>No transports configured on this server.</p>');
} else {
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr><th>Hostname:Port</th><th>Username</th><th>Used by Domains</th><th>Active</th><th>Actions</th></tr></thead><tbody>`;
rows.forEach(r => {
html += `<tr>
<td><code>${esc(r.hostname)}</code></td>
<td>${esc(r.username)}</td>
<td>${esc(r.used_by_domains || '—')}</td>
<td>${r.active == 1 ? '✓' : ''}</td>
<td><button class="button button-small woocow-icon-btn wc-tr-del" title="Delete" data-id="${r.id}" style="color:#a00">
<span class="dashicons dashicons-trash"></span>
</button></td>
</tr>`;
});
html += '</tbody></table>';
$('#wc-tr-table-wrap').html(html);
}
$('#wc-tr-add-btn').show();
});
};
$('#wc-tr-load').on('click', loadTransports);
$('#wc-tr-add-btn').on('click', () => { $('#wc-tr-hostname,#wc-tr-user,#wc-tr-pass').val(''); $('#wc-tr-form').slideDown(); });
$('#wc-tr-cancel').on('click', () => $('#wc-tr-form').slideUp());
$('#wc-tr-save').on('click', () => {
const $note = $('#wc-tr-form-notice').text('Saving…');
ajax('woocow_admin_relayhost_save', {
server_id: trServerId,
hostname: $('#wc-tr-hostname').val().trim(),
username: $('#wc-tr-user').val().trim(),
password: $('#wc-tr-pass').val(),
active: $('#wc-tr-active').is(':checked') ? 1 : 0,
}).done(res => {
if (res.success) { $note.html('<span style="color:green">✓ Transport added.</span>'); $('#wc-tr-form').slideUp(); loadTransports(); }
else $note.html(`<span style="color:red">${esc(res.data)}</span>`);
});
});
$(document).on('click', '.wc-tr-del', function () {
if (!confirm('Delete this transport?')) return;
ajax('woocow_admin_relayhost_delete', { server_id: trServerId, id: $(this).data('id') }).done(res => {
if (res.success) loadTransports();
else notice($('#wc-tr-notices'), 'error', res.data);
});
});
}
// ── Logs Page ─────────────────────────────────────────────────────────────
if ($('#wc-log-server').length) {
$('#wc-log-server').on('change', function () {
$('#wc-log-load').prop('disabled', !$(this).val());
});
$('#wc-log-load').on('click', () => {
const sid = $('#wc-log-server').val();
const type = $('#wc-log-type').val();
$('#wc-log-wrap').html('<p>Loading…</p>');
ajax('woocow_admin_logs', { server_id: sid, log_type: type }).done(res => {
if (!res.success) {
$('#wc-log-wrap').html(`<p style="color:red">${esc(res.data)}</p>`);
return;
}
const entries = Array.isArray(res.data) ? res.data : Object.values(res.data);
if (!entries.length) {
$('#wc-log-wrap').html('<p>No log entries.</p>');
return;
}
// Render as a scrollable pre block
const text = entries.map(e => {
if (typeof e === 'string') return e;
if (e.time && e.message) return `[${e.time}] ${e.message}`;
return JSON.stringify(e);
}).join('\n');
$('#wc-log-wrap').html(`
<div class="woocow-log-toolbar">
<strong>${esc(type.charAt(0).toUpperCase() + type.slice(1))} log</strong>
<span style="color:#666;font-size:12px">${entries.length} entries</span>
</div>
<pre class="woocow-log-pre">${esc(text)}</pre>
`);
});
});
}
})(jQuery);

View File

@@ -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 ───────────────────────────────────────────────
/**

View File

@@ -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 &amp; 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'] ?? [] );
}
}

View File

@@ -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

Binary file not shown.

135
languages/woocow-es_ES.po Normal file
View 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

Binary file not shown.

135
languages/woocow-ro_RO.po Normal file
View 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"

View File

@@ -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 () {