| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- <?php
- /**
- * Writes the order note and WC log line that EmailLogger was suppressed from
- * writing at enqueue time.
- *
- * This lives apart from the worker because it must be reachable from EVERY
- * terminal state, not just the two the worker owns. A queue row can reach
- * `sent` or `failed` from four places:
- *
- * Worker::on_success() mail went out
- * Worker::on_failure() max_attempts exhausted
- * Worker::send() payload was corrupt, unsendable
- * Reaper::recover_crashed() max_reclaims exhausted
- *
- * Miss any of them and the order carries no note and `transactional-emails`
- * carries no line: the customer never received the email and the shop's audit
- * trail says nothing happened at all. That is the same defect as a queue which
- * lies about delivery, inverted from a false positive into a silent hole.
- *
- * @package studiou-wc-mail-queue
- * @copyright 2026 QUADARAX
- * @author Dalibor Votruba <dvotruba@quadarax.com>
- * @license GPL-2.0-or-later
- */
- defined( 'ABSPATH' ) || exit;
- /**
- * Class Studiou_WCMQ_Notifier
- */
- class Studiou_WCMQ_Notifier {
- const LOG_SOURCE = 'transactional-emails';
- /**
- * Record the real outcome of a queued mail.
- *
- * Never throws: callers invoke this after the row has already been committed,
- * and a bookkeeping failure must not undo a send or re-open a resolved row.
- *
- * @param object $row Queue row.
- * @param bool $success Whether the mail went out.
- * @param string|null $error Error message, if any.
- */
- public static function annotate( $row, $success, $error = null ) {
- try {
- self::write( $row, (bool) $success, self::redact_emails( (string) $error ) );
- } catch ( Throwable $e ) {
- Studiou_WCMQ_DB::log( 'warning', 'Could not record mail outcome: ' . $e->getMessage(), (int) $row->id );
- }
- }
- /**
- * Order note + WC log line.
- *
- * @param object $row Queue row.
- * @param bool $success Outcome.
- * @param string $error Redacted error message.
- */
- private static function write( $row, $success, $error ) {
- $order_id = (int) $row->order_id;
- if ( $order_id > 0 ) {
- // order_id > 0 does not mean the order still exists. An order trashed
- // and emptied between enqueue and send makes wc_get_order() return
- // false, and false->add_order_note() is a fatal Error.
- $order = wc_get_order( $order_id );
- if ( $order instanceof WC_Order ) {
- if ( $success ) {
- /* translators: 1: email type, 2: recipient */
- $note = sprintf( __( 'Queued email "%1$s" sent to %2$s.', 'studiou-wc-mail-queue' ), $row->context, $row->recipient );
- } else {
- /* translators: 1: email type, 2: error message */
- $note = sprintf( __( 'Queued email "%1$s" failed to send: %2$s', 'studiou-wc-mail-queue' ), $row->context, $error );
- }
- // Groups with WooCommerce's own email notes in the order-notes UI.
- // The value is the literal behind OrderNoteGroup::EMAIL_NOTIFICATION;
- // that class is in the Internal\ namespace, so never depend on it.
- // The $meta_data parameter postdates our declared WC floor — PHP
- // drops extra arguments to a userland method, so an older signature
- // simply ignores it.
- $order->add_order_note( $note, 0, false, array( 'note_group' => 'email_notification' ) );
- }
- }
- if ( ! function_exists( 'wc_get_logger' ) ) {
- return;
- }
- // We suppressed EmailLogger for this mail, so we inherit its privacy
- // contract. The `transactional-emails` log is readable by anyone with
- // manage_woocommerce, and WooCommerce deliberately never writes raw
- // recipient addresses or raw PHPMailer error strings into it. The
- // plugin's own queue table holds the real address; this log must not.
- $context = array(
- 'source' => self::LOG_SOURCE,
- 'email_type' => $row->context,
- 'status' => $success ? 'sent' : 'failed',
- 'recipient' => self::resolve_recipient( (string) $row->recipient ),
- );
- if ( $order_id > 0 ) {
- $context['order'] = $order_id;
- }
- $logger = wc_get_logger();
- if ( $success ) {
- $logger->info( sprintf( 'Queued email "%s" sent.', $row->context ), $context );
- } else {
- $logger->error( sprintf( 'Queued email "%s" failed: %s', $row->context, $error ), $context );
- }
- }
- /**
- * Map recipient addresses to usernames, or 'guest'.
- *
- * Mirrors EmailLogger::resolve_recipient().
- *
- * @param string $recipient Comma-separated addresses.
- * @return string
- */
- public static function resolve_recipient( $recipient ) {
- if ( '' === $recipient ) {
- return 'guest';
- }
- $labels = array();
- foreach ( explode( ',', $recipient ) as $address ) {
- $user = get_user_by( 'email', trim( $address ) );
- $labels[] = $user instanceof WP_User ? $user->user_login : 'guest';
- }
- return implode( ', ', $labels );
- }
- /**
- * Strip email addresses out of a message.
- *
- * Mirrors EmailLogger::redact_emails(). PHPMailer error strings routinely
- * embed the recipient.
- *
- * @param string $message Message.
- * @return string
- */
- public static function redact_emails( $message ) {
- return (string) preg_replace(
- '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/',
- '[redacted_email]',
- $message
- );
- }
- }
|