*/ private $settings = array(); /** * Batch size for bulk sending. * * @var int */ protected $batch_size = 50; /** * 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). // SES can handle larger batches than SMTP. if ( ! empty( $options['turbo'] ) ) { $this->batch_size = max( 1, (int) $options['turbo'] ); } } /** * 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; } /** * Get SES client instance. * * @return \Aws\SesV2\SesV2Client * @throws \Exception If SES client cannot be initialized. */ private function get_ses_client() { if ( $this->ses_client !== null ) { return $this->ses_client; } if ( ! class_exists( '\Robotstxt_SMTP_AmazonSES\Plugin' ) ) { throw new \Exception( __( 'Amazon SES plugin not available.', 'robotstxt-smtp-newsletter' ) ); } $ses_plugin = \Robotstxt_SMTP_AmazonSES\Plugin::get_instance(); $this->ses_client = $ses_plugin->get_ses_client( $this->settings['amazon_ses_access_key'], $this->settings['amazon_ses_secret_key'], $this->settings['amazon_ses_region'] ); return $this->ses_client; } /** * Send single email via SES API. * * @param \TNP_Mailer_Message $message Message to send. * * @return bool|WP_Error True on success, WP_Error on failure. */ public function send( $message ) { try { $client = $this->get_ses_client(); // 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' ) ); } $params = $this->prepare_ses_params( $message ); $result = $client->sendEmail( $params ); // Track rate limit. $this->track_email_sent(); return true; } catch ( \Aws\Exception\AwsException $e ) { $this->log_error( $message, $e->getAwsErrorMessage() ); return new \WP_Error( self::ERROR_GENERIC, $e->getAwsErrorMessage() ); } catch ( \Exception $e ) { $this->log_error( $message, $e->getMessage() ); return new \WP_Error( self::ERROR_GENERIC, $e->getMessage() ); } } /** * Send chunk of emails via SES API. * * Note: SES API doesn't have batch endpoint, so we send sequentially. * However, API calls are much faster than SMTP connections. * * @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 ) { $undelivered = 0; $fatal_error = null; foreach ( $messages as $message ) { try { $client = $this->get_ses_client(); // 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. } $params = $this->prepare_ses_params( $message ); $result = $client->sendEmail( $params ); // Track rate limit. $this->track_email_sent(); } catch ( \Aws\Exception\AwsException $e ) { $this->log_error( $message, $e->getAwsErrorMessage() ); $message->error = $e->getAwsErrorMessage(); ++$undelivered; // Only stop on critical AWS errors (credentials, rate limits). // Continue sending if only individual recipient failed (bounce, suppress list). $error_code = $e->getAwsErrorCode(); $is_fatal = in_array( $error_code, array( 'InvalidClientTokenId', 'SignatureDoesNotMatch', 'Throttling', 'RequestExpired' ), true ); if ( $is_fatal ) { $fatal_error = new \WP_Error( self::ERROR_FATAL, $e->getAwsErrorMessage() ); break; // Stop batch on critical errors. } // Otherwise continue with next recipient. } catch ( \Exception $e ) { $this->log_error( $message, $e->getMessage() ); $message->error = $e->getMessage(); ++$undelivered; // On unexpected errors, stop the batch. $fatal_error = new \WP_Error( self::ERROR_FATAL, $e->getMessage() ); break; } } if ( $fatal_error ) { return $fatal_error; } if ( $undelivered > 0 ) { return new \WP_Error( self::ERROR_GENERIC, sprintf( '%d emails undelivered', $undelivered ) ); } return true; } /** * Prepare SES API parameters from message. * * @param \TNP_Mailer_Message $message Message to prepare. * * @return array SES API parameters. */ private function prepare_ses_params( $message ) { $params = array( 'FromEmailAddress' => ! empty( $message->from_name ) ? sprintf( '%s <%s>', $message->from_name, $message->from ) : $message->from, 'Destination' => array( 'ToAddresses' => array( $message->to ), ), 'Content' => array( 'Simple' => array( 'Subject' => array( 'Data' => $message->subject, 'Charset' => 'UTF-8', ), 'Body' => array(), ), ), ); // Add HTML body. if ( ! empty( $message->body ) ) { $params['Content']['Simple']['Body']['Html'] = array( 'Data' => $message->body, 'Charset' => 'UTF-8', ); } // Add text body. if ( ! empty( $message->body_text ) ) { $params['Content']['Simple']['Body']['Text'] = array( 'Data' => $message->body_text, 'Charset' => 'UTF-8', ); } // Reply-To. if ( ! empty( $this->settings['reply_to_email'] ) ) { $params['ReplyToAddresses'] = array( $this->settings['reply_to_email'] ); } // Custom headers. if ( ! empty( $message->headers ) && is_array( $message->headers ) ) { $email_headers = array(); foreach ( $message->headers as $key => $value ) { $email_headers[] = array( 'Name' => $key, 'Value' => $value, ); } if ( ! empty( $email_headers ) ) { $params['Content']['Raw'] = array( 'Data' => $this->build_raw_message( $message, $email_headers ), ); unset( $params['Content']['Simple'] ); } } return $params; } /** * Build raw MIME message for custom headers. * * @param \TNP_Mailer_Message $message Message. * @param array $headers Custom headers. * * @return string Base64-encoded raw message. */ private function build_raw_message( $message, $headers ) { // This is a simplified implementation. // For production, consider using a proper MIME builder. $raw = "From: {$message->from}\r\n"; $raw .= "To: {$message->to}\r\n"; $raw .= "Subject: {$message->subject}\r\n"; foreach ( $headers as $header ) { $raw .= "{$header['Name']}: {$header['Value']}\r\n"; } $raw .= "MIME-Version: 1.0\r\n"; $raw .= "Content-Type: text/html; charset=UTF-8\r\n\r\n"; $raw .= $message->body; return base64_encode( $raw ); } /** * Check rate limits using SES quotas. * * @return bool True if sending is allowed, false otherwise. */ private function check_rate_limits() { // Rate limiting will be implemented using SES quota integration. // For now, return true to allow sending. // TODO: Integrate with robotstxt-smtp-amazonses quota checking. 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. } /** * Log error to Newsletter system. * * @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 (SES): Failed to send to %s. Error: %s', $message->to, $error_message ) ); if ( $logger ) { $logger->error( sprintf( 'Amazon SES: Failed to send to %s. Error: %s', $message->to, $error_message ) ); } } } }