* @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 ); } }