emails = $emails; $this->settings = $settings; } /** * {@inheritdoc} */ public function register(): void { add_filter( 'cron_schedules', array( $this, 'add_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval -- Five minutes keeps the queue moving without hammering shared hosting. add_action( 'init', array( $this, 'schedule' ) ); add_action( self::HOOK, array( $this, 'run' ) ); add_filter( 'hostkhata_cron_hooks', array( $this, 'cron_hooks' ) ); add_action( 'wp_mail_failed', array( $this, 'remember_error' ) ); } /** * Adds the five-minute schedule. * * @since 0.1.0 * * @param array> $schedules Schedules. * @return array> */ public function add_schedule( array $schedules ): array { $schedules[ self::SCHEDULE ] = array( 'interval' => 5 * MINUTE_IN_SECONDS, 'display' => __( 'Every five minutes', 'hostkhata' ), ); return $schedules; } /** * Schedules the cron event once. * * @since 0.1.0 * * @return void */ public function schedule(): void { if ( ! wp_next_scheduled( self::HOOK ) ) { wp_schedule_event( time() + MINUTE_IN_SECONDS, self::SCHEDULE, self::HOOK ); } } /** * Registers the hook for unscheduling on deactivation. * * @since 0.1.0 * * @param string[] $hooks Hooks. * @return string[] */ public function cron_hooks( array $hooks ): array { $hooks[] = self::HOOK; return $hooks; } /** * Remembers why wp_mail() failed. * * @since 0.1.0 * * @param WP_Error $error Error. * @return void */ public function remember_error( WP_Error $error ): void { $this->last_error = $error->get_error_message(); } /** * Sends one batch. * * @since 0.1.0 * * @return array{sent: int, failed: int, retry: int} */ public function run(): array { $result = array( 'sent' => 0, 'failed' => 0, 'retry' => 0, ); foreach ( $this->emails->claim_due( (int) $this->settings->get( 'email_batch_size' ) ) as $email ) { $error = $this->send( $email ); if ( '' === $error ) { $this->emails->log( $email, 'sent' ); $this->emails->delete( $email['id'] ); ++$result['sent']; } elseif ( $email['attempts'] >= self::MAX_ATTEMPTS ) { $this->emails->update( $email['id'], array( 'status' => 'failed', 'last_error' => $error, ) ); $this->emails->log( $email, 'failed', $error ); ++$result['failed']; } else { $this->emails->update( $email['id'], array( 'status' => 'pending', 'last_error' => $error, 'send_after_gmt' => gmdate( 'Y-m-d H:i:s', time() + $email['attempts'] * 15 * MINUTE_IN_SECONDS ), ) ); ++$result['retry']; } } $this->emails->purge_log( self::LOG_DAYS ); /** This action is documented in src/Domain/Billing/BillingRunner.php. */ do_action( 'hostkhata_cron_ran', 'email_queue', $result ); return $result; } /** * Puts a failed email back on the queue. * * @since 0.1.0 * * @param int $id Queue id. * @return bool False when the email is not failed. */ public function retry( int $id ): bool { $email = $this->emails->find( $id ); if ( null === $email || 'failed' !== $email['status'] ) { return false; } return $this->emails->update( $id, array( 'status' => 'pending', 'attempts' => 0, 'send_after_gmt' => gmdate( 'Y-m-d H:i:s' ), ) ); } /** * Sends one email. * * @param array $email Queue row. * @return string Empty on success, otherwise the error message. */ private function send( array $email ): string { $from_name = (string) $this->settings->get( 'email_from_name' ); $from_address = (string) $this->settings->get( 'email_from_address' ); $headers = array( 'Content-Type: text/html; charset=UTF-8' ); if ( is_email( $from_address ) ) { $headers[] = sprintf( 'From: %s <%s>', str_replace( array( "\r", "\n", '<', '>' ), '', $from_name ), $from_address ); } $to = '' !== $email['to_name'] ? sprintf( '%s <%s>', str_replace( array( "\r", "\n", '<', '>', ',' ), '', $email['to_name'] ), $email['to_email'] ) : $email['to_email']; $this->alt_body = $email['body_text']; $this->last_error = ''; add_action( 'phpmailer_init', array( $this, 'add_alt_body' ) ); $sent = wp_mail( $to, $email['subject'], $email['body_html'], $headers ); remove_action( 'phpmailer_init', array( $this, 'add_alt_body' ) ); if ( $sent ) { return ''; } return '' !== $this->last_error ? $this->last_error : __( 'The mail server did not accept the message.', 'hostkhata' ); } /** * Adds the plain-text alternative to the message being sent. * * @since 0.1.0 * * @param \PHPMailer\PHPMailer\PHPMailer $mailer Mailer. * @return void */ public function add_alt_body( $mailer ): void { $mailer->AltBody = $this->alt_body; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHPMailer property. } }