2026-02-27 08:06:22 +01:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* WooCow Account – WooCommerce My Account integration.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Registers the "email-hosting" endpoint and handles all customer-facing AJAX.
|
|
|
|
|
|
*/
|
|
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
|
|
|
|
|
|
|
|
class WooCow_Account {
|
|
|
|
|
|
|
|
|
|
|
|
const ENDPOINT = 'email-hosting';
|
|
|
|
|
|
|
|
|
|
|
|
public function __construct() {
|
|
|
|
|
|
// Endpoint registration
|
|
|
|
|
|
add_action( 'init', [ $this, 'register_endpoint' ] );
|
|
|
|
|
|
add_filter( 'woocommerce_account_menu_items', [ $this, 'add_menu_item' ] );
|
|
|
|
|
|
add_action( 'woocommerce_account_' . self::ENDPOINT . '_endpoint', [ $this, 'render_page' ] );
|
|
|
|
|
|
|
|
|
|
|
|
// Assets
|
|
|
|
|
|
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
|
|
|
|
|
|
|
|
|
|
|
|
// Password reset integration
|
|
|
|
|
|
add_action( 'woocommerce_save_account_details', [ $this, 'maybe_sync_password' ], 20 );
|
|
|
|
|
|
|
|
|
|
|
|
// AJAX
|
|
|
|
|
|
$actions = [
|
|
|
|
|
|
'woocow_acct_domains',
|
|
|
|
|
|
'woocow_acct_mailboxes',
|
|
|
|
|
|
'woocow_acct_mailbox_create',
|
|
|
|
|
|
'woocow_acct_mailbox_password',
|
|
|
|
|
|
'woocow_acct_aliases',
|
|
|
|
|
|
'woocow_acct_alias_create',
|
|
|
|
|
|
'woocow_acct_alias_delete',
|
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>
2026-02-27 08:38:52 +01:00
|
|
|
|
'woocow_acct_quarantine',
|
|
|
|
|
|
'woocow_acct_quarantine_delete',
|
|
|
|
|
|
'woocow_acct_spam_score',
|
2026-02-27 08:06:22 +01:00
|
|
|
|
];
|
|
|
|
|
|
foreach ( $actions as $action ) {
|
|
|
|
|
|
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function register_endpoint(): void {
|
|
|
|
|
|
add_rewrite_endpoint( self::ENDPOINT, EP_ROOT | EP_PAGES );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function add_menu_item( array $items ): array {
|
|
|
|
|
|
// Insert before logout
|
|
|
|
|
|
$logout = $items['customer-logout'] ?? null;
|
|
|
|
|
|
unset( $items['customer-logout'] );
|
|
|
|
|
|
$items[ self::ENDPOINT ] = __( 'Email Hosting', 'woocow' );
|
|
|
|
|
|
if ( $logout ) {
|
|
|
|
|
|
$items['customer-logout'] = $logout;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $items;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function enqueue_assets(): void {
|
|
|
|
|
|
if ( ! is_account_page() ) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
wp_enqueue_style( 'woocow-account', WOOCOW_PLUGIN_URL . 'assets/css/woocow.css', [], WOOCOW_VERSION );
|
|
|
|
|
|
wp_enqueue_script( 'woocow-account', WOOCOW_PLUGIN_URL . 'assets/js/woocow-account.js', [ 'jquery' ], WOOCOW_VERSION, true );
|
|
|
|
|
|
wp_localize_script( 'woocow-account', 'woocowAcct', [
|
|
|
|
|
|
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
|
|
|
|
|
'nonce' => wp_create_nonce( 'woocow_account' ),
|
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>
2026-02-27 08:38:52 +01:00
|
|
|
|
'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' ),
|
|
|
|
|
|
],
|
2026-02-27 08:06:22 +01:00
|
|
|
|
] );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── My Account page render ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
public function render_page(): void {
|
|
|
|
|
|
if ( ! is_user_logged_in() ) {
|
|
|
|
|
|
echo '<p>' . esc_html__( 'Please log in to manage your email hosting.', 'woocow' ) . '</p>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$customer_id = get_current_user_id();
|
|
|
|
|
|
$assignments = $this->get_customer_assignments( $customer_id );
|
|
|
|
|
|
|
|
|
|
|
|
if ( empty( $assignments ) ) {
|
|
|
|
|
|
echo '<div class="woocommerce-info">' . esc_html__( 'You have no email domains assigned yet. Please contact support.', 'woocow' ) . '</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
?>
|
|
|
|
|
|
<div class="woocow-account" id="woocow-account">
|
|
|
|
|
|
<div id="woocow-acct-notices"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<?php foreach ( $assignments as $assignment ) : ?>
|
|
|
|
|
|
<div class="woocow-domain-panel" data-assignment-id="<?php echo esc_attr( $assignment->id ); ?>"
|
|
|
|
|
|
data-server-id="<?php echo esc_attr( $assignment->server_id ); ?>"
|
|
|
|
|
|
data-domain="<?php echo esc_attr( $assignment->domain ); ?>">
|
|
|
|
|
|
|
|
|
|
|
|
<div class="woocow-domain-header">
|
|
|
|
|
|
<span class="woocow-domain-name"><?php echo esc_html( $assignment->domain ); ?></span>
|
|
|
|
|
|
<span class="woocow-domain-server"><?php echo esc_html( $assignment->server_name ); ?></span>
|
|
|
|
|
|
<a href="<?php echo esc_url( $assignment->webmail_url ); ?>" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm">
|
|
|
|
|
|
Open Webmail
|
|
|
|
|
|
</a>
|
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>
2026-02-27 08:38:52 +01:00
|
|
|
|
<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>
|
2026-02-27 08:06:22 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="woocow-mailboxes-wrap" style="display:none">
|
|
|
|
|
|
<div class="woocow-mailboxes-list"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Create mailbox form -->
|
|
|
|
|
|
<div class="woocow-create-mbox-form" style="display:none">
|
|
|
|
|
|
<h4>Create Mailbox</h4>
|
|
|
|
|
|
<div class="woocow-field-row">
|
|
|
|
|
|
<div class="woocow-flex-inline">
|
|
|
|
|
|
<input type="text" class="wc-mbox-local woocow-input" placeholder="username">
|
|
|
|
|
|
<span class="woocow-at">@</span>
|
|
|
|
|
|
<strong><?php echo esc_html( $assignment->domain ); ?></strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="woocow-field-row">
|
|
|
|
|
|
<input type="text" class="wc-mbox-name woocow-input" placeholder="Full Name">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="woocow-field-row">
|
|
|
|
|
|
<input type="password" class="wc-mbox-pass woocow-input" placeholder="Password">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="woocow-field-row">
|
|
|
|
|
|
<input type="password" class="wc-mbox-pass2 woocow-input" placeholder="Confirm Password">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="woocow-field-row">
|
|
|
|
|
|
<label>Quota (MB): <input type="number" class="wc-mbox-quota woocow-input-sm" value="1024" min="1"></label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="woocow-form-actions">
|
|
|
|
|
|
<button class="woocow-btn woocow-btn-primary wc-mbox-submit">Create Mailbox</button>
|
|
|
|
|
|
<button class="woocow-btn woocow-btn-outline wc-mbox-cancel">Cancel</button>
|
|
|
|
|
|
<span class="wc-mbox-notice"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button class="woocow-btn woocow-btn-outline woocow-create-mbox-btn">+ Add Mailbox</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Change Password Modal -->
|
|
|
|
|
|
<div id="woocow-pw-modal" class="woocow-modal" style="display:none">
|
|
|
|
|
|
<div class="woocow-modal-box">
|
|
|
|
|
|
<h3>Change Mailbox Password</h3>
|
|
|
|
|
|
<p class="woocow-modal-subtitle" id="woocow-pw-email"></p>
|
|
|
|
|
|
<input type="hidden" id="woocow-pw-server-id">
|
|
|
|
|
|
<input type="hidden" id="woocow-pw-mailbox">
|
|
|
|
|
|
<div class="woocow-field-row">
|
|
|
|
|
|
<input type="password" id="woocow-pw-new" class="woocow-input" placeholder="New Password">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="woocow-field-row">
|
|
|
|
|
|
<input type="password" id="woocow-pw-new2" class="woocow-input" placeholder="Confirm New Password">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="woocow-modal-actions">
|
|
|
|
|
|
<button class="woocow-btn woocow-btn-primary" id="woocow-pw-save">Update Password</button>
|
|
|
|
|
|
<button class="woocow-btn woocow-btn-outline" id="woocow-pw-cancel">Cancel</button>
|
|
|
|
|
|
<span id="woocow-pw-notice"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
private function get_customer_assignments( int $customer_id ): array {
|
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
$rows = $wpdb->get_results( $wpdb->prepare( "
|
|
|
|
|
|
SELECT a.id, a.server_id, a.domain, s.name AS server_name, s.url AS server_url
|
|
|
|
|
|
FROM {$wpdb->prefix}woocow_assignments a
|
|
|
|
|
|
JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id
|
|
|
|
|
|
WHERE a.customer_id = %d AND s.active = 1
|
|
|
|
|
|
ORDER BY a.domain
|
|
|
|
|
|
", $customer_id ) );
|
|
|
|
|
|
|
|
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
|
|
$row->webmail_url = rtrim( $row->server_url, '/' ) . '/SOGo';
|
|
|
|
|
|
}
|
|
|
|
|
|
return $rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function get_server( int $id ): ?object {
|
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
return $wpdb->get_row( $wpdb->prepare(
|
|
|
|
|
|
"SELECT * FROM {$wpdb->prefix}woocow_servers WHERE id = %d AND active = 1",
|
|
|
|
|
|
$id
|
|
|
|
|
|
) );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Verify that the current user owns the given assignment. */
|
|
|
|
|
|
private function verify_ownership( int $server_id, string $domain ): bool {
|
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
$found = $wpdb->get_var( $wpdb->prepare( "
|
|
|
|
|
|
SELECT id FROM {$wpdb->prefix}woocow_assignments
|
|
|
|
|
|
WHERE customer_id = %d AND server_id = %d AND domain = %s
|
|
|
|
|
|
", get_current_user_id(), $server_id, $domain ) );
|
|
|
|
|
|
return (bool) $found;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function account_verify(): void {
|
|
|
|
|
|
check_ajax_referer( 'woocow_account', 'nonce' );
|
|
|
|
|
|
if ( ! is_user_logged_in() ) {
|
|
|
|
|
|
wp_send_json_error( 'Not logged in.', 401 );
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── AJAX: Account ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
public function ajax_woocow_acct_domains(): void {
|
|
|
|
|
|
$this->account_verify();
|
|
|
|
|
|
$assignments = $this->get_customer_assignments( get_current_user_id() );
|
|
|
|
|
|
wp_send_json_success( $assignments );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function ajax_woocow_acct_mailboxes(): 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_domain_mailboxes( $domain );
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $result['success'] ) {
|
|
|
|
|
|
wp_send_json_error( $result['error'] ?? 'Could not load mailboxes.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wp_send_json_success( [
|
|
|
|
|
|
'mailboxes' => $result['data'] ?? [],
|
|
|
|
|
|
'webmail_url' => $api->get_webmail_url(),
|
|
|
|
|
|
] );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function ajax_woocow_acct_mailbox_create(): void {
|
|
|
|
|
|
$this->account_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 ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
|
|
|
|
|
wp_send_json_error( 'Access denied.', 403 );
|
|
|
|
|
|
}
|
|
|
|
|
|
if ( ! $local_part || ! $password ) {
|
|
|
|
|
|
wp_send_json_error( 'Username and password are required.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
if ( $password !== $password2 ) {
|
|
|
|
|
|
wp_send_json_error( 'Passwords do not match.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$server = $this->get_server( $server_id );
|
|
|
|
|
|
if ( ! $server ) {
|
|
|
|
|
|
wp_send_json_error( 'Server unavailable.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$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'] ) {
|
|
|
|
|
|
wp_send_json_error( $result['error'] ?? 'Failed to create mailbox.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wp_send_json_success( [ 'email' => $local_part . '@' . $domain ] );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function ajax_woocow_acct_mailbox_password(): void {
|
|
|
|
|
|
$this->account_verify();
|
|
|
|
|
|
|
|
|
|
|
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
|
|
|
|
|
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
|
|
|
|
|
$email = sanitize_email( $_POST['email'] ?? '' );
|
|
|
|
|
|
$password = $_POST['password'] ?? '';
|
|
|
|
|
|
$password2 = $_POST['password2'] ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
|
|
|
|
|
wp_send_json_error( 'Access denied.', 403 );
|
|
|
|
|
|
}
|
|
|
|
|
|
if ( ! $password || $password !== $password2 ) {
|
|
|
|
|
|
wp_send_json_error( 'Passwords do not match or are empty.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate that email belongs to the customer's domain.
|
|
|
|
|
|
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->edit_mailbox( [ $email ], [
|
|
|
|
|
|
'password' => $password,
|
|
|
|
|
|
'password2' => $password2,
|
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $result['success'] ) {
|
|
|
|
|
|
wp_send_json_error( $result['error'] ?? 'Failed to update password.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wp_send_json_success();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function ajax_woocow_acct_aliases(): 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_all_aliases();
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $result['success'] ) {
|
|
|
|
|
|
wp_send_json_error( $result['error'] ?? 'Could not load aliases.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filter to this domain only.
|
|
|
|
|
|
$aliases = array_filter( (array) $result['data'], function ( $a ) use ( $domain ) {
|
|
|
|
|
|
return isset( $a['address'] ) && str_ends_with( $a['address'], '@' . $domain );
|
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
|
|
wp_send_json_success( array_values( $aliases ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function ajax_woocow_acct_alias_create(): void {
|
|
|
|
|
|
$this->account_verify();
|
|
|
|
|
|
|
|
|
|
|
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
|
|
|
|
|
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
|
|
|
|
|
$address = sanitize_email( $_POST['address'] ?? '' );
|
|
|
|
|
|
$goto = sanitize_email( $_POST['goto'] ?? '' );
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
|
|
|
|
|
wp_send_json_error( 'Access denied.', 403 );
|
|
|
|
|
|
}
|
|
|
|
|
|
if ( ! $address || ! $goto ) {
|
|
|
|
|
|
wp_send_json_error( 'Alias address and destination are required.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
if ( ! str_ends_with( $address, '@' . $domain ) ) {
|
|
|
|
|
|
wp_send_json_error( 'Alias address must 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->create_alias( [
|
|
|
|
|
|
'address' => $address,
|
|
|
|
|
|
'goto' => $goto,
|
|
|
|
|
|
'active' => 1,
|
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $result['success'] ) {
|
|
|
|
|
|
wp_send_json_error( $result['error'] ?? 'Failed to create alias.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wp_send_json_success();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function ajax_woocow_acct_alias_delete(): void {
|
|
|
|
|
|
$this->account_verify();
|
|
|
|
|
|
|
|
|
|
|
|
$server_id = absint( $_POST['server_id'] ?? 0 );
|
|
|
|
|
|
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
|
|
|
|
|
$alias_id = absint( $_POST['alias_id'] ?? 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_alias( $alias_id );
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $result['success'] ) {
|
|
|
|
|
|
wp_send_json_error( $result['error'] ?? 'Failed to delete alias.' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wp_send_json_success();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-02-27 08:38:52 +01:00
|
|
|
|
// ── 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 08:06:22 +01:00
|
|
|
|
// ── WP password change sync ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* When a customer saves their WooCommerce account details with a new password,
|
|
|
|
|
|
* offer them the option to sync it to all their mailboxes.
|
|
|
|
|
|
*
|
|
|
|
|
|
* NOTE: This hook fires after the WP password has been updated.
|
|
|
|
|
|
* We only sync if the customer explicitly checked the option.
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function maybe_sync_password( int $user_id ): void {
|
|
|
|
|
|
// Only proceed if the "sync to mailcow" checkbox was checked and a new password given.
|
|
|
|
|
|
if ( empty( $_POST['woocow_sync_pw'] ) || empty( $_POST['password_1'] ) ) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$password = $_POST['password_1'];
|
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
|
|
$assignments = $wpdb->get_results( $wpdb->prepare( "
|
|
|
|
|
|
SELECT a.server_id, a.domain, s.url, s.api_key
|
|
|
|
|
|
FROM {$wpdb->prefix}woocow_assignments a
|
|
|
|
|
|
JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id
|
|
|
|
|
|
WHERE a.customer_id = %d AND s.active = 1
|
|
|
|
|
|
", $user_id ) );
|
|
|
|
|
|
|
|
|
|
|
|
foreach ( $assignments as $assignment ) {
|
|
|
|
|
|
$api = new WooCow_API( $assignment->url, $assignment->api_key );
|
|
|
|
|
|
$mboxes = $api->get_domain_mailboxes( $assignment->domain );
|
|
|
|
|
|
if ( ! $mboxes['success'] ) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ( (array) ( $mboxes['data'] ?? [] ) as $mbox ) {
|
|
|
|
|
|
$api->edit_mailbox( [ $mbox['username'] ], [
|
|
|
|
|
|
'password' => $password,
|
|
|
|
|
|
'password2' => $password,
|
|
|
|
|
|
] );
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|