#!/usr/bin/env php [--url=] * Example: php worker.php 10 "w1" * MultiSite: php worker.php 10 "w1" --url="https://example.com" * Parallel: seq 1 10 | xargs -P10 -I{} php worker.php 10 "w{}" * Parallel MultiSite: seq 1 10 | xargs -P10 -I{} php worker.php 10 "w{}" --url="https://example.com" * * @package ROBOTSTXT_SMTP_Newsletter * @since 2.2.0 */ // Start output buffering to prevent header warnings from plugins. ob_start(); // Security: CLI-only execution. if ( php_sapi_name() !== 'cli' ) { http_response_code( 403 ); die( 'Forbidden: This script must be run from command line' ); } // Parse command-line arguments. if ( $argc < 3 ) { fwrite( STDERR, "Usage: php worker.php [--url=]\n" ); fwrite( STDERR, "Example: php worker.php 10 \"w1\"\n" ); fwrite( STDERR, "MultiSite: php worker.php 10 \"w1\" --url=\"https://example.com\"\n" ); exit( 1 ); } $batch_size = max( 1, (int) $argv[1] ); $worker_id = $argv[2]; // Parse optional --url parameter for MultiSite support. $site_url = null; for ( $i = 3; $i < $argc; $i++ ) { if ( strpos( $argv[ $i ], '--url=' ) === 0 ) { $site_url = substr( $argv[ $i ], 6 ); break; } } // If --url is provided, set HTTP_HOST for WordPress to load the correct site. if ( $site_url ) { $parsed_url = parse_url( $site_url ); if ( isset( $parsed_url['host'] ) ) { $_SERVER['HTTP_HOST'] = $parsed_url['host']; $_SERVER['SERVER_NAME'] = $parsed_url['host']; // Set HTTPS if URL uses https. if ( isset( $parsed_url['scheme'] ) && 'https' === $parsed_url['scheme'] ) { $_SERVER['HTTPS'] = 'on'; } // Set REQUEST_URI from URL path (critical for MultiSite subdirectory detection). $_SERVER['REQUEST_URI'] = isset( $parsed_url['path'] ) ? $parsed_url['path'] : '/'; } } // Load WordPress with CLI context flags. define( 'DOING_CRON', true ); // Find wp-load.php (3 levels up from plugin directory). $wp_load_path = __DIR__ . '/../../../wp-load.php'; if ( ! file_exists( $wp_load_path ) ) { fwrite( STDERR, "Error: Could not find wp-load.php at {$wp_load_path}\n" ); exit( 1 ); } require_once $wp_load_path; // Discard any output from WordPress initialization (headers, plugin warnings, etc.). ob_end_clean(); // Load Newsletter base classes if not already loaded if ( ! class_exists( 'NewsletterAddon' ) ) { $newsletter_base = WP_PLUGIN_DIR . '/newsletter/classes/NewsletterAddon.php'; if ( file_exists( $newsletter_base ) ) { require_once $newsletter_base; } else { fwrite( STDERR, "Error: Newsletter plugin base addon class not found\n" ); exit( 1 ); } } if ( ! class_exists( 'NewsletterMailerAddon' ) ) { $newsletter_mailer = WP_PLUGIN_DIR . '/newsletter/classes/NewsletterMailerAddon.php'; if ( file_exists( $newsletter_mailer ) ) { require_once $newsletter_mailer; } else { fwrite( STDERR, "Error: Newsletter plugin mailer addon class not found\n" ); exit( 1 ); } } // Load plugin if not already loaded (happens in DOING_CRON context) $plugin_manually_loaded = false; if ( ! defined( 'ROBOTSTXT_SMTP_NEWSLETTER_VERSION' ) ) { $plugin_file = __DIR__ . '/robotstxt-smtp-newsletter.php'; if ( file_exists( $plugin_file ) ) { require_once $plugin_file; $plugin_manually_loaded = true; } else { fwrite( STDERR, "Error: Newsletter SMTP plugin file not found\n" ); exit( 1 ); } } // Verify plugin loaded successfully if ( ! defined( 'ROBOTSTXT_SMTP_NEWSLETTER_VERSION' ) ) { fwrite( STDERR, "Error: Newsletter SMTP plugin could not be loaded\n" ); exit( 1 ); } // If we manually loaded the plugin, we need to initialize it manually // (normally this happens via newsletter_loaded hook, but that doesn't fire in DOING_CRON) if ( $plugin_manually_loaded ) { \Robotstxt_SMTP_Newsletter\Plugin::instance_init(); } // Get queue and statistics instances. $queue = \Robotstxt_SMTP_Newsletter\Queue::get_instance(); $stats = \Robotstxt_SMTP_Newsletter\Statistics::get_instance(); $total_success = 0; $total_failure = 0; // Continuous loop: keep processing batches until the queue is empty. while ( true ) { $items = $queue->get_and_lock_batch( $batch_size, $worker_id ); if ( empty( $items ) ) { break; } foreach ( $items as $item ) { try { // Build headers array. $headers = array(); // Add From header if specified. if ( ! empty( $item->from_email ) ) { $from = ! empty( $item->from_name ) ? sprintf( '%s <%s>', $item->from_name, $item->from_email ) : $item->from_email; $headers[] = 'From: ' . $from; } // Add Reply-To header if specified. if ( ! empty( $item->reply_to ) ) { $headers[] = 'Reply-To: ' . $item->reply_to; } // Add Content-Type header (prefer HTML if available). if ( ! empty( $item->body_html ) ) { $headers[] = 'Content-Type: text/html; charset=UTF-8'; } // Decode and add custom headers from JSON. if ( ! empty( $item->headers_json ) ) { $custom = json_decode( $item->headers_json, true ); if ( is_array( $custom ) ) { foreach ( $custom as $key => $value ) { $headers[] = sprintf( '%s: %s', $key, $value ); } } } // Select body (prefer HTML over text). $body = ! empty( $item->body_html ) ? $item->body_html : $item->body_text; if ( empty( $body ) ) { throw new \Exception( 'Email body is empty' ); } // Send via wp_mail (uses core SMTP configuration). $result = wp_mail( $item->to_email, $item->subject, $body, $headers ); if ( $result ) { $queue->mark_sent( $item->id ); $total_success++; echo sprintf( " ✓ [%s] Sent: %s\n", $worker_id, $item->to_email ); } else { $queue->mark_failed( $item->id, 'wp_mail returned false' ); $total_failure++; echo sprintf( " ✗ [%s] Failed: %s (wp_mail returned false)\n", $worker_id, $item->to_email ); } } catch ( \Exception $e ) { $queue->mark_failed( $item->id, $e->getMessage() ); $total_failure++; echo sprintf( " ✗ [%s] Error: %s - %s\n", $worker_id, $item->to_email, $e->getMessage() ); } } // Sync statistics after each batch. $stats->sync_from_queue(); } if ( 0 === $total_success && 0 === $total_failure ) { echo sprintf( "Worker %s: No items to process\n", $worker_id ); } else { echo sprintf( "Worker %s: Done. Success: %d, Failed: %d\n", $worker_id, $total_success, $total_failure ); } exit( 0 );