get( 'to' ); $to = array_shift( $to )->get_address(); $message = ''; if ( 'html' === $email_class->get( 'content_type' ) ) { self::$is_html = 1; $message = $email_class->get( 'content_html', 'replace-tokens' ); } else { self::$is_html = 0; $message = $email_class->get( 'content_plaintext', 'replace-tokens' ); } $bt_segment = self::get_backtrace(); $plugin_slug = ''; if ( isset( $bt_segment['file'] ) ) { $plugin_base = Plugin_Theme_Helper::get_plugin_from_file_path( $bt_segment['file'] ); if ( $plugin_base ) { $plugin_slug = $plugin_base; } } // Extract CC, BCC, Reply-To from BuddyPress headers. $headers = $email_class->get( 'headers' ); $extra_recipients = self::extract_extra_recipients( is_array( $headers ) ? $headers : array() ); // Calculate message size. $message_size = strlen( $message ); // Categorize the email. $email_category = self::categorize_email( $email_class->get( 'subject', 'replace-tokens' ), $message, $bt_segment ); // Determine if email can be resent. $can_resend = self::can_resend_email( $email_class->get( 'subject', 'replace-tokens' ), $message ) ? 1 : 0; self::$bp_mail = array( 'time' => time(), 'email_to' => self::filter_html( (string) $to ), 'subject' => self::filter_html( $email_class->get( 'subject', 'replace-tokens' ) ), 'message' => self::filter_html( $message ), 'backtrace_segment' => \wp_json_encode( self::get_backtrace() ), 'plugin_slug' => $plugin_slug, 'status' => 1, 'attachments' => \wp_json_encode( self::get_attachment_locations( array() ) ), 'additional_headers' => \wp_json_encode( $email_class->get( 'headers' ) ), 'is_html' => (int) self::$is_html, 'blog_id' => (int) \get_current_blog_id(), 'email_cc' => $extra_recipients['cc'], 'email_bcc' => $extra_recipients['bcc'], 'email_reply_to' => $extra_recipients['reply_to'], 'message_size' => $message_size, 'total_size' => $message_size, // BuddyPress emails typically have no attachments. 'attachment_count' => 0, 'attachment_total_size' => 0, 'delivery_time' => null, 'email_category' => $email_category, 'can_resend' => $can_resend, ); } } /** * Extracts all of the mail information and stores it in the DB * * @param array $args - Array with all of the mail arguments. * * @return void * * @since 3.0.0 */ public static function record_mail( $args ) { if ( \is_array( $args ) ) { $start_time = microtime( true ); $bt_segment = self::get_backtrace(); $plugin_slug = ''; if ( isset( $bt_segment['file'] ) ) { $plugin_base = Plugin_Theme_Helper::get_plugin_from_file_path( $bt_segment['file'] ); if ( $plugin_base ) { $plugin_slug = $plugin_base; } } // Extract CC, BCC, Reply-To from headers. $extra_recipients = self::extract_extra_recipients( $args['headers'] ); // Get attachment information. $attachments_data = self::get_attachment_locations( $args['attachments'] ); $attachment_stats = self::calculate_attachment_stats( $attachments_data ); // Calculate message size. $message_size = strlen( $args['message'] ); // Calculate total size (message + attachments). $total_size = $message_size + $attachment_stats['total_size']; // Categorize the email. $email_category = self::categorize_email( $args['subject'], $args['message'], $bt_segment ); // Determine if email can be resent. $can_resend = self::can_resend_email( $args['subject'], $args['message'] ) ? 1 : 0; $log_entry = array( 'time' => time(), 'email_to' => self::filter_html( self::array_to_string( $args['to'] ) ), 'subject' => self::filter_html( $args['subject'] ), 'message' => self::filter_html( $args['message'] ), 'backtrace_segment' => \wp_json_encode( self::get_backtrace() ), 'plugin_slug' => $plugin_slug, 'status' => 1, 'attachments' => \wp_json_encode( $attachments_data ), 'additional_headers' => \wp_json_encode( $args['headers'] ), 'is_html' => (int) self::$is_html, 'blog_id' => (int) \get_current_blog_id(), 'email_cc' => $extra_recipients['cc'], 'email_bcc' => $extra_recipients['bcc'], 'email_reply_to' => $extra_recipients['reply_to'], 'message_size' => $message_size, 'total_size' => $total_size, 'attachment_count' => $attachment_stats['count'], 'attachment_total_size' => $attachment_stats['total_size'], 'delivery_time' => null, // Will be updated in extract_more_mail_info. 'email_category' => $email_category, 'can_resend' => $can_resend, 'delivery_time' =>microtime( true ) - $start_time, ); self::$last_id = WP_Mail_Entity::insert( $log_entry ); } } /** * Tries to extract more information from the PHPMailer object. * * @param \PHPMailer $phpmailer - The PHPMailer initialized object from WP. * * @return void * * @since 3.0.1 */ public static function extract_more_mail_info( $phpmailer ) { if ( \property_exists( $phpmailer, 'From' ) && ! empty( $phpmailer->From ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( 0 === self::$last_id ) { // Someone is doing nasty things and killed all of the params passed to wp_mail hook - lets intercept directly then. $from = array(); $from['email'] = $phpmailer->From; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( \property_exists( $phpmailer, 'FromName' ) && ! empty( $phpmailer->FromName ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $from['name'] = $phpmailer->FromName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } // Access protected PHPMailer internals without using deprecated ReflectionProperty::setAccessible. $to = self::read_phpmailer_property( $phpmailer, 'to' ); $attachment = self::read_phpmailer_property( $phpmailer, 'attachment' ); $mail_header = self::read_phpmailer_property( $phpmailer, 'mailHeader' ); $bt_segment = self::get_backtrace(); $plugin_slug = ''; if ( isset( $phpmailer->Backtrace, $phpmailer->Backtrace['file'] ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $plugin_base = Plugin_Theme_Helper::get_plugin_from_file_path( $phpmailer->Backtrace['file'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( $plugin_base ) { $plugin_slug = $plugin_base; } } elseif ( isset( $bt_segment['file'] ) ) { $plugin_base = Plugin_Theme_Helper::get_plugin_from_file_path( $bt_segment['file'] ); if ( $plugin_base ) { $plugin_slug = $plugin_base; } } $log_entry = array( 'time' => time(), 'email_to' => self::filter_html( self::to_mail_get( $to ) ), 'email_from' => self::filter_html( self::array_to_string( $from ) ), 'subject' => self::filter_html( $phpmailer->Subject ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 'message' => self::filter_html( $phpmailer->Body ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 'backtrace_segment' => \wp_json_encode( self::get_backtrace() ), 'plugin_slug' => $plugin_slug, 'status' => 1, 'attachments' => \wp_json_encode( self::get_attachment_locations( $attachment ) ), 'additional_headers' => \wp_json_encode( $mail_header ), 'is_html' => ( 'text/html' === $phpmailer->ContentType ) ? 1 : 0, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 'blog_id' => (int) \get_current_blog_id(), ); // Extract CC, BCC, Reply-To from mail headers. $extra_recipients = self::extract_extra_recipients( $mail_header ); $log_entry['email_cc'] = $extra_recipients['cc']; $log_entry['email_bcc'] = $extra_recipients['bcc']; $log_entry['email_reply_to'] = $extra_recipients['reply_to']; // Calculate sizes and attachment stats. $attachments_data = self::get_attachment_locations( $attachment ); $attachment_stats = self::calculate_attachment_stats( $attachments_data ); $message_size = strlen( $phpmailer->Body ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $log_entry['message_size'] = $message_size; $log_entry['total_size'] = $message_size + $attachment_stats['total_size']; $log_entry['attachment_count'] = $attachment_stats['count']; $log_entry['attachment_total_size'] = $attachment_stats['total_size']; // Categorize and check resendability. $log_entry['email_category'] = self::categorize_email( $phpmailer->Subject, $phpmailer->Body, $bt_segment ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $log_entry['can_resend'] = self::can_resend_email( $phpmailer->Subject, $phpmailer->Body ) ? 1 : 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $log_entry['delivery_time'] = null; // Will be updated after email is sent. self::$last_id = WP_Mail_Entity::insert( $log_entry ); } else { $log_entry = WP_Mail_Entity::load( 'id=%d', array( self::$last_id ) ); $from = array(); $from['email'] = $phpmailer->From; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( \property_exists( $phpmailer, 'FromName' ) && ! empty( $phpmailer->FromName ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $from['name'] = $phpmailer->FromName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } $log_entry['email_from'] = self::filter_html( self::array_to_string( $from ) ); WP_Mail_Entity::insert( $log_entry ); } } } /** * Extracts "From" address information from Post SMTP plugin. * * Post SMTP replaces wp_mail() entirely and doesn't trigger phpmailer_init, * so we need to hook into its custom actions to capture the "From" address. * * @param object $log Post SMTP's PostmanEmailLog object (unused). * @param object $message Post SMTP's PostmanMessage object. * @param string $transcript Transport transcript (unused). * @param object $transport Post SMTP transport object (unused). * * @return void * * @since 4.7.1 */ public static function post_smtp_extract_from( $log, $message, $transcript = '', $transport = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed // Only proceed if we have a valid last_id from a previous mail log entry. if ( 0 === self::$last_id ) { return; } // Ensure the message object has the getFromAddress method. if ( ! is_object( $message ) || ! method_exists( $message, 'getFromAddress' ) ) { return; } $from_address = $message->getFromAddress(); // Ensure we have a valid PostmanEmailAddress object. if ( ! is_object( $from_address ) ) { return; } $from = array(); // Extract email address. if ( method_exists( $from_address, 'getEmail' ) ) { $from['email'] = $from_address->getEmail(); } // Extract name if available. if ( method_exists( $from_address, 'getName' ) ) { $name = $from_address->getName(); if ( ! empty( $name ) ) { $from['name'] = $name; } } // Update the log entry with the from address. if ( ! empty( $from ) ) { $log_entry = WP_Mail_Entity::load( 'id=%d', array( self::$last_id ) ); if ( ! empty( $log_entry ) ) { $log_entry['email_from'] = self::filter_html( self::array_to_string( $from ) ); WP_Mail_Entity::insert( $log_entry ); } } } /** * Records the error information for a failed email. * * @param \WP_Error $error - The error triggered. * * @return void * * @since 3.0.0 */ public static function record_error( $error ) { $log_entry = WP_Mail_Entity::load( 'id=%d', array( self::$last_id ) ); $log_entry['status'] = 0; $log_entry['error'] = \sanitize_text_field( (string) $error->get_error_message() ); WP_Mail_Entity::insert( $log_entry ); } /** * Stores class constant about the typo of the email - HTML or plain text. * * @param string $content_type - The current content type of the mail. * * @return string * * @since 3.0.0 */ public static function save_is_html( $content_type ) { self::$is_html = ( 'text/html' === $content_type ) ? 1 : 0; return $content_type; } /** * Filters HTML content of the mail. * * @param string $value - The mail body. * * @return string * * @since 3.0.0 */ public static function filter_html( $value ): string { $value = preg_replace( '~~s', '', (string) $value ); $value = htmlspecialchars_decode( (string) $value ); $string = \wp_kses( $value, self::get_allowed_tags() ); return $string; } /** * What tags are allowed in the content of the mail. * * @return array * * @since 3.0.0 */ private static function get_allowed_tags(): array { $tags = \wp_kses_allowed_html( 'post' ); $tags['style'] = array(); // Security hardening: do NOT allow