feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes

Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
  display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
  with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
  server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
  date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
  to any external address)

UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
  (lock, chart-bar, trash) with title tooltips

i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 08:38:52 +01:00
parent 1ea2ed7e74
commit 1c5b58f238
11 changed files with 1252 additions and 14 deletions

View File

@@ -31,6 +31,9 @@ class WooCow_Account {
'woocow_acct_aliases',
'woocow_acct_alias_create',
'woocow_acct_alias_delete',
'woocow_acct_quarantine',
'woocow_acct_quarantine_delete',
'woocow_acct_spam_score',
];
foreach ( $actions as $action ) {
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
@@ -61,6 +64,32 @@ class WooCow_Account {
wp_localize_script( 'woocow-account', 'woocowAcct', [
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocow_account' ),
'i18n' => [
'load_mailboxes' => __( 'Load Mailboxes', 'woocow' ),
'refresh' => __( 'Refresh', 'woocow' ),
'loading' => __( 'Loading…', 'woocow' ),
'no_mailboxes' => __( 'No mailboxes yet. Create one below.', 'woocow' ),
'change_password' => __( 'Change Password', 'woocow' ),
'aliases_forwarders' => __( 'Aliases & Forwarders', 'woocow' ),
'spam_filter' => __( 'Spam Filter', 'woocow' ),
'webmail' => __( 'Webmail', 'woocow' ),
'add_alias' => __( 'Add Alias / Forwarder', 'woocow' ),
'no_aliases' => __( 'No aliases for this domain yet.', 'woocow' ),
'add_mailbox' => __( 'Add Mailbox', 'woocow' ),
'create_mailbox' => __( 'Create Mailbox', 'woocow' ),
'cancel' => __( 'Cancel', 'woocow' ),
'save' => __( 'Save', 'woocow' ),
'delete' => __( 'Delete', 'woocow' ),
'quarantine' => __( 'Quarantine', 'woocow' ),
'no_quarantine' => __( 'No quarantined messages for this domain.', 'woocow' ),
'q_release_note' => __( 'To release a message to your inbox, use the link in your quarantine notification email or via Webmail.', 'woocow' ),
'q_delete_confirm' => __( 'Permanently delete this quarantined message?', 'woocow' ),
'spam_threshold' => __( 'Spam Filter Threshold', 'woocow' ),
'spam_help' => __( 'Lower = stricter. Default is 5. Emails above this score go to spam/quarantine.', 'woocow' ),
'update_password' => __( 'Update Password', 'woocow' ),
'passwords_mismatch' => __( 'Passwords do not match or are empty.', 'woocow' ),
'unlimited' => __( 'Unlimited', 'woocow' ),
],
] );
}
@@ -94,7 +123,13 @@ class WooCow_Account {
<a href="<?php echo esc_url( $assignment->webmail_url ); ?>" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm">
Open Webmail
</a>
<button class="woocow-btn woocow-btn-sm woocow-btn-outline woocow-load-mailboxes">Load Mailboxes</button>
<button class="woocow-btn woocow-btn-sm woocow-btn-outline woocow-load-mailboxes"><?php esc_html_e( 'Load Mailboxes', 'woocow' ); ?></button>
<button class="woocow-btn woocow-btn-sm woocow-btn-ghost woocow-load-quarantine"><?php esc_html_e( 'Quarantine', 'woocow' ); ?></button>
</div>
<!-- Quarantine panel -->
<div class="woocow-quarantine-wrap" style="display:none">
<div class="woocow-quarantine-list"></div>
</div>
<div class="woocow-mailboxes-wrap" style="display:none">
@@ -416,6 +451,98 @@ class WooCow_Account {
wp_send_json_success();
}
// ── AJAX: Quarantine ──────────────────────────────────────────────────────
public function ajax_woocow_acct_quarantine(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
wp_send_json_error( 'Access denied.', 403 );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
wp_send_json_error( 'Server unavailable.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->get_quarantine();
if ( ! $result['success'] ) {
wp_send_json_error( $result['error'] ?? 'Could not load quarantine.' );
}
// Filter to messages where the recipient belongs to this domain.
$messages = array_values( array_filter( (array) $result['data'], function ( $m ) use ( $domain ) {
return isset( $m['rcpt'] ) && str_ends_with( $m['rcpt'], '@' . $domain );
} ) );
wp_send_json_success( $messages );
}
public function ajax_woocow_acct_quarantine_delete(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$qid = absint( $_POST['qid'] ?? 0 );
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
wp_send_json_error( 'Access denied.', 403 );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
wp_send_json_error( 'Server unavailable.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->delete_quarantine( $qid );
if ( ! $result['success'] ) {
wp_send_json_error( $result['error'] ?? 'Failed to delete quarantine message.' );
}
wp_send_json_success();
}
// ── AJAX: Spam score ──────────────────────────────────────────────────────
public function ajax_woocow_acct_spam_score(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$email = sanitize_email( $_POST['email'] ?? '' );
$spam_score = floatval( $_POST['spam_score'] ?? 5 );
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
wp_send_json_error( 'Access denied.', 403 );
}
if ( ! str_ends_with( $email, '@' . $domain ) ) {
wp_send_json_error( 'Email does not belong to your domain.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
wp_send_json_error( 'Server unavailable.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->request( 'POST', '/api/v1/edit/spam-score/' . rawurlencode( $email ), [
'spam_score' => $spam_score,
] );
if ( ! $result['success'] ) {
wp_send_json_error( $result['error'] ?? 'Failed to update spam score.' );
}
wp_send_json_success();
}
// ── WP password change sync ───────────────────────────────────────────────
/**