Files
WooCow/includes/class-woocow-admin.php
Malin dbe4abccf7 feat: domain edit/delete, fix logs, add admin quarantine with block
- Add ajax_woocow_admin_domain_edit and _delete PHP handlers
- Domain table: richer columns (mailboxes used/limit, quota), icon buttons
- Edit domain modal: pre-populates fields, loads relayhosts for transport select
- Fix logs: correct Mailcow API slugs (rspamd-history, ratelimited) and add /{count} suffix to endpoint
- Add admin Quarantine submenu: view all quarantined messages, delete, blacklist sender via domain policy
- Add domain policy methods to API class (add/delete/get_bl)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:53:11 +01:00

1290 lines
56 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* WooCow Admin menu pages + AJAX handlers for the backend.
*/
defined( 'ABSPATH' ) || exit;
class WooCow_Admin {
public function __construct() {
add_action( 'admin_menu', [ $this, 'register_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
$ajax_actions = [
'woocow_servers_list',
'woocow_server_save',
'woocow_server_delete',
'woocow_server_test',
'woocow_server_domains',
'woocow_assignments_list',
'woocow_assignment_save',
'woocow_assignment_delete',
'woocow_customers_search',
'woocow_admin_mailboxes',
'woocow_admin_mailbox_create',
'woocow_admin_mailbox_delete',
'woocow_admin_mailbox_edit',
'woocow_admin_domain_add',
'woocow_admin_domain_dns',
'woocow_admin_domain_edit',
'woocow_admin_domain_delete',
'woocow_admin_relayhosts_list',
'woocow_admin_relayhost_save',
'woocow_admin_relayhost_delete',
'woocow_admin_logs',
'woocow_admin_quarantine',
'woocow_admin_quarantine_delete',
'woocow_admin_quarantine_block',
];
foreach ( $ajax_actions as $action ) {
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
}
}
// ── Menu ─────────────────────────────────────────────────────────────────
public function register_menu(): void {
add_menu_page(
'WooCow',
'WooCow',
'manage_woocommerce',
'woocow',
[ $this, 'page_dashboard' ],
'dashicons-email-alt2',
56
);
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' ] );
add_submenu_page( 'woocow', 'Quarantine', 'Quarantine', 'manage_woocommerce', 'woocow-quarantine', [ $this, 'page_quarantine' ] );
}
public function enqueue_assets( string $hook ): void {
if ( strpos( $hook, 'woocow' ) === false ) {
return;
}
wp_enqueue_style( 'woocow-admin', WOOCOW_PLUGIN_URL . 'assets/css/woocow.css', [], WOOCOW_VERSION );
wp_enqueue_script( 'woocow-admin', WOOCOW_PLUGIN_URL . 'assets/js/woocow-admin.js', [ 'jquery' ], WOOCOW_VERSION, true );
wp_localize_script( 'woocow-admin', 'woocow', [
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocow_admin' ),
] );
}
// ── Page: Dashboard ──────────────────────────────────────────────────────
public function page_dashboard(): void {
global $wpdb;
$servers = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocow_servers WHERE active=1" );
$assignments = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocow_assignments" );
$customers = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT customer_id) FROM {$wpdb->prefix}woocow_assignments" );
?>
<div class="wrap woocow-wrap">
<h1>WooCow <span class="woocow-version">v<?php echo esc_html( WOOCOW_VERSION ); ?></span></h1>
<div class="woocow-dashboard-cards">
<div class="woocow-card">
<span class="woocow-card-number"><?php echo esc_html( $servers ); ?></span>
<span class="woocow-card-label">Active Servers</span>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-servers' ) ); ?>" class="button button-secondary">Manage</a>
</div>
<div class="woocow-card">
<span class="woocow-card-number"><?php echo esc_html( $customers ); ?></span>
<span class="woocow-card-label">Customers with Email</span>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-assignments' ) ); ?>" class="button button-secondary">Manage</a>
</div>
<div class="woocow-card">
<span class="woocow-card-number"><?php echo esc_html( $assignments ); ?></span>
<span class="woocow-card-label">Domain Assignments</span>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-mailboxes' ) ); ?>" class="button button-secondary">View Mailboxes</a>
</div>
</div>
</div>
<?php
}
// ── Page: Servers ────────────────────────────────────────────────────────
public function page_servers(): void {
?>
<div class="wrap woocow-wrap">
<h1>WooCow Servers</h1>
<p>Add your Mailcow server instances here. The API key must be a read-write key from <strong>Configuration &rarr; Access &rarr; Edit administrator details &rarr; API</strong>.</p>
<div class="woocow-toolbar">
<button class="button button-primary" id="wc-add-server">+ Add Server</button>
</div>
<div id="wc-notices"></div>
<!-- Add / Edit form (hidden by default) -->
<div id="wc-server-form" class="woocow-card woocow-form" style="display:none">
<h3 id="wc-server-form-title">Add Server</h3>
<input type="hidden" id="wc-server-id" value="">
<table class="form-table">
<tr>
<th><label for="wc-server-name">Name</label></th>
<td><input type="text" id="wc-server-name" class="regular-text" placeholder="My Mailcow Server"></td>
</tr>
<tr>
<th><label for="wc-server-url">Server URL</label></th>
<td><input type="url" id="wc-server-url" class="regular-text" placeholder="https://mail.example.com"></td>
</tr>
<tr>
<th><label for="wc-server-key">API Key</label></th>
<td><input type="text" id="wc-server-key" class="regular-text" placeholder="Your read-write API key"></td>
</tr>
<tr>
<th><label for="wc-server-active">Active</label></th>
<td><input type="checkbox" id="wc-server-active" checked></td>
</tr>
</table>
<div class="woocow-form-actions">
<button class="button button-primary" id="wc-server-save">Save Server</button>
<button class="button" id="wc-server-test">Test Connection</button>
<button class="button" id="wc-server-cancel">Cancel</button>
<span id="wc-server-test-result"></span>
</div>
</div>
<!-- Servers table -->
<div id="wc-servers-table-wrap">
<p id="wc-servers-loading">Loading servers…</p>
</div>
</div>
<?php
}
// ── Page: Assignments ────────────────────────────────────────────────────
public function page_assignments(): void {
?>
<div class="wrap woocow-wrap">
<h1>WooCow Domain Assignments</h1>
<p>Assign one or more Mailcow domains to a WooCommerce customer. The customer can then manage mailboxes for those domains from <em>My Account &rarr; Email Hosting</em>.</p>
<div class="woocow-card woocow-form" id="wc-assign-form">
<h3>Assign Domain to Customer</h3>
<table class="form-table">
<tr>
<th><label>Customer</label></th>
<td>
<input type="text" id="wc-cust-search" class="regular-text" placeholder="Search by name or email…" autocomplete="off">
<div id="wc-cust-results" class="woocow-autocomplete"></div>
<input type="hidden" id="wc-cust-id">
<span id="wc-cust-selected" class="woocow-selected-badge"></span>
</td>
</tr>
<tr>
<th><label for="wc-assign-server">Server</label></th>
<td>
<select id="wc-assign-server" class="regular-text">
<option value="">— Select a server —</option>
</select>
</td>
</tr>
<tr id="wc-domain-row" style="display:none">
<th><label for="wc-assign-domain">Domain</label></th>
<td>
<select id="wc-assign-domain" class="regular-text">
<option value="">— Loading domains —</option>
</select>
</td>
</tr>
</table>
<div class="woocow-form-actions">
<button class="button button-primary" id="wc-assign-save">Assign Domain</button>
</div>
<div id="wc-assign-notice"></div>
</div>
<h2>Current Assignments</h2>
<div id="wc-assignments-loading">Loading…</div>
<div id="wc-assignments-table-wrap"></div>
</div>
<?php
}
// ── Page: Mailboxes ──────────────────────────────────────────────────────
public function page_mailboxes(): 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 Mailboxes</h1>
<div class="woocow-toolbar woocow-flex">
<select id="wc-mb-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-mb-domain" class="regular-text" style="display:none">
<option value="">— Select domain —</option>
</select>
<button class="button" id="wc-mb-load" disabled>Load Mailboxes</button>
<button class="button button-primary" id="wc-mb-create" style="display:none">+ Create Mailbox</button>
</div>
<div id="wc-mb-notices"></div>
<div id="wc-mb-table-wrap"></div>
<!-- Edit Mailbox Modal (password / quota) -->
<div id="wc-mb-edit-modal" class="woocow-modal" style="display:none">
<div class="woocow-modal-box">
<h3 id="wc-mb-edit-title">Edit Mailbox</h3>
<p id="wc-mb-edit-subtitle" class="woocow-modal-subtitle"></p>
<div id="wc-mb-edit-pw-section" style="display:none">
<table class="form-table">
<tr>
<th><label for="wc-mb-edit-pass">New Password</label></th>
<td><input type="password" id="wc-mb-edit-pass" class="regular-text" autocomplete="new-password"></td>
</tr>
<tr>
<th><label for="wc-mb-edit-pass2">Confirm Password</label></th>
<td><input type="password" id="wc-mb-edit-pass2" class="regular-text" autocomplete="new-password"></td>
</tr>
</table>
</div>
<div id="wc-mb-edit-quota-section" style="display:none">
<table class="form-table">
<tr>
<th><label for="wc-mb-edit-quota">Max Quota (MB)</label></th>
<td>
<input type="number" id="wc-mb-edit-quota" class="small-text" min="1" step="1">
<p class="description">Current usage is shown in the mailbox list. 1024 MB = 1 GB.</p>
</td>
</tr>
</table>
</div>
<div class="woocow-modal-actions">
<button class="button button-primary" id="wc-mb-edit-save">Save</button>
<button class="button" id="wc-mb-edit-cancel">Cancel</button>
<span id="wc-mb-edit-notice"></span>
</div>
</div>
</div>
<!-- Create Mailbox Modal -->
<div id="wc-mb-modal" class="woocow-modal" style="display:none">
<div class="woocow-modal-box">
<h3>Create Mailbox</h3>
<table class="form-table">
<tr>
<th><label for="wc-mb-local">Local Part</label></th>
<td>
<div class="woocow-flex-inline">
<input type="text" id="wc-mb-local" placeholder="user">
<span class="woocow-at">@</span>
<span id="wc-mb-domain-label" class="woocow-domain-label"></span>
</div>
</td>
</tr>
<tr>
<th><label for="wc-mb-fullname">Full Name</label></th>
<td><input type="text" id="wc-mb-fullname" class="regular-text" placeholder="Jane Doe"></td>
</tr>
<tr>
<th><label for="wc-mb-pass">Password</label></th>
<td><input type="password" id="wc-mb-pass" class="regular-text"></td>
</tr>
<tr>
<th><label for="wc-mb-pass2">Confirm Password</label></th>
<td><input type="password" id="wc-mb-pass2" class="regular-text"></td>
</tr>
<tr>
<th><label for="wc-mb-quota">Quota (MB)</label></th>
<td><input type="number" id="wc-mb-quota" value="1024" min="1"></td>
</tr>
</table>
<div class="woocow-modal-actions">
<button class="button button-primary" id="wc-mb-modal-save">Create</button>
<button class="button" id="wc-mb-modal-cancel">Cancel</button>
<span id="wc-mb-modal-notice"></span>
</div>
</div>
</div>
</div>
<?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>
<!-- Edit Domain Modal -->
<div id="wc-dom-edit-modal" class="woocow-modal" style="display:none">
<div class="woocow-modal-box" style="max-width:600px">
<h3>Edit Domain: <span id="wc-dom-edit-name"></span></h3>
<input type="hidden" id="wc-dom-edit-domain">
<table class="form-table">
<tr>
<th><label for="wc-dom-edit-desc">Description</label></th>
<td><input type="text" id="wc-dom-edit-desc" class="regular-text"></td>
</tr>
<tr>
<th><label for="wc-dom-edit-mboxes">Max Mailboxes</label></th>
<td><input type="number" id="wc-dom-edit-mboxes" class="small-text" min="1"></td>
</tr>
<tr>
<th><label for="wc-dom-edit-aliases">Max Aliases</label></th>
<td><input type="number" id="wc-dom-edit-aliases" class="small-text" min="0"></td>
</tr>
<tr>
<th><label for="wc-dom-edit-quota">Total Quota (MB)</label></th>
<td><input type="number" id="wc-dom-edit-quota" class="small-text" min="1">
<p class="description">Sum of all mailbox quotas for this domain.</p></td>
</tr>
<tr>
<th><label for="wc-dom-edit-defquota">Default Mailbox Quota (MB)</label></th>
<td><input type="number" id="wc-dom-edit-defquota" class="small-text" min="1"></td>
</tr>
<tr>
<th><label>Rate Limit</label></th>
<td>
<div class="woocow-flex-inline">
<input type="number" id="wc-dom-edit-rl-value" class="small-text" min="0" placeholder="0 = off">
<span>msgs per</span>
<select id="wc-dom-edit-rl-frame">
<option value="s">Second</option>
<option value="m">Minute</option>
<option value="h">Hour</option>
<option value="d">Day</option>
</select>
<span class="description">(0 = disabled)</span>
</div>
</td>
</tr>
<tr>
<th><label for="wc-dom-edit-relayhost">Outbound Transport</label></th>
<td>
<select id="wc-dom-edit-relayhost" class="regular-text">
<option value="0">— Direct delivery (no relay) —</option>
</select>
<p class="description">Route outbound mail for this domain through a sender-dependent transport.</p>
</td>
</tr>
<tr>
<th><label for="wc-dom-edit-active">Active</label></th>
<td><input type="checkbox" id="wc-dom-edit-active"></td>
</tr>
</table>
<div class="woocow-modal-actions">
<button class="button button-primary" id="wc-dom-edit-save">Save Changes</button>
<button class="button" id="wc-dom-edit-cancel">Cancel</button>
<span id="wc-dom-edit-notice"></span>
</div>
</div>
</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' => 'Postfix',
'dovecot' => 'Dovecot',
'rspamd-history' => 'Rspamd',
'ratelimited' => 'Rate Limit',
'api' => 'API',
'acme' => 'ACME',
'autodiscover' => 'Autodiscover',
'sogo' => 'SOGo',
'netfilter' => 'Netfilter',
'watchdog' => '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 $slug => $label ) : ?>
<option value="<?php echo esc_attr( $slug ); ?>"><?php echo esc_html( $label ); ?></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
}
// ── Page: Quarantine ─────────────────────────────────────────────────────
public function page_quarantine(): 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 Quarantine</h1>
<p>View and manage quarantined messages across all servers. You can permanently delete messages or blacklist senders by domain.</p>
<div class="woocow-toolbar woocow-flex">
<select id="wc-quar-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-quar-load" disabled>Load Quarantine</button>
</div>
<div id="wc-quar-notices"></div>
<div id="wc-quar-wrap">
<p class="description">Select a server above to view quarantined messages.</p>
</div>
</div>
<?php
}
// ── AJAX helpers ─────────────────────────────────────────────────────────
private function verify(): void {
check_ajax_referer( 'woocow_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Insufficient permissions.', 403 );
}
}
private function get_server( int $id ): ?object {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocow_servers WHERE id = %d",
$id
) );
}
private function json_ok( $data = null ): void {
wp_send_json_success( $data );
}
private function json_err( string $msg ): void {
wp_send_json_error( $msg );
}
// ── AJAX: Servers ─────────────────────────────────────────────────────────
public function ajax_woocow_servers_list(): void {
$this->verify();
global $wpdb;
$rows = $wpdb->get_results( "SELECT id, name, url, active, created_at FROM {$wpdb->prefix}woocow_servers ORDER BY name" );
$this->json_ok( $rows );
}
public function ajax_woocow_server_save(): void {
$this->verify();
global $wpdb;
$id = absint( $_POST['id'] ?? 0 );
$name = sanitize_text_field( $_POST['name'] ?? '' );
$url = esc_url_raw( $_POST['url'] ?? '' );
$key = sanitize_text_field( $_POST['api_key'] ?? '' );
$active = absint( $_POST['active'] ?? 1 );
if ( ! $name || ! $url || ! $key ) {
$this->json_err( 'Name, URL, and API key are required.' );
}
$data = compact( 'name', 'url', 'active' ) + [ 'api_key' => $key ];
if ( $id ) {
$wpdb->update( "{$wpdb->prefix}woocow_servers", $data, [ 'id' => $id ] );
$this->json_ok( [ 'id' => $id ] );
} else {
$wpdb->insert( "{$wpdb->prefix}woocow_servers", $data );
$this->json_ok( [ 'id' => $wpdb->insert_id ] );
}
}
public function ajax_woocow_server_delete(): void {
$this->verify();
global $wpdb;
$id = absint( $_POST['id'] ?? 0 );
$wpdb->delete( "{$wpdb->prefix}woocow_servers", [ 'id' => $id ] );
$wpdb->delete( "{$wpdb->prefix}woocow_assignments", [ 'server_id' => $id ] );
$this->json_ok();
}
public function ajax_woocow_server_test(): void {
$this->verify();
$id = absint( $_POST['id'] ?? 0 );
// Allow testing before saving (pass url+key directly)
if ( $id ) {
$server = $this->get_server( $id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
} else {
$url = esc_url_raw( $_POST['url'] ?? '' );
$key = sanitize_text_field( $_POST['api_key'] ?? '' );
if ( ! $url || ! $key ) {
$this->json_err( 'URL and API key required.' );
}
$api = new WooCow_API( $url, $key );
}
$result = $api->test_connection();
if ( $result['success'] ) {
$version = $result['data']['version'] ?? $result['data'][0]['version'] ?? 'unknown';
$this->json_ok( [ 'version' => $version ] );
} else {
$this->json_err( $result['error'] ?? 'Connection failed.' );
}
}
public function ajax_woocow_server_domains(): void {
$this->verify();
$id = absint( $_POST['server_id'] ?? 0 );
$server = $this->get_server( $id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->get_domains();
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to fetch domains.' );
}
$domains = array_map( function ( $d ) {
$rl = is_array( $d['rl'] ?? false ) ? $d['rl'] : [];
return [
'domain' => $d['domain_name'],
'active' => $d['active'],
'description' => $d['description'] ?? '',
'mailboxes' => $d['max_num_mboxes_for_domain'] ?? 0,
'mboxes_in' => $d['mboxes_in_domain'] ?? 0,
'aliases' => $d['max_num_aliases_for_domain'] ?? 0,
'aliases_in' => $d['aliases_in_domain'] ?? 0,
'quota' => (int) round( ( $d['max_quota_for_domain'] ?? 0 ) / 1024 / 1024 ),
'defquota' => (int) round( ( $d['def_new_mailbox_quota'] ?? 0 ) / 1024 / 1024 ),
'quota_used' => (int) round( ( $d['quota_used_in_domain'] ?? 0 ) / 1024 / 1024 ),
'relayhost' => $d['relayhost'] ?? '0',
'rl_value' => $rl['value'] ?? 0,
'rl_frame' => $rl['frame'] ?? 's',
'gal' => $d['gal'] ?? '0',
'backupmx' => $d['backupmx'] ?? '0',
];
}, (array) $result['data'] );
$this->json_ok( $domains );
}
// ── AJAX: Assignments ─────────────────────────────────────────────────────
public function ajax_woocow_assignments_list(): void {
$this->verify();
global $wpdb;
$rows = $wpdb->get_results( "
SELECT a.id, a.customer_id, a.domain, a.created_at,
s.name AS server_name, s.url AS server_url,
u.display_name, u.user_email
FROM {$wpdb->prefix}woocow_assignments a
JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id
JOIN {$wpdb->users} u ON u.ID = a.customer_id
ORDER BY u.display_name, a.domain
" );
$this->json_ok( $rows );
}
public function ajax_woocow_assignment_save(): void {
$this->verify();
global $wpdb;
$customer_id = absint( $_POST['customer_id'] ?? 0 );
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
if ( ! $customer_id || ! $server_id || ! $domain ) {
$this->json_err( 'Customer, server, and domain are all required.' );
}
$existing = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}woocow_assignments WHERE customer_id=%d AND domain=%s",
$customer_id, $domain
) );
if ( $existing ) {
$this->json_err( 'This domain is already assigned to this customer.' );
}
$wpdb->insert( "{$wpdb->prefix}woocow_assignments", compact( 'customer_id', 'server_id', 'domain' ) );
$this->json_ok( [ 'id' => $wpdb->insert_id ] );
}
public function ajax_woocow_assignment_delete(): void {
$this->verify();
global $wpdb;
$id = absint( $_POST['id'] ?? 0 );
$wpdb->delete( "{$wpdb->prefix}woocow_assignments", [ 'id' => $id ] );
$this->json_ok();
}
// ── AJAX: Customer search ─────────────────────────────────────────────────
public function ajax_woocow_customers_search(): void {
$this->verify();
$term = sanitize_text_field( $_POST['term'] ?? '' );
if ( strlen( $term ) < 2 ) {
$this->json_ok( [] );
}
$users = get_users( [
'search' => '*' . $term . '*',
'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
'role__in' => [ 'customer', 'subscriber', 'administrator', 'shop_manager' ],
'number' => 15,
] );
$out = array_map( fn( $u ) => [
'id' => $u->ID,
'label' => sprintf( '%s (%s)', $u->display_name, $u->user_email ),
], $users );
$this->json_ok( $out );
}
// ── AJAX: Admin Mailboxes ─────────────────────────────────────────────────
public function ajax_woocow_admin_mailboxes(): 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 );
$result = $domain
? $api->get_domain_mailboxes( $domain )
: $api->get_all_mailboxes();
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to fetch mailboxes.' );
}
$this->json_ok( [
'mailboxes' => $result['data'] ?? [],
'webmail_url' => $api->get_webmail_url(),
] );
}
public function ajax_woocow_admin_mailbox_create(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$local_part = sanitize_text_field( $_POST['local_part'] ?? '' );
$name = sanitize_text_field( $_POST['name'] ?? '' );
$password = $_POST['password'] ?? '';
$password2 = $_POST['password2'] ?? '';
$quota = absint( $_POST['quota'] ?? 1024 );
if ( ! $domain || ! $local_part || ! $password ) {
$this->json_err( 'Domain, local part, and password are required.' );
}
if ( $password !== $password2 ) {
$this->json_err( 'Passwords do not match.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->create_mailbox( [
'local_part' => $local_part,
'domain' => $domain,
'name' => $name ?: $local_part,
'password' => $password,
'password2' => $password2,
'quota' => $quota,
'active' => 1,
'force_pw_update' => 0,
'tls_enforce_in' => 0,
'tls_enforce_out' => 0,
] );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to create mailbox.' );
}
$this->json_ok( [ 'email' => $local_part . '@' . $domain ] );
}
public function ajax_woocow_admin_mailbox_delete(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$email = sanitize_email( $_POST['email'] ?? '' );
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->delete_mailbox( $email );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to delete mailbox.' );
}
$this->json_ok();
}
public function ajax_woocow_admin_mailbox_edit(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$email = sanitize_email( $_POST['email'] ?? '' );
$type = sanitize_text_field( $_POST['type'] ?? '' ); // 'password' or 'quota'
if ( ! $email ) {
$this->json_err( 'Email address is required.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$attr = [];
if ( $type === 'password' ) {
$pass = $_POST['password'] ?? '';
$pass2 = $_POST['password2'] ?? '';
if ( ! $pass ) {
$this->json_err( 'Password cannot be empty.' );
}
if ( $pass !== $pass2 ) {
$this->json_err( 'Passwords do not match.' );
}
$attr = [ 'password' => $pass, 'password2' => $pass2 ];
} elseif ( $type === 'quota' ) {
$quota = absint( $_POST['quota'] ?? 0 );
if ( $quota < 1 ) {
$this->json_err( 'Quota must be at least 1 MB.' );
}
$attr = [ 'quota' => $quota ];
} else {
$this->json_err( 'Unknown edit type.' );
}
$result = $api->edit_mailbox( [ $email ], $attr );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to update mailbox.' );
}
$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' ],
],
] );
}
public function ajax_woocow_admin_domain_edit(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
if ( ! $domain ) {
$this->json_err( 'Domain name is required.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$attr = [];
if ( isset( $_POST['description'] ) ) {
$attr['description'] = sanitize_text_field( $_POST['description'] );
}
if ( isset( $_POST['mailboxes'] ) ) {
$attr['mailboxes'] = absint( $_POST['mailboxes'] );
}
if ( isset( $_POST['aliases'] ) ) {
$attr['aliases'] = absint( $_POST['aliases'] );
}
if ( isset( $_POST['quota'] ) ) {
$attr['quota'] = absint( $_POST['quota'] );
$attr['maxquota'] = absint( $_POST['quota'] );
}
if ( isset( $_POST['defquota'] ) ) {
$attr['defquota'] = absint( $_POST['defquota'] );
}
if ( isset( $_POST['rl_value'] ) ) {
$attr['rl_value'] = absint( $_POST['rl_value'] );
}
if ( isset( $_POST['rl_frame'] ) ) {
$frame = sanitize_text_field( $_POST['rl_frame'] );
if ( in_array( $frame, [ 's', 'm', 'h', 'd' ], true ) ) {
$attr['rl_frame'] = $frame;
}
}
if ( isset( $_POST['relayhost'] ) ) {
$attr['relayhost'] = absint( $_POST['relayhost'] );
}
if ( isset( $_POST['active'] ) ) {
$attr['active'] = absint( $_POST['active'] );
}
$api = WooCow_API::from_server( $server );
$result = $api->edit_domain( [ $domain ], $attr );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to update domain.' );
}
$this->json_ok();
}
public function ajax_woocow_admin_domain_delete(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
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 );
$result = $api->delete_domain( $domain );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to delete domain.' );
}
$this->json_ok();
}
// ── 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-history', 'ratelimited', '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 . '/100' );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to fetch logs.' );
}
$this->json_ok( $result['data'] ?? [] );
}
// ── AJAX: Admin Quarantine ─────────────────────────────────────────────────
public function ajax_woocow_admin_quarantine(): 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_quarantine();
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to fetch quarantine.' );
}
$this->json_ok( $result['data'] ?? [] );
}
public function ajax_woocow_admin_quarantine_delete(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$qid = absint( $_POST['qid'] ?? 0 );
if ( ! $qid ) {
$this->json_err( 'Message ID is required.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->delete_quarantine( $qid );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to delete quarantine message.' );
}
$this->json_ok();
}
public function ajax_woocow_admin_quarantine_block(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$object_from = sanitize_text_field( $_POST['object_from'] ?? '' );
if ( ! $domain || ! $object_from ) {
$this->json_err( 'Domain and sender address are required.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->add_domain_policy( $domain, $object_from, 'bl' );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to blacklist sender.' );
}
$this->json_ok();
}
}