\WC_Email ). The plugin's own come first; an add-on adds * its own. A class outside any namespace: WooCommerce reads the class name * back as the section of its settings screen and the type of its preview. */ const CLASSES_FILTER = 'careback_email_classes'; /** The plugin's own emails: class => file in includes/emails/. */ const OWN = array( 'Careback_Email_Request_Received' => 'class-careback-email-request-received.php', 'Careback_Email_Request_Status' => 'class-careback-email-request-status.php', 'Careback_Email_New_Request' => 'class-careback-email-new-request.php', ); /** The frame of every email, HTML (a shop's theme may override it in woocommerce/emails/). */ const TEMPLATE_HTML = 'emails/careback-email.php'; /** The frame of every email, plain text. */ const TEMPLATE_PLAIN = 'emails/plain/careback-email.php'; /** * Hooks the emails into WooCommerce's list. * * @return void */ public static function init() { add_filter( 'woocommerce_email_classes', array( __CLASS__, 'register' ) ); } /** * Adds the plugin's emails, and those of `careback_email_classes`, to * WooCommerce's. Each one under its class name, which is what WooCommerce * reads back; one WooCommerce already holds is left as it is. * * @param array $emails WooCommerce's emails, class name => \WC_Email. * @return array */ public static function register( $emails ) { $emails = is_array( $emails ) ? $emails : array(); if ( ! class_exists( 'WC_Email' ) ) { return $emails; } $own = array(); foreach ( self::OWN as $class => $file ) { if ( ! class_exists( $class, false ) ) { require_once CAREBACK_PATH . 'includes/emails/' . $file; } $own[ $class ] = new $class(); } /** * Filters the emails the plugin adds to WooCommerce → Settings → Emails. * See api/admin.md §5a. * * @param array $emails Class name => \WC_Email; the plugin's own first. */ $added = apply_filters( self::CLASSES_FILTER, $own ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_email_classes, the final prefix. // The plugin's own stay, first; what an add-on adds follows. $added = is_array( $added ) ? $own + $added : $own; foreach ( $added as $email ) { if ( ! $email instanceof \WC_Email ) { continue; } $class = get_class( $email ); if ( ! isset( $emails[ $class ] ) ) { $emails[ $class ] = $email; } } return $emails; } /** * One of the registered emails, as WooCommerce holds it. * * @param string $email_class Class name. * @return \WC_Email|null Null while WooCommerce's mailer is not there or does not hold it. */ public static function get( $email_class ) { if ( ! function_exists( 'WC' ) || ! class_exists( 'WC_Emails' ) ) { return null; } $mailer = WC()->mailer(); $emails = is_object( $mailer ) && method_exists( $mailer, 'get_emails' ) ? $mailer->get_emails() : array(); return isset( $emails[ $email_class ] ) && $emails[ $email_class ] instanceof \WC_Email ? $emails[ $email_class ] : null; } /** * Gives an email the plugin's frame: its HTML and plain-text templates (a * theme overrides them in woocommerce/emails/) and the group WooCommerce * lists it under where it groups its emails. Called before * \WC_Email::__construct(). * * @param \WC_Email $email The email. * @return void */ public static function frame( \WC_Email $email ) { $email->template_base = self::template_base(); $email->template_html = self::TEMPLATE_HTML; $email->template_plain = self::TEMPLATE_PLAIN; // WooCommerce 10.3 and later group their list; earlier ones have no such property. if ( property_exists( $email, 'email_group' ) ) { $email->email_group = 'order-changes'; } } /** * Folder holding the plugin's templates. * * @return string */ public static function template_base() { return CAREBACK_PATH . 'templates/'; } /** * The whole email: the heading, the body, the shop's additional content, * inside the shop's header and footer — or the same as plain text. * * @param \WC_Email $email The email. * @param string $html Body as HTML, already escaped. * @param string $plain_text Body as plain text. * @param bool $plain True for the plain-text version. * @return string */ public static function content( \WC_Email $email, $html, $plain_text, $plain ) { $template = $plain ? $email->template_plain : $email->template_html; $base = $email->template_base ? $email->template_base : self::template_base(); return wc_get_template_html( $template ? $template : ( $plain ? self::TEMPLATE_PLAIN : self::TEMPLATE_HTML ), array( 'email_heading' => $email->get_heading(), 'body' => $plain ? (string) $plain_text : (string) $html, 'additional_content' => $email->get_additional_content(), 'sent_to_admin' => ! $email->is_customer_email(), 'plain_text' => (bool) $plain, 'email' => $email, ), '', $base ); } /** * A request for WooCommerce's preview and test email: filed as a * withdrawal now, on the sample order the preview hands over (or an empty * one), with that order's lines and billing name, and its receipt code. * * @param mixed $about What the preview set on the email (its sample order). * @return array{request_id:int,order:\WC_Order,items:array,payload:array} */ public static function sample( $about ) { $order = $about instanceof \WC_Order ? $about : new \WC_Order(); $items = array(); foreach ( $order->get_items() as $line_id => $item ) { $items[] = array( 'line_item_id' => (int) $line_id, 'name' => (string) $item->get_name(), 'quantity' => max( 1, (int) $item->get_quantity() ), ); } if ( ! $items ) { $items[] = array( 'line_item_id' => 0, 'name' => __( 'Sample item', 'careback' ), 'quantity' => 1, ); } $name = trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ); $name = '' !== $name ? $name : Texts::get( 'email_name_fallback', careback()->settings() ); $now = current_time( 'mysql', true ); $id = 123; return array( 'request_id' => $id, 'order' => $order, 'items' => $items, 'payload' => array( 'type' => 'withdrawal', 'original_type' => '', 'name' => $name, 'reason' => '', 'comment' => '', 'received_at' => $now, 'receipt_code' => Receipt::code( array( 'id' => $id, 'order_number' => $order->get_order_number(), 'declaration_received_at' => $now, 'type' => 'withdrawal', 'customer_name' => $name, 'items' => $items, ) ), ), ); } /** * Sends an email the shop has switched on, to the given address or list. * * @param \WC_Email $email The email, with what it is about already set. * @param string|string[] $to Recipient, or recipients. * @return bool|null True when handed to the mailer, false when that failed or there is nobody to send to, null when the shop switched the email off. */ public static function send( \WC_Email $email, $to ) { if ( ! $email->is_enabled() ) { return null; } $to = is_array( $to ) ? array_values( array_filter( array_map( 'trim', $to ), 'is_email' ) ) : trim( (string) $to ); if ( empty( $to ) ) { return false; } return (bool) $email->send( $to, $email->get_subject(), $email->get_content(), $email->get_headers(), $email->get_attachments() ); } /** * Writes to WooCommerce's log (WooCommerce → Status → Logs, source * "careback") that an email could not be built, and why. * * @param \WC_Email $email The email. * @param \Throwable $error What went wrong. * @return void */ public static function log_failure( \WC_Email $email, \Throwable $error ) { if ( function_exists( 'wc_get_logger' ) ) { wc_get_logger()->error( sprintf( 'Email %s could not be built: %s', (string) $email->id, $error->getMessage() ), array( 'source' => 'careback' ) ); } } /** * Placeholders for the subject, heading and additional content the shop * writes in WooCommerce, each value escaped for the HTML they may land in * (subject() decodes a subject again). * * @param array $values Token without braces => value. * @return array `{token}` => value. */ public static function placeholders( array $values ) { $out = array(); foreach ( $values as $token => $value ) { $out[ '{' . $token . '}' ] = esc_html( Notifications::header_safe( (string) $value ) ); } return $out; } /** * A subject as a mail header carries it: entities decoded, on one line. * * @param string $subject Subject as WooCommerce built it. * @return string */ public static function subject( $subject ) { return Notifications::header_safe( html_entity_decode( (string) $subject, ENT_QUOTES, 'UTF-8' ) ); } /** * Lines as an HTML block. A non-empty line becomes a paragraph, a blank one * vertical space. A Trusted_Line is the shop's (or the plugin's) own and * passes through wp_kses_post with its markup; every other line is escaped * in full, so no markup in what a customer wrote reaches the message. * * @param array $lines Strings and Trusted_Line objects. * @return string */ public static function lines_html( array $lines ) { $html = ''; foreach ( $lines as $line ) { $trusted = $line instanceof Trusted_Line; $text = (string) $line; if ( '' === trim( $text ) ) { $html .= '
'; continue; } $html .= '

' . ( $trusted ? wp_kses_post( $text ) : esc_html( $text ) ) . '

'; } return $html; } /** * The same lines as plain text, one per line. A Trusted_Line loses its * markup; a link keeps its address. * * @param array $lines Strings and Trusted_Line objects. * @return string */ public static function lines_plain( array $lines ) { $out = array(); foreach ( $lines as $line ) { $text = $line instanceof Trusted_Line ? self::html_to_plain( (string) $line ) : (string) $line; $out[] = '' === trim( $text ) ? '' : trim( $text ); } return implode( "\n", $out ) . "\n"; } /** * HTML as plain text: paragraphs and line breaks become new lines, a link * becomes its text followed by its address. * * @param string $html HTML. * @return string */ public static function html_to_plain( $html ) { $text = (string) $html; $text = (string) preg_replace( '#]*href=(["\'])([^"\']*)\1[^>]*>(.*?)#is', '$3 ($2)', $text ); $text = (string) preg_replace( '##i', "\n", $text ); $text = (string) preg_replace( '##i', "\n", $text ); $text = wp_strip_all_tags( $text ); $text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' ); return trim( (string) preg_replace( "/\n{3,}/", "\n\n", (string) preg_replace( "/[ \t]+\n/", "\n", $text ) ) ); } /** * A moment as the emails write it: date and time in the shop's time zone, * with that zone's offset from UTC, e.g. "23.10.2026 12:15 (UTC+02:00)". * A fixed format rather than the site's display preference: a confirmation * has to state the moment unambiguously. * * @param int|string|null $moment Timestamp, a UTC date as stored ('Y-m-d H:i:s'), or null for now. * @return string */ public static function moment( $moment = null ) { if ( null === $moment || '' === $moment ) { $timestamp = time(); } elseif ( is_numeric( $moment ) ) { $timestamp = (int) $moment; } else { $timestamp = strtotime( (string) $moment . ' UTC' ); $timestamp = false === $timestamp ? time() : $timestamp; } $offset = (string) wp_date( 'P', $timestamp ); /* translators: a PHP date() format for the date and time a request arrived (29.10.2026 14:05): keep the letters, set the order and separators of your language. */ return wp_date( _x( 'd.m.Y H:i', 'date and time format (PHP date())', 'careback' ), $timestamp ) . ' (UTC' . ( '+00:00' === $offset ? '' : $offset ) . ')'; } /** * The request on the Requests screen. * * @param int $request_id Request. * @return string */ public static function request_url( $request_id ) { return add_query_arg( array( 'page' => 'careback', 's' => (int) $request_id, ), admin_url( 'admin.php' ) ); } /** * Headers with their reply-to replaced by the given person. * * @param string $headers Headers as WooCommerce built them. * @param string $name Name to show; reduced to one plain line. * @param string $address Address; headers are returned unchanged when it is not one. * @return string */ public static function reply_to( $headers, $name, $address ) { $address = sanitize_email( (string) $address ); if ( ! is_email( $address ) ) { return (string) $headers; } // A comma, quote or angle bracket would turn the name into a second address. $name = trim( (string) preg_replace( '/\s+/', ' ', str_replace( array( '"', ',', ';', '<', '>', '\\', '(', ')' ), ' ', Notifications::header_safe( $name ) ) ) ); $kept = array(); foreach ( preg_split( "/\r\n|\n|\r/", (string) $headers ) as $line ) { if ( '' === trim( $line ) || 0 === stripos( ltrim( $line ), 'reply-to:' ) ) { continue; } $kept[] = $line; } $kept[] = 'Reply-to: ' . ( '' !== $name ? $name . ' <' . $address . '>' : $address ); return implode( "\r\n", $kept ) . "\r\n"; } }