PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.1
WooCommerce v10.9.1
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / Email / EmailLogger.php
woocommerce / src / Internal / Email Last commit date
DeferredEmailQueue.php 4 weeks ago EmailColors.php 11 months ago EmailFont.php 1 year ago EmailLogger.php 4 weeks ago EmailStyleSync.php 11 months ago OrderPriceFormatter.php 1 year ago
EmailLogger.php
395 lines
1 <?php
2 declare( strict_types = 1 );
3
4 namespace Automattic\WooCommerce\Internal\Email;
5
6 use Automattic\WooCommerce\Internal\Orders\OrderNoteGroup;
7 use Automattic\WooCommerce\Internal\RegisterHooksInterface;
8 use WC_Email;
9 use WC_Log_Levels;
10 use WC_Order;
11 use WC_Product;
12 use WP_Error;
13 use WP_User;
14
15 /**
16 * Logs transactional email send attempts so store owners can inspect what WooCommerce attempted locally.
17 *
18 * Records are written to the WooCommerce logger under the `transactional-emails` source and include the email type,
19 * related object, recipient identifier, and the local send state. The recipient is logged as the WordPress username
20 * when the address is linked to an account, or as 'guest' for unrecognised addresses. Failure reasons are captured
21 * from wp_mail_failed.
22 *
23 * @since 10.9.0
24 * @internal
25 */
26 class EmailLogger implements RegisterHooksInterface {
27
28 /**
29 * Logger source used for all email log entries.
30 */
31 private const LOG_SOURCE = 'transactional-emails';
32
33 /**
34 * Holds the PHPMailer error message from the most recent failed wp_mail() call.
35 *
36 * @var string|null
37 */
38 private ?string $last_mail_error = null;
39
40 /**
41 * Register hooks.
42 *
43 * @return void
44 */
45 public function register(): void {
46 add_action( 'wp_mail_failed', array( $this, 'capture_mail_error' ), 10, 1 );
47 add_action( 'woocommerce_email_sent', array( $this, 'handle_woocommerce_email_sent' ), 10, 3 );
48 add_action( 'woocommerce_email_disabled', array( $this, 'handle_woocommerce_email_disabled' ), 10, 2 );
49 add_action( 'woocommerce_email_skipped', array( $this, 'handle_woocommerce_email_skipped' ), 10, 3 );
50 }
51
52 /**
53 * Capture the PHPMailer error from a failed wp_mail() call so it can be included in the log entry.
54 *
55 * Error attribution is best-effort: wp_mail_failed is a global hook, so any plugin's failed
56 * wp_mail() call will set $last_mail_error. The trailing edge is controlled — $last_mail_error
57 * is cleared immediately after each WooCommerce send — but the leading edge is unbounded: a
58 * non-WooCommerce wp_mail_failed fired before a WooCommerce send failure will be attributed
59 * to that WooCommerce send. This may produce misleading error reasons in stores where other
60 * plugins also call wp_mail().
61 *
62 * @param WP_Error $error The error returned by wp_mail.
63 * @return void
64 */
65 public function capture_mail_error( WP_Error $error ): void {
66 $this->last_mail_error = $error->get_error_message();
67 }
68
69 /**
70 * Handle the woocommerce_email_sent action.
71 *
72 * @param bool $success Whether the email was sent successfully.
73 * @param string $email_id The email type ID (e.g. `customer_processing_order`).
74 * @param WC_Email $email The WC_Email instance.
75 * @return void
76 */
77 public function handle_woocommerce_email_sent( $success, string $email_id, WC_Email $email ): void {
78 /**
79 * Filter whether to log this transactional email attempt.
80 *
81 * Return false to skip logging for a particular email or globally.
82 *
83 * @since 10.9.0
84 *
85 * @param bool $enabled Whether logging is enabled.
86 * @param string $email_id The email type ID.
87 * @param WC_Email $email The WC_Email instance.
88 */
89 if ( ! apply_filters( 'woocommerce_email_log_enabled', true, $email_id, $email ) ) {
90 $this->last_mail_error = null;
91 return;
92 }
93
94 $object_context = $this->get_object_context( $email->object );
95 $object_label = isset( $object_context['type'], $object_context['id'] )
96 ? sprintf( ' for %s #%d', $object_context['type'], $object_context['id'] )
97 : '';
98 $last_mail_error = $this->last_mail_error;
99
100 $this->last_mail_error = null;
101
102 $context = array(
103 'source' => self::LOG_SOURCE,
104 'email_type' => $email_id,
105 'status' => $success ? 'sent' : 'failed',
106 'recipient' => $this->resolve_recipient( $email->get_recipient() ),
107 );
108
109 if ( ! empty( $object_context ) ) {
110 $context[ $object_context['type'] ] = $object_context['id'] ?? null;
111 }
112
113 /**
114 * Filter the context array logged for each transactional email attempt.
115 *
116 * @since 10.9.0
117 *
118 * @param array $context The context array to be logged.
119 * @param string $email_id The email type ID.
120 * @param WC_Email $email The WC_Email instance.
121 */
122 $context = (array) apply_filters( 'woocommerce_email_log_context', $context, $email_id, $email );
123
124 $type_label = ! empty( $context['is_test'] ) ? 'Test email' : 'Email';
125
126 if ( $success ) {
127 $message = sprintf( '%s "%s"%s sent', $type_label, $email_id, $object_label );
128 } else {
129 $reason = $last_mail_error ? ': ' . $this->redact_emails( $last_mail_error ) : '';
130 $message = sprintf( '%s "%s"%s failed to send%s', $type_label, $email_id, $object_label, $reason );
131 }
132
133 $level = $success ? WC_Log_Levels::INFO : WC_Log_Levels::WARNING;
134 wc_get_logger()->log( $level, $message, $context );
135
136 $this->maybe_add_order_note( $email->object, $email_id, $email, (bool) $success, $last_mail_error );
137 }
138
139 /**
140 * Add a private order note when a transactional email is sent or fails for an order.
141 *
142 * Accepts mixed input because $email->object is loosely typed (any object the email subclass attaches),
143 * and we narrow to WC_Order at the top of the method before doing anything with it.
144 *
145 * @param mixed $wc_object The email's related object, or false/null when none is set.
146 * @param string $email_id The email type ID (e.g. `customer_processing_order`).
147 * @param WC_Email $email The WC_Email instance.
148 * @param bool $success Whether the email was sent successfully.
149 * @param string|null $error_reason The error message from wp_mail_failed, or null.
150 * @return void
151 */
152 private function maybe_add_order_note( $wc_object, string $email_id, WC_Email $email, bool $success, ?string $error_reason ): void {
153 if ( ! $wc_object instanceof WC_Order || ! $wc_object->get_object_read() ) {
154 return;
155 }
156
157 /**
158 * Filter whether to add an order note for this transactional email attempt.
159 *
160 * Return false to suppress the order note for a particular email or globally,
161 * while still allowing the WooCommerce logger entry to be written.
162 *
163 * @since 10.9.0
164 *
165 * @param bool $enabled Whether to add the order note.
166 * @param string $email_id The email type ID.
167 * @param WC_Email $email The WC_Email instance.
168 * @param WC_Order $order The order the note would be added to.
169 */
170 if ( ! apply_filters( 'woocommerce_email_log_add_order_note', true, $email_id, $email, $wc_object ) ) {
171 return;
172 }
173
174 $email_title = $email->get_title();
175 $email_label = '' !== $email_title ? $email_title : $email_id;
176
177 if ( $success ) {
178 $note = sprintf(
179 /* translators: %s: Email title or type identifier */
180 __( 'Email "%s" sent.', 'woocommerce' ),
181 $email_label
182 );
183 } elseif ( $error_reason ) {
184 $note = sprintf(
185 /* translators: 1: Email title or type identifier, 2: Error reason */
186 __( 'Email "%1$s" failed to send: %2$s.', 'woocommerce' ),
187 $email_label,
188 $this->redact_emails( $error_reason )
189 );
190 } else {
191 $note = sprintf(
192 /* translators: %s: Email title or type identifier */
193 __( 'Email "%s" failed to send.', 'woocommerce' ),
194 $email_label
195 );
196 }
197
198 $wc_object->add_order_note( $note, 0, false, array( 'note_group' => OrderNoteGroup::EMAIL_NOTIFICATION ) );
199 }
200
201 /**
202 * Handle the woocommerce_email_disabled action.
203 *
204 * @param string $email_id The email type ID (e.g. `customer_processing_order`).
205 * @param WC_Email $email The WC_Email instance.
206 * @return void
207 */
208 public function handle_woocommerce_email_disabled( string $email_id, WC_Email $email ): void {
209 $this->log_non_send_outcome( $email_id, $email, 'disabled' );
210 }
211
212 /**
213 * Handle the woocommerce_email_skipped action.
214 *
215 * @param string $reason Short identifier for why the email was skipped (e.g. 'no_recipient').
216 * @param string $email_id The email type ID (e.g. `new_order`).
217 * @param WC_Email $email The WC_Email instance.
218 * @return void
219 */
220 public function handle_woocommerce_email_skipped( string $reason, string $email_id, WC_Email $email ): void {
221 $this->log_non_send_outcome( $email_id, $email, 'skipped', $reason );
222 }
223
224 /**
225 * Write a log entry for an email that was not sent (disabled or skipped).
226 *
227 * Centralises the shared logic for disabled and skipped outcomes so that the context
228 * schema (`source`, `email_type`, `status`, `reason`, `recipient`, object key) is
229 * defined in exactly one place. Future additions (e.g. a `correlation_id` field) only
230 * need to be made here.
231 *
232 * @param string $email_id The email type ID.
233 * @param WC_Email $email The WC_Email instance.
234 * @param string $status The outcome status: 'disabled' or 'skipped'.
235 * @param string|null $reason Optional reason identifier (only set for 'skipped' status).
236 * @return void
237 */
238 private function log_non_send_outcome( string $email_id, WC_Email $email, string $status, ?string $reason = null ): void {
239 /**
240 * Filter whether to log this transactional email attempt.
241 *
242 * This filter is documented in src/Internal/Email/EmailLogger.php
243 *
244 * @since 10.9.0
245 */
246 if ( ! apply_filters( 'woocommerce_email_log_enabled', true, $email_id, $email ) ) {
247 return;
248 }
249
250 $object_context = $this->get_object_context( $email->object );
251 $object_label = isset( $object_context['type'], $object_context['id'] )
252 ? sprintf( ' for %s #%d', $object_context['type'], $object_context['id'] )
253 : '';
254
255 if ( 'disabled' === $status ) {
256 $message = sprintf( 'Email "%s"%s not sent: email type is disabled', $email_id, $object_label );
257 } else {
258 $message = sprintf( 'Email "%s"%s not sent: %s', $email_id, $object_label, $reason );
259 }
260
261 $context = array(
262 'source' => self::LOG_SOURCE,
263 'email_type' => $email_id,
264 'status' => $status,
265 'recipient' => $this->resolve_recipient( $email->get_recipient() ),
266 );
267
268 if ( null !== $reason ) {
269 $context['reason'] = $reason;
270 }
271
272 if ( ! empty( $object_context ) ) {
273 $context[ $object_context['type'] ] = $object_context['id'] ?? null;
274 }
275
276 /**
277 * Filter the context array logged for each transactional email attempt.
278 *
279 * This filter is documented in src/Internal/Email/EmailLogger.php
280 *
281 * @since 10.9.0
282 */
283 $context = (array) apply_filters( 'woocommerce_email_log_context', $context, $email_id, $email );
284
285 wc_get_logger()->log( WC_Log_Levels::NOTICE, $message, $context );
286 }
287
288 /**
289 * Resolve a recipient email string to an identifier safe for logging.
290 *
291 * Each address is mapped to the corresponding WordPress username when an account
292 * exists, or to the string 'guest' for addresses with no associated account.
293 * This avoids storing plain email addresses in logs while still giving support
294 * teams a useful identifier for troubleshooting.
295 *
296 * @param string $recipient Comma-separated recipient email string from WC_Email::get_recipient().
297 * @return string Comma-separated usernames or 'guest' labels.
298 */
299 private function resolve_recipient( string $recipient ): string {
300 if ( '' === $recipient ) {
301 return 'guest';
302 }
303
304 $labels = array_map(
305 function ( string $email ): string {
306 $user = get_user_by( 'email', trim( $email ) );
307 return $user instanceof WP_User ? $user->user_login : 'guest';
308 },
309 explode( ',', $recipient )
310 );
311
312 return implode( ', ', $labels );
313 }
314
315 /**
316 * Replace any email addresses in a log message fragment with `[redacted_email]`.
317 *
318 * PHPMailer / SMTP error strings frequently embed the recipient address
319 * (e.g. "SMTP Error: Could not send to foo@example.com"). Without redaction,
320 * the address would be written into the log message and — when the database
321 * log handler is active — surface in WC > Status > Logs to anyone with
322 * `manage_woocommerce`, defeating the username/`guest` resolution applied
323 * to the `recipient` context field.
324 *
325 * Mirrors the regex used by RemoteLogger::redact_user_data() so the privacy
326 * posture stays consistent across loggers.
327 *
328 * @param string $message The message fragment to scrub.
329 * @return string The fragment with any email addresses replaced.
330 */
331 private function redact_emails( string $message ): string {
332 return (string) preg_replace( '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', '[redacted_email]', $message );
333 }
334
335 /**
336 * Extract loggable context from the WooCommerce object attached to the email.
337 *
338 * Returns a stable short type identifier rather than the raw class name so that log aggregation
339 * is not brittle across subclasses (e.g. WC_Order_Refund still returns type 'order').
340 *
341 * @param mixed $wc_object The email's related object (WC_Order, WC_Product, WP_User, etc.) or false/null.
342 * @return array{type: string, id?: int}|array{} Type and (when resolvable) ID of the object, or empty when no object is set.
343 */
344 private function get_object_context( $wc_object ): array {
345 if ( ! is_object( $wc_object ) ) {
346 return array();
347 }
348
349 if ( $wc_object instanceof WC_Order ) {
350 $type = 'order';
351 } elseif ( $wc_object instanceof WC_Product ) {
352 $type = 'product';
353 } elseif ( $wc_object instanceof WP_User ) {
354 $type = 'user';
355 } else {
356 $type = get_class( $wc_object );
357 }
358
359 $id = null;
360 if ( $wc_object instanceof WC_Order || $wc_object instanceof WC_Product ) {
361 // Both have an explicit get_id() — safe to call directly.
362 $id = (int) $wc_object->get_id();
363 } elseif ( $wc_object instanceof WP_User ) {
364 // WP_User has no get_id() method; __call() returns false for unknown methods,
365 // which casts to 0 and bypasses the ID-property fallback below.
366 $id = (int) $wc_object->ID;
367 } elseif ( method_exists( $wc_object, 'get_id' ) ) {
368 try {
369 $method = new \ReflectionMethod( $wc_object, 'get_id' );
370 if ( 0 === $method->getNumberOfRequiredParameters() ) {
371 $id = (int) $wc_object->get_id();
372 }
373 } catch ( \Throwable $e ) {
374 $id = null;
375 }
376 }
377
378 if ( null === $id ) {
379 $public_props = get_object_vars( $wc_object );
380 if ( array_key_exists( 'ID', $public_props ) ) {
381 $id = (int) $public_props['ID'];
382 }
383 }
384
385 if ( null === $id ) {
386 return array( 'type' => $type );
387 }
388
389 return array(
390 'type' => $type,
391 'id' => $id,
392 );
393 }
394 }
395