223 lines
6.5 KiB
PHP
223 lines
6.5 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Newsletter Queue Worker
|
|
*
|
|
* Processes queued emails via wp_mail() using configured SMTP settings.
|
|
*
|
|
* This script runs as a standalone CLI process, independent of WordPress cron.
|
|
* It loads WordPress with DOING_CRON defined to prevent triggering actual cron.
|
|
*
|
|
* Usage: php worker.php <batch_size> <worker_id> [--url=<site_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 <batch_size> <worker_id> [--url=<site_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 );
|