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:
@@ -24,6 +24,12 @@ class WooCow_Admin {
|
||||
'woocow_admin_mailbox_create',
|
||||
'woocow_admin_mailbox_delete',
|
||||
'woocow_admin_mailbox_edit',
|
||||
'woocow_admin_domain_add',
|
||||
'woocow_admin_domain_dns',
|
||||
'woocow_admin_relayhosts_list',
|
||||
'woocow_admin_relayhost_save',
|
||||
'woocow_admin_relayhost_delete',
|
||||
'woocow_admin_logs',
|
||||
];
|
||||
|
||||
foreach ( $ajax_actions as $action ) {
|
||||
@@ -43,10 +49,13 @@ class WooCow_Admin {
|
||||
'dashicons-email-alt2',
|
||||
56
|
||||
);
|
||||
add_submenu_page( 'woocow', 'Dashboard', 'Dashboard', 'manage_woocommerce', 'woocow', [ $this, 'page_dashboard' ] );
|
||||
add_submenu_page( 'woocow', 'Dashboard', 'Dashboard', 'manage_woocommerce', 'woocow', [ $this, 'page_dashboard' ] );
|
||||
add_submenu_page( 'woocow', 'Servers', 'Servers', 'manage_woocommerce', 'woocow-servers', [ $this, 'page_servers' ] );
|
||||
add_submenu_page( 'woocow', 'Assignments','Assignments', 'manage_woocommerce', 'woocow-assignments', [ $this, 'page_assignments' ] );
|
||||
add_submenu_page( 'woocow', 'Mailboxes', 'Mailboxes', 'manage_woocommerce', 'woocow-mailboxes', [ $this, 'page_mailboxes' ] );
|
||||
add_submenu_page( 'woocow', 'Domains', 'Domains', 'manage_woocommerce', 'woocow-domains', [ $this, 'page_domains' ] );
|
||||
add_submenu_page( 'woocow', 'Transports','Transports', 'manage_woocommerce', 'woocow-transports', [ $this, 'page_transports' ] );
|
||||
add_submenu_page( 'woocow', 'Logs', 'Logs', 'manage_woocommerce', 'woocow-logs', [ $this, 'page_logs' ] );
|
||||
}
|
||||
|
||||
public function enqueue_assets( string $hook ): void {
|
||||
@@ -301,6 +310,181 @@ class WooCow_Admin {
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Domains ────────────────────────────────────────────────────────
|
||||
|
||||
public function page_domains(): void {
|
||||
global $wpdb;
|
||||
$servers = $wpdb->get_results( "SELECT id, name, url FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Domains</h1>
|
||||
<p>Add and manage domains on your Mailcow servers. After adding a domain, DNS records (including DKIM) are generated automatically.</p>
|
||||
|
||||
<div class="woocow-toolbar woocow-flex">
|
||||
<select id="wc-dom-server" class="regular-text">
|
||||
<option value="">— Select server —</option>
|
||||
<?php foreach ( $servers as $s ) : ?>
|
||||
<option value="<?php echo esc_attr( $s->id ); ?>"
|
||||
data-url="<?php echo esc_attr( $s->url ); ?>">
|
||||
<?php echo esc_html( $s->name ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button" id="wc-dom-load" disabled>Load Domains</button>
|
||||
<button class="button button-primary" id="wc-dom-add-btn" style="display:none">+ Add Domain</button>
|
||||
</div>
|
||||
|
||||
<div id="wc-dom-notices"></div>
|
||||
|
||||
<!-- Add Domain form -->
|
||||
<div id="wc-dom-form" class="woocow-card woocow-form" style="display:none">
|
||||
<h3>Add Domain</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="wc-dom-name">Domain</label></th>
|
||||
<td><input type="text" id="wc-dom-name" class="regular-text" placeholder="example.com"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-desc">Description</label></th>
|
||||
<td><input type="text" id="wc-dom-desc" class="regular-text" placeholder="Optional"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-mailboxes">Max Mailboxes</label></th>
|
||||
<td><input type="number" id="wc-dom-mailboxes" value="10" min="1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-aliases">Max Aliases</label></th>
|
||||
<td><input type="number" id="wc-dom-aliases" value="400" min="0"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-quota">Total Quota (MB)</label></th>
|
||||
<td><input type="number" id="wc-dom-quota" value="10240" min="1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-defquota">Default Mailbox Quota (MB)</label></th>
|
||||
<td><input type="number" id="wc-dom-defquota" value="3072" min="1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-dom-dkim-size">DKIM Key Size</label></th>
|
||||
<td>
|
||||
<select id="wc-dom-dkim-size">
|
||||
<option value="2048" selected>2048 (recommended)</option>
|
||||
<option value="1024">1024</option>
|
||||
<option value="4096">4096</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="button button-primary" id="wc-dom-save">Add Domain & Generate DKIM</button>
|
||||
<button class="button" id="wc-dom-cancel">Cancel</button>
|
||||
<span id="wc-dom-form-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Domain list -->
|
||||
<div id="wc-dom-table-wrap"></div>
|
||||
|
||||
<!-- DNS Records panel -->
|
||||
<div id="wc-dom-dns-panel" class="woocow-card" style="display:none">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
||||
<h3 style="margin:0" id="wc-dom-dns-title">DNS Records</h3>
|
||||
<button class="button" id="wc-dom-dns-close">Close</button>
|
||||
</div>
|
||||
<p class="description">Add these records to your DNS provider for <strong id="wc-dom-dns-domain"></strong>.</p>
|
||||
<div id="wc-dom-dns-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Transports ─────────────────────────────────────────────────────
|
||||
|
||||
public function page_transports(): void {
|
||||
global $wpdb;
|
||||
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Sender-Dependent Transports</h1>
|
||||
<p>Configure relay hosts for outbound mail delivery. Each transport can be associated with one or more domains in Mailcow.</p>
|
||||
|
||||
<div class="woocow-toolbar woocow-flex">
|
||||
<select id="wc-tr-server" class="regular-text">
|
||||
<option value="">— Select server —</option>
|
||||
<?php foreach ( $servers as $s ) : ?>
|
||||
<option value="<?php echo esc_attr( $s->id ); ?>"><?php echo esc_html( $s->name ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button" id="wc-tr-load" disabled>Load Transports</button>
|
||||
<button class="button button-primary" id="wc-tr-add-btn" style="display:none">+ Add Transport</button>
|
||||
</div>
|
||||
|
||||
<div id="wc-tr-notices"></div>
|
||||
|
||||
<div id="wc-tr-form" class="woocow-card woocow-form" style="display:none">
|
||||
<h3>Add Transport (Relay Host)</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="wc-tr-hostname">Hostname:Port</label></th>
|
||||
<td><input type="text" id="wc-tr-hostname" class="regular-text" placeholder="smtp.relay.com:587"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-tr-user">SMTP Username</label></th>
|
||||
<td><input type="text" id="wc-tr-user" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-tr-pass">SMTP Password</label></th>
|
||||
<td><input type="password" id="wc-tr-pass" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-tr-active">Active</label></th>
|
||||
<td><input type="checkbox" id="wc-tr-active" checked></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="button button-primary" id="wc-tr-save">Add Transport</button>
|
||||
<button class="button" id="wc-tr-cancel">Cancel</button>
|
||||
<span id="wc-tr-form-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wc-tr-table-wrap"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Logs ────────────────────────────────────────────────────────────
|
||||
|
||||
public function page_logs(): void {
|
||||
global $wpdb;
|
||||
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||
$log_types = [ 'postfix', 'dovecot', 'rspamd', 'ratelimit', 'api', 'acme', 'autodiscover', 'sogo', 'netfilter', 'watchdog' ];
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Server Logs</h1>
|
||||
|
||||
<div class="woocow-toolbar woocow-flex">
|
||||
<select id="wc-log-server" class="regular-text">
|
||||
<option value="">— Select server —</option>
|
||||
<?php foreach ( $servers as $s ) : ?>
|
||||
<option value="<?php echo esc_attr( $s->id ); ?>"><?php echo esc_html( $s->name ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select id="wc-log-type">
|
||||
<?php foreach ( $log_types as $t ) : ?>
|
||||
<option value="<?php echo esc_attr( $t ); ?>"><?php echo esc_html( ucfirst( $t ) ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button" id="wc-log-load" disabled>Load Logs</button>
|
||||
</div>
|
||||
|
||||
<div id="wc-log-wrap">
|
||||
<p class="description">Select a server and log type above.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── AJAX helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
private function verify(): void {
|
||||
@@ -632,4 +816,189 @@ class WooCow_Admin {
|
||||
|
||||
$this->json_ok();
|
||||
}
|
||||
|
||||
// ── AJAX: Domains ─────────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_admin_domain_add(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
$desc = sanitize_text_field( $_POST['description'] ?? '' );
|
||||
$mailboxes = absint( $_POST['mailboxes'] ?? 10 );
|
||||
$aliases = absint( $_POST['aliases'] ?? 400 );
|
||||
$quota = absint( $_POST['quota'] ?? 10240 );
|
||||
$defquota = absint( $_POST['defquota'] ?? 3072 );
|
||||
$dkim_size = absint( $_POST['dkim_size'] ?? 2048 );
|
||||
|
||||
if ( ! $domain ) {
|
||||
$this->json_err( 'Domain name is required.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
|
||||
// Add domain
|
||||
$result = $api->create_domain( [
|
||||
'domain' => $domain,
|
||||
'description' => $desc,
|
||||
'mailboxes' => $mailboxes,
|
||||
'aliases' => $aliases,
|
||||
'quota' => $quota,
|
||||
'defquota' => $defquota,
|
||||
'maxquota' => $quota,
|
||||
'active' => 1,
|
||||
'restart_sogo'=> 1,
|
||||
] );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to add domain.' );
|
||||
}
|
||||
|
||||
// Auto-generate DKIM
|
||||
$api->generate_dkim( $domain, 'dkim', $dkim_size );
|
||||
|
||||
$this->json_ok( [ 'domain' => $domain ] );
|
||||
}
|
||||
|
||||
public function ajax_woocow_admin_domain_dns(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$mail_host = parse_url( $server->url, PHP_URL_HOST );
|
||||
|
||||
// Fetch DKIM
|
||||
$dkim_result = $api->get_dkim( $domain );
|
||||
$dkim_txt = '';
|
||||
$dkim_sel = 'dkim';
|
||||
if ( $dkim_result['success'] && ! empty( $dkim_result['data']['dkim_txt'] ) ) {
|
||||
$dkim_txt = $dkim_result['data']['dkim_txt'];
|
||||
$dkim_sel = $dkim_result['data']['dkim_selector'] ?? 'dkim';
|
||||
}
|
||||
|
||||
$this->json_ok( [
|
||||
'domain' => $domain,
|
||||
'mail_host' => $mail_host,
|
||||
'dkim_sel' => $dkim_sel,
|
||||
'dkim_txt' => $dkim_txt,
|
||||
'records' => [
|
||||
[ 'type' => 'MX', 'host' => $domain, 'value' => $mail_host . '.', 'prio' => '10', 'ttl' => '3600' ],
|
||||
[ 'type' => 'TXT', 'host' => $domain, 'value' => 'v=spf1 mx ~all', 'prio' => '', 'ttl' => '3600', 'note' => 'SPF' ],
|
||||
[ 'type' => 'TXT', 'host' => '_dmarc.' . $domain, 'value' => 'v=DMARC1; p=quarantine; rua=mailto:postmaster@' . $domain, 'prio' => '', 'ttl' => '3600', 'note' => 'DMARC' ],
|
||||
[ 'type' => 'TXT', 'host' => $dkim_sel . '._domainkey.' . $domain, 'value' => $dkim_txt ?: '(generate DKIM first)', 'prio' => '', 'ttl' => '3600', 'note' => 'DKIM' ],
|
||||
[ 'type' => 'CNAME','host' => 'autoconfig.' . $domain, 'value' => $mail_host . '.', 'prio' => '', 'ttl' => '3600', 'note' => 'Autoconfig' ],
|
||||
[ 'type' => 'CNAME','host' => 'autodiscover.' . $domain, 'value' => $mail_host . '.', 'prio' => '', 'ttl' => '3600', 'note' => 'Autodiscover' ],
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
// ── AJAX: Transports ──────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_admin_relayhosts_list(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->get_relayhosts();
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to fetch transports.' );
|
||||
}
|
||||
|
||||
$this->json_ok( $result['data'] ?? [] );
|
||||
}
|
||||
|
||||
public function ajax_woocow_admin_relayhost_save(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$hostname = sanitize_text_field( $_POST['hostname'] ?? '' );
|
||||
$username = sanitize_text_field( $_POST['username'] ?? '' );
|
||||
$password = $_POST['password'] ?? '';
|
||||
$active = absint( $_POST['active'] ?? 1 );
|
||||
|
||||
if ( ! $hostname || ! $username ) {
|
||||
$this->json_err( 'Hostname and username are required.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->create_relayhost( compact( 'hostname', 'username', 'password', 'active' ) );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to add transport.' );
|
||||
}
|
||||
|
||||
$this->json_ok();
|
||||
}
|
||||
|
||||
public function ajax_woocow_admin_relayhost_delete(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$id = absint( $_POST['id'] ?? 0 );
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->delete_relayhost( $id );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to delete transport.' );
|
||||
}
|
||||
|
||||
$this->json_ok();
|
||||
}
|
||||
|
||||
// ── AJAX: Logs ────────────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_admin_logs(): void {
|
||||
$this->verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$log_type = sanitize_key( $_POST['log_type'] ?? 'postfix' );
|
||||
|
||||
$allowed = [ 'postfix', 'dovecot', 'rspamd', 'ratelimit', 'api', 'acme', 'autodiscover', 'sogo', 'netfilter', 'watchdog' ];
|
||||
if ( ! in_array( $log_type, $allowed, true ) ) {
|
||||
$this->json_err( 'Invalid log type.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
$this->json_err( 'Server not found.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->request( 'GET', '/api/v1/get/logs/' . $log_type );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
$this->json_err( $result['error'] ?? 'Failed to fetch logs.' );
|
||||
}
|
||||
|
||||
$this->json_ok( $result['data'] ?? [] );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user