@@ -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('
Loading quarantine…
');
+ $wrap.slideDown();
+
+ ajax('woocow_acct_quarantine', { server_id: sid, domain }).done(res => {
+ if (!res.success) { $list.html(`
${esc(res.data)}
`); return; }
+ const msgs = res.data;
+ if (!msgs.length) { $list.html('
No quarantined messages for this domain.
'); return; }
+
+ let html = `
+ | From | To | Subject | Score | Date | |
`;
+ 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 += `
+ | ${esc(m.sender)} |
+ ${esc(m.rcpt)} |
+ ${esc(m.subject)}${virus} |
+ ${score} |
+ ${esc(date)} |
+ |
+
`;
+ });
+ html += '
';
+ html += '
To release a message to your inbox, use the link in your quarantine notification email or via Webmail.
';
+ $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(`
+
+
Spam Filter Threshold
+
Lower = stricter. Default is 5. Emails above this score go to spam/quarantine.
+
+
+ ${parseFloat(score).toFixed(1)}
+
+
+
+
+ `);
+
+ $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('
✓ Saved');
+ else $note.html(`
${esc(res.data)}`);
+ });
+ });
+
})(jQuery);
diff --git a/assets/js/woocow-admin.js b/assets/js/woocow-admin.js
index b428246..719c7a5 100644
--- a/assets/js/woocow-admin.js
+++ b/assets/js/woocow-admin.js
@@ -324,12 +324,18 @@
${max} |
${m.active == 1 ? '✓' : '–'} |
-
-
-
+
+
+
|
`;
});
@@ -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('
No domains on this server yet.
').show();
+ } else {
+ let html = `
+ | Domain | Active | Actions |
`;
+ domains.forEach(d => {
+ html += `
+ | ${esc(d.domain)} |
+ ${d.active == 1 ? 'Active' : 'Inactive'} |
+
+
+ |
+
`;
+ });
+ html += '
';
+ $('#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('
Domain name required.'); return; }
+
+ ajax('woocow_admin_domain_add', data).done(res => {
+ if (res.success) {
+ $note.html('
✓ Domain added with DKIM generated!');
+ $('#wc-dom-form').slideUp();
+ loadDomains();
+ } else {
+ $note.html(`
${esc(res.data)}`);
+ }
+ });
+ });
+
+ // 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('
Loading…
');
+ $('#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(`
${esc(res.data)}
`);
+ return;
+ }
+ const d = res.data;
+ let html = `
+ | Type | Host / Name | Value | Priority | TTL | Note | |
`;
+ d.records.forEach(r => {
+ html += `
+ ${esc(r.type)} |
+ ${esc(r.host)} |
+ ${esc(r.value)} |
+ ${esc(r.prio)} |
+ ${esc(r.ttl)} |
+ ${esc(r.note || '')} |
+ |
+
`;
+ });
+ html += '
';
+ if (!d.dkim_txt) {
+ html += `
⚠ DKIM key not yet generated for this domain. Add the domain first, then view DNS records again.
`;
+ }
+ $('#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('
No transports configured on this server.
');
+ } else {
+ let html = `
+ | Hostname:Port | Username | Used by Domains | Active | Actions |
`;
+ rows.forEach(r => {
+ html += `
+ ${esc(r.hostname)} |
+ ${esc(r.username)} |
+ ${esc(r.used_by_domains || '—')} |
+ ${r.active == 1 ? '✓' : '–'} |
+ |
+
`;
+ });
+ html += '
';
+ $('#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('
✓ Transport added.'); $('#wc-tr-form').slideUp(); loadTransports(); }
+ else $note.html(`
${esc(res.data)}`);
+ });
+ });
+
+ $(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('
Loading…
');
+
+ ajax('woocow_admin_logs', { server_id: sid, log_type: type }).done(res => {
+ if (!res.success) {
+ $('#wc-log-wrap').html(`
${esc(res.data)}
`);
+ return;
+ }
+ const entries = Array.isArray(res.data) ? res.data : Object.values(res.data);
+ if (!entries.length) {
+ $('#wc-log-wrap').html('
No log entries.
');
+ 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(`
+
+ ${esc(type.charAt(0).toUpperCase() + type.slice(1))} log
+ ${entries.length} entries
+
+
${esc(text)}
+ `);
+ });
+ });
+ }
+
})(jQuery);
diff --git a/includes/class-woocow-account.php b/includes/class-woocow-account.php
index 0d34e73..00f280c 100644
--- a/includes/class-woocow-account.php
+++ b/includes/class-woocow-account.php
@@ -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 {
Open Webmail
-
+
+
+
@@ -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 ───────────────────────────────────────────────
/**
diff --git a/includes/class-woocow-admin.php b/includes/class-woocow-admin.php
index 9e58fe0..0cd6894 100644
--- a/includes/class-woocow-admin.php
+++ b/includes/class-woocow-admin.php
@@ -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 {
get_results( "SELECT id, name, url FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
+ ?>
+
+
WooCow – Domains
+
Add and manage domains on your Mailcow servers. After adding a domain, DNS records (including DKIM) are generated automatically.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
DNS Records
+
+
+
Add these records to your DNS provider for .
+
+
+
+ get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
+ ?>
+
+
WooCow – Sender-Dependent Transports
+
Configure relay hosts for outbound mail delivery. Each transport can be associated with one or more domains in Mailcow.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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' ];
+ ?>
+
+
WooCow – Server Logs
+
+
+
+
+
+
+
+
+
Select a server and log type above.
+
+
+ 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'] ?? [] );
+ }
}
diff --git a/includes/class-woocow-api.php b/includes/class-woocow-api.php
index fc0f1d9..dc628ff 100644
--- a/includes/class-woocow-api.php
+++ b/includes/class-woocow-api.php
@@ -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 {
diff --git a/languages/woocow-es_ES.mo b/languages/woocow-es_ES.mo
new file mode 100644
index 0000000..514a0e5
Binary files /dev/null and b/languages/woocow-es_ES.mo differ
diff --git a/languages/woocow-es_ES.po b/languages/woocow-es_ES.po
new file mode 100644
index 0000000..f6adc6f
--- /dev/null
+++ b/languages/woocow-es_ES.po
@@ -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"
diff --git a/languages/woocow-ro_RO.mo b/languages/woocow-ro_RO.mo
new file mode 100644
index 0000000..015f525
Binary files /dev/null and b/languages/woocow-ro_RO.mo differ
diff --git a/languages/woocow-ro_RO.po b/languages/woocow-ro_RO.po
new file mode 100644
index 0000000..6235617
--- /dev/null
+++ b/languages/woocow-ro_RO.po
@@ -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"
diff --git a/woocow.php b/woocow.php
index 0f9f34a..053b61a 100644
--- a/woocow.php
+++ b/woocow.php
@@ -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 () {