*/ private $settings = array(); /** * Batch size for bulk sending. * * @var int */ protected $batch_size = 10; /** * Maximum emails per connection before reconnecting. * * @var int */ private $max_emails_per_connection = 50; /** * Count of emails sent in current connection. * * @var int */ private $emails_sent_in_connection = 0; /** * Constructor. * * @param array $options Plugin options. */ public function __construct( $options = array() ) { parent::__construct( 'robotstxt-smtp-newsletter', $options ); $this->settings = Plugin::get_instance()->get_smtp_settings(); // Configure batch_size from turbo (standard Newsletter pattern). if ( ! empty( $options['turbo'] ) ) { $this->batch_size = max( 1, (int) $options['turbo'] ); } $this->max_emails_per_connection = max( 1, (int) ( $options['max_per_connection'] ?? 50 ) ); } /** * Get speed (emails per hour) for Newsletter engine. * * Standard Newsletter addons don't implement this, but we do * to allow dynamic speed calculation based on rate limits. * * @return int */ public function get_speed() { return $this->speed; } /** * Initialize PHPMailer with SMTPKeepAlive. * * @return \PHPMailer\PHPMailer\PHPMailer * @throws \Exception If PHPMailer cannot be initialized. */ private function get_phpmailer() { if ( $this->phpmailer !== null ) { return $this->phpmailer; } $this->phpmailer = new \PHPMailer\PHPMailer\PHPMailer( true ); // Configure SMTP. $this->phpmailer->isSMTP(); $this->phpmailer->Host = $this->settings['host']; $this->phpmailer->Port = $this->settings['port']; $this->phpmailer->SMTPAuth = ! empty( $this->settings['user'] ); $this->phpmailer->Username = $this->settings['user']; $this->phpmailer->Password = $this->settings['password']; // Security. $encryption = $this->settings['encryption'] ?? ''; if ( 'tls' === $encryption ) { $this->phpmailer->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; } elseif ( 'ssl' === $encryption ) { $this->phpmailer->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; } // CRITICAL: Enable KeepAlive for batch sending. $this->phpmailer->SMTPKeepAlive = true; // Set HELO hostname using WordPress site URL (works in multisite and WP-CLI). $site_hostname = wp_parse_url( get_site_url(), PHP_URL_HOST ); if ( $site_hostname ) { $this->phpmailer->Hostname = $site_hostname; } // Additional settings. $this->phpmailer->Timeout = 30; $this->phpmailer->SMTPDebug = 0; $this->phpmailer->CharSet = 'UTF-8'; return $this->phpmailer; } /** * Send single email. * * Uses persistent SMTP connection (SMTPKeepAlive) for efficiency. * * @param \TNP_Mailer_Message $message Message to send. * * @return bool|WP_Error True on success, WP_Error on failure. */ public function send( $message ) { try { $mailer = $this->get_phpmailer(); // Clear previous message data but keep connection. $mailer->clearAddresses(); $mailer->clearReplyTos(); $mailer->clearAttachments(); $mailer->clearCustomHeaders(); $this->prepare_message( $mailer, $message ); // Check rate limits BEFORE sending. if ( ! $this->check_rate_limits() ) { return new \WP_Error( self::ERROR_FATAL, __( 'Rate limit exceeded. Please wait before sending more emails.', 'robotstxt-smtp-newsletter' ) ); } $mailer->send(); ++$this->emails_sent_in_connection; // Track rate limit. $this->track_email_sent(); // Reconnect if threshold reached. if ( $this->emails_sent_in_connection >= $this->max_emails_per_connection ) { $this->close_connection(); } return true; } catch ( \Exception $e ) { $this->log_error( $message, $e->getMessage() ); return new \WP_Error( self::ERROR_GENERIC, $e->getMessage() ); } } /** * Send chunk of emails using persistent SMTP connection. * * Unlike API-based addons that use curl_multi for parallel sending, * SMTP is sequential but benefits from keeping the connection alive. * * @param array<\TNP_Mailer_Message> $messages Messages to send. * * @return bool|WP_Error True on success, WP_Error on fatal error. */ public function send_chunk( $messages ) { $mailer = $this->get_phpmailer(); $undelivered = 0; $fatal_error = null; foreach ( $messages as $index => $message ) { try { // Clear previous message data but keep connection. $mailer->clearAddresses(); $mailer->clearReplyTos(); $mailer->clearAttachments(); $mailer->clearCustomHeaders(); $this->prepare_message( $mailer, $message ); // Check rate limits. if ( ! $this->check_rate_limits() ) { $message->error = __( 'Rate limit exceeded', 'robotstxt-smtp-newsletter' ); $fatal_error = new \WP_Error( self::ERROR_FATAL, __( 'Rate limit exceeded. Batch stopped.', 'robotstxt-smtp-newsletter' ) ); break; // Stop on rate limit. } $mailer->send(); ++$this->emails_sent_in_connection; $this->track_email_sent(); // Reconnect if threshold reached. if ( $this->emails_sent_in_connection >= $this->max_emails_per_connection ) { $this->close_connection(); $mailer = $this->get_phpmailer(); // Get fresh connection. } } catch ( \Exception $e ) { $this->log_error( $message, $e->getMessage() ); $message->error = $e->getMessage(); ++$undelivered; // Only stop on critical SMTP errors (connection, auth). // Continue sending if only individual recipient failed. $error_msg = $e->getMessage(); $is_fatal = ( stripos( $error_msg, 'Could not connect' ) !== false || stripos( $error_msg, 'MAIL FROM command failed' ) !== false || stripos( $error_msg, 'Authentication failed' ) !== false || stripos( $error_msg, 'Invalid HELO' ) !== false ); if ( $is_fatal ) { $fatal_error = new \WP_Error( self::ERROR_FATAL, $e->getMessage() ); break; // Stop batch on critical errors. } // Otherwise continue with next recipient. } } if ( $fatal_error ) { return $fatal_error; } if ( $undelivered > 0 ) { return new \WP_Error( self::ERROR_GENERIC, sprintf( '%d emails undelivered', $undelivered ) ); } return true; } /** * Prepare message for sending. * * @param \PHPMailer\PHPMailer\PHPMailer $mailer PHPMailer instance. * @param \TNP_Mailer_Message $message Message to prepare. * * @return void * @throws \PHPMailer\PHPMailer\Exception If preparation fails. */ private function prepare_message( $mailer, $message ) { // Use FROM configured in ROBOTSTXT SMTP plugin (required for SMTP auth). // Falls back to Newsletter's FROM if not configured. $from_email = ! empty( $this->settings['from_email'] ) ? $this->settings['from_email'] : $message->from; $from_name = ! empty( $this->settings['from_name'] ) ? $this->settings['from_name'] : $message->from_name; $mailer->setFrom( $from_email, $from_name ); $mailer->addAddress( $message->to ); $mailer->Subject = $message->subject; if ( ! empty( $message->body ) ) { $mailer->isHTML( true ); $mailer->Body = $message->body; if ( ! empty( $message->body_text ) ) { $mailer->AltBody = $message->body_text; } } else { $mailer->isHTML( false ); $mailer->Body = $message->body_text; } // Reply-To. if ( ! empty( $this->settings['reply_to_email'] ) ) { $mailer->addReplyTo( $this->settings['reply_to_email'], $this->settings['reply_to_name'] ?? '' ); } // Custom headers. if ( ! empty( $message->headers ) && is_array( $message->headers ) ) { foreach ( $message->headers as $key => $value ) { $mailer->addCustomHeader( $key, $value ); } } } /** * Check rate limits using robotstxt-smtp logic. * * @return bool True if sending is allowed, false otherwise. */ private function check_rate_limits() { // Rate limiting will be implemented using robotstxt-smtp methods. // For now, return true to allow sending. // TODO: Integrate with robotstxt-smtp rate limiting system. return true; } /** * Track sent email for rate limiting. * * @return void */ private function track_email_sent() { // Track email for rate limiting. // TODO: Integrate with robotstxt-smtp rate limiting tracking. } /** * Close SMTP connection. * * @return void */ private function close_connection() { if ( $this->phpmailer !== null ) { $this->phpmailer->smtpClose(); $this->phpmailer = null; $this->emails_sent_in_connection = 0; } } /** * Log error to PHP error log. * * @param \TNP_Mailer_Message $message Message that failed. * @param string $error_message Error message. * * @return void */ private function log_error( $message, $error_message ) { error_log( sprintf( 'ROBOTSTXT SMTP Newsletter: Failed to send to %s. Error: %s', $message->to, $error_message ) ); } /** * Cleanup on destruction. */ public function __destruct() { $this->close_connection(); } }