Commit 5e7c5748717 for woocommerce

commit 5e7c57487171c6988c0a282b26f2e691fc3e2a2c
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Fri Sep 11 09:49:19 2026 +0100

    Add configurable order withdrawal emails (#67799)

    * Add configurable order withdrawal emails

    * Add changelog entry for order withdrawal emails

    * Update versions and remove redundant functions

    * Return to public method

    * Ensure order withdrawal emails are available for direct processing

    * Normalize field data consistently

    * Move order withdrawal customizable emails

    * Add email preview data

    * Fix orderwithdrawal merge conflict issue

    * Remove if condition

    * Remove accidental inclusion of email WC_Email_Customer_Abandoned_Cart_Recovery

    * Fix tests

diff --git a/plugins/woocommerce/changelog/add-order-withdrawal-emails b/plugins/woocommerce/changelog/add-order-withdrawal-emails
new file mode 100644
index 00000000000..6dec8a1a602
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-order-withdrawal-emails
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add configurable customer and merchant emails for order withdrawal requests.
diff --git a/plugins/woocommerce/includes/class-wc-emails.php b/plugins/woocommerce/includes/class-wc-emails.php
index 88466c05046..c855da5dc0e 100644
--- a/plugins/woocommerce/includes/class-wc-emails.php
+++ b/plugins/woocommerce/includes/class-wc-emails.php
@@ -309,6 +309,10 @@ class WC_Emails {
 		if ( FeaturesUtil::feature_is_enabled( 'customer_review_request' ) ) {
 			$emails['WC_Email_Customer_Review_Request'] = __DIR__ . '/emails/class-wc-email-customer-review-request.php';
 		}
+		if ( FeaturesUtil::feature_is_enabled( 'order_withdrawal' ) ) {
+			$emails['WC_Email_Customer_Order_Withdrawal_Requested'] = __DIR__ . '/emails/class-wc-email-customer-order-withdrawal-requested.php';
+			$emails['WC_Email_Order_Withdrawal_Requested']          = __DIR__ . '/emails/class-wc-email-order-withdrawal-requested.php';
+		}

 		// Prime caches to reduce future queries.
 		wp_prime_option_caches(
diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-order-withdrawal-requested.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-order-withdrawal-requested.php
new file mode 100644
index 00000000000..59e5bd058ad
--- /dev/null
+++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-order-withdrawal-requested.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * Class WC_Email_Customer_Order_Withdrawal_Requested file.
+ *
+ * @package WooCommerce\Emails
+ */
+
+use Automattic\WooCommerce\Internal\OrderWithdrawal\Emails\OrderWithdrawalEmailDataFormatter;
+use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFormProcessor;
+
+defined( 'ABSPATH' ) || exit;
+
+if ( ! class_exists( 'WC_Email_Customer_Order_Withdrawal_Requested', false ) ) :
+
+	/**
+	 * Customer order withdrawal request email.
+	 *
+	 * @class   WC_Email_Customer_Order_Withdrawal_Requested
+	 * @package WooCommerce\Classes\Emails
+	 */
+	class WC_Email_Customer_Order_Withdrawal_Requested extends WC_Email {
+
+		/**
+		 * Form data submitted by the customer.
+		 *
+		 * @var array<string,string>
+		 */
+		public array $withdrawal_data = array();
+
+		/**
+		 * Submission timestamp.
+		 *
+		 * @var int
+		 */
+		public int $submitted_at = 0;
+
+		/**
+		 * Data formatter.
+		 *
+		 * @var OrderWithdrawalEmailDataFormatter
+		 */
+		private OrderWithdrawalEmailDataFormatter $formatter;
+
+		/**
+		 * Constructor.
+		 */
+		public function __construct() {
+			$this->id             = 'customer_order_withdrawal_requested';
+			$this->customer_email = true;
+			$this->title          = __( 'Order withdrawal request received', 'woocommerce' );
+			$this->description    = __( 'Sent to customers when their order withdrawal request is received.', 'woocommerce' );
+			$this->email_group    = 'order-changes';
+			$this->template_html  = 'emails/customer-order-withdrawal-requested.php';
+			$this->template_plain = 'emails/plain/customer-order-withdrawal-requested.php';
+			$this->placeholders   = array(
+				'{order_number}'       => '',
+				'{order_billing_name}' => '',
+			);
+			$this->formatter      = new OrderWithdrawalEmailDataFormatter();
+
+			parent::__construct();
+		}
+
+		/**
+		 * Get email subject.
+		 *
+		 * @return string
+		 */
+		public function get_default_subject() {
+			return __( 'We received your withdrawal request', 'woocommerce' );
+		}
+
+		/**
+		 * Get email heading.
+		 *
+		 * @return string
+		 */
+		public function get_default_heading() {
+			return __( 'We received your withdrawal request', 'woocommerce' );
+		}
+
+		/**
+		 * Default content to show below main email content.
+		 *
+		 * @return string
+		 */
+		public function get_default_additional_content() {
+			return __( 'We will review your request and contact you about next steps, including any refund due.', 'woocommerce' );
+		}
+
+		/**
+		 * Trigger the sending of this email.
+		 *
+		 * @param array<string,mixed> $data         Form data.
+		 * @param int                 $submitted_at Unix timestamp for the submission.
+		 * @return bool Whether the email was sent successfully.
+		 */
+		public function trigger( array $data, int $submitted_at ): bool {
+			$this->setup_locale();
+
+			$data = $this->formatter->normalize_withdrawal_data( $data );
+
+			if ( null === $data ) {
+				$this->restore_locale();
+
+				return false;
+			}
+
+			$this->object                               = (object) $data;
+			$this->withdrawal_data                      = $data;
+			$this->submitted_at                         = $submitted_at;
+			$this->recipient                            = $data[ OrderWithdrawalFormProcessor::FIELD_EMAIL ];
+			$this->placeholders['{order_number}']       = $data[ OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER ];
+			$this->placeholders['{order_billing_name}'] = $this->formatter->get_customer_name( $data );
+
+			$sent = $this->send_notification();
+
+			$this->restore_locale();
+
+			return $sent;
+		}
+
+		/**
+		 * Get content html.
+		 *
+		 * @return string
+		 */
+		public function get_content_html() {
+			return wc_get_template_html(
+				$this->template_html,
+				array(
+					'email_heading'      => $this->get_heading(),
+					'additional_content' => $this->get_additional_content(),
+					'withdrawal_data'    => $this->withdrawal_data,
+					'detail_rows'        => $this->formatter->get_detail_rows( $this->withdrawal_data, $this->submitted_at ),
+					'sent_to_admin'      => false,
+					'plain_text'         => false,
+					'email'              => $this,
+				)
+			);
+		}
+
+		/**
+		 * Get content plain.
+		 *
+		 * @return string
+		 */
+		public function get_content_plain() {
+			return wc_get_template_html(
+				$this->template_plain,
+				array(
+					'email_heading'      => $this->get_heading(),
+					'additional_content' => $this->get_additional_content(),
+					'withdrawal_data'    => $this->withdrawal_data,
+					'detail_rows'        => $this->formatter->get_detail_rows( $this->withdrawal_data, $this->submitted_at ),
+					'sent_to_admin'      => false,
+					'plain_text'         => true,
+					'email'              => $this,
+				)
+			);
+		}
+	}
+
+endif;
+
+return new WC_Email_Customer_Order_Withdrawal_Requested();
diff --git a/plugins/woocommerce/includes/emails/class-wc-email-order-withdrawal-requested.php b/plugins/woocommerce/includes/emails/class-wc-email-order-withdrawal-requested.php
new file mode 100644
index 00000000000..769aee16b06
--- /dev/null
+++ b/plugins/woocommerce/includes/emails/class-wc-email-order-withdrawal-requested.php
@@ -0,0 +1,306 @@
+<?php
+/**
+ * Class WC_Email_Order_Withdrawal_Requested file.
+ *
+ * @package WooCommerce\Emails
+ */
+
+use Automattic\WooCommerce\Internal\OrderWithdrawal\Emails\OrderWithdrawalEmailDataFormatter;
+use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFormProcessor;
+use Automattic\WooCommerce\Utilities\FeaturesUtil;
+
+defined( 'ABSPATH' ) || exit;
+
+if ( ! class_exists( 'WC_Email_Order_Withdrawal_Requested', false ) ) :
+
+	/**
+	 * Merchant order withdrawal request email.
+	 *
+	 * @class   WC_Email_Order_Withdrawal_Requested
+	 * @package WooCommerce\Classes\Emails
+	 */
+	class WC_Email_Order_Withdrawal_Requested extends WC_Email {
+
+		/**
+		 * Form data submitted by the customer.
+		 *
+		 * @var array<string,string>
+		 */
+		public array $withdrawal_data = array();
+
+		/**
+		 * Matched order, if found.
+		 *
+		 * @var WC_Order|null
+		 */
+		public ?WC_Order $matched_order = null;
+
+		/**
+		 * Submission timestamp.
+		 *
+		 * @var int
+		 */
+		public int $submitted_at = 0;
+
+		/**
+		 * Whether the matched order is outside the valid withdrawal window.
+		 *
+		 * @var bool
+		 */
+		public bool $outside_withdrawal_window = false;
+
+		/**
+		 * Withdrawal window warning message.
+		 *
+		 * @var string
+		 */
+		public string $withdrawal_window_warning = '';
+
+		/**
+		 * Data formatter.
+		 *
+		 * @var OrderWithdrawalEmailDataFormatter
+		 */
+		private OrderWithdrawalEmailDataFormatter $formatter;
+
+		/**
+		 * Constructor.
+		 */
+		public function __construct() {
+			$this->id             = 'order_withdrawal_requested';
+			$this->title          = __( 'Order withdrawal request', 'woocommerce' );
+			$this->description    = __( 'Sent to chosen recipients when a customer submits an order withdrawal request.', 'woocommerce' );
+			$this->email_group    = 'orders';
+			$this->template_html  = 'emails/admin-order-withdrawal-requested.php';
+			$this->template_plain = 'emails/plain/admin-order-withdrawal-requested.php';
+			$this->placeholders   = array(
+				'{order_number}'       => '',
+				'{order_billing_name}' => '',
+			);
+			$this->formatter      = new OrderWithdrawalEmailDataFormatter();
+
+			parent::__construct();
+
+			$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
+		}
+
+		/**
+		 * Get email subject.
+		 *
+		 * @return string
+		 */
+		public function get_default_subject() {
+			return __( '[{site_title}]: Order withdrawal request for order {order_number}', 'woocommerce' );
+		}
+
+		/**
+		 * Get email heading.
+		 *
+		 * @return string
+		 */
+		public function get_default_heading() {
+			return __( 'Order withdrawal request received', 'woocommerce' );
+		}
+
+		/**
+		 * Default content to show below main email content.
+		 *
+		 * @return string
+		 */
+		public function get_default_additional_content() {
+			return __( 'Review the request details and contact the customer about next steps.', 'woocommerce' );
+		}
+
+		/**
+		 * Trigger the sending of this email.
+		 *
+		 * @param array<string,mixed> $data                      Form data.
+		 * @param WC_Order|null       $matched_order             Matched order, if found.
+		 * @param int                 $submitted_at              Unix timestamp for the submission.
+		 * @param bool                $outside_withdrawal_window Whether the matched order is outside the withdrawal window.
+		 * @param string              $withdrawal_window_warning Withdrawal window warning message.
+		 * @return bool Whether the email was sent successfully.
+		 */
+		public function trigger( array $data, ?WC_Order $matched_order, int $submitted_at, bool $outside_withdrawal_window, string $withdrawal_window_warning ): bool {
+			$this->setup_locale();
+
+			$data = $this->formatter->normalize_withdrawal_data( $data );
+
+			if ( null === $data ) {
+				$this->restore_locale();
+
+				return false;
+			}
+
+			$this->object                               = $matched_order ? $matched_order : (object) $data;
+			$this->withdrawal_data                      = $data;
+			$this->matched_order                        = $matched_order;
+			$this->submitted_at                         = $submitted_at;
+			$this->outside_withdrawal_window            = $outside_withdrawal_window;
+			$this->withdrawal_window_warning            = $withdrawal_window_warning;
+			$this->placeholders['{order_number}']       = $data[ OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER ];
+			$this->placeholders['{order_billing_name}'] = $this->formatter->get_customer_name( $data );
+
+			$sent = $this->send_notification();
+
+			$this->restore_locale();
+
+			return $sent;
+		}
+
+		/**
+		 * Get email headers.
+		 *
+		 * @return string
+		 */
+		public function get_headers() {
+			$headers = 'Content-Type: ' . $this->get_content_type() . "\r\n";
+			$name    = $this->formatter->get_customer_name( $this->withdrawal_data );
+			$email   = $this->withdrawal_data[ OrderWithdrawalFormProcessor::FIELD_EMAIL ] ?? '';
+
+			if ( '' !== $name && is_email( $email ) ) {
+				$headers .= 'Reply-to: ' . sanitize_text_field( $name ) . ' <' . sanitize_email( $email ) . ">\r\n";
+			}
+
+			if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
+				$cc = $this->get_cc_recipient();
+				if ( ! empty( $cc ) ) {
+					$headers .= 'Cc: ' . sanitize_text_field( $cc ) . "\r\n";
+				}
+
+				$bcc = $this->get_bcc_recipient();
+				if ( ! empty( $bcc ) ) {
+					$headers .= 'Bcc: ' . sanitize_text_field( $bcc ) . "\r\n";
+				}
+			}
+
+			/**
+			 * Filter the email headers.
+			 *
+			 * @since 2.0.0
+			 * @param string   $headers Email headers.
+			 * @param string   $email_id Email ID.
+			 * @param object|bool $object Email object.
+			 * @param WC_Email $email    Email instance.
+			 */
+			return apply_filters( 'woocommerce_email_headers', $headers, $this->id, $this->object, $this );
+		}
+
+		/**
+		 * Get content html.
+		 *
+		 * @return string
+		 */
+		public function get_content_html() {
+			return wc_get_template_html(
+				$this->template_html,
+				array(
+					'email_heading'             => $this->get_heading(),
+					'additional_content'        => $this->get_additional_content(),
+					'withdrawal_data'           => $this->withdrawal_data,
+					'detail_rows'               => $this->formatter->get_detail_rows( $this->withdrawal_data, $this->submitted_at ),
+					'matched_order'             => $this->matched_order,
+					'outside_withdrawal_window' => $this->outside_withdrawal_window,
+					'withdrawal_window_warning' => $this->withdrawal_window_warning,
+					'sent_to_admin'             => true,
+					'plain_text'                => false,
+					'email'                     => $this,
+				)
+			);
+		}
+
+		/**
+		 * Get content plain.
+		 *
+		 * @return string
+		 */
+		public function get_content_plain() {
+			return wc_get_template_html(
+				$this->template_plain,
+				array(
+					'email_heading'             => $this->get_heading(),
+					'additional_content'        => $this->get_additional_content(),
+					'withdrawal_data'           => $this->withdrawal_data,
+					'detail_rows'               => $this->formatter->get_detail_rows( $this->withdrawal_data, $this->submitted_at ),
+					'matched_order'             => $this->matched_order,
+					'outside_withdrawal_window' => $this->outside_withdrawal_window,
+					'withdrawal_window_warning' => $this->withdrawal_window_warning,
+					'sent_to_admin'             => true,
+					'plain_text'                => true,
+					'email'                     => $this,
+				)
+			);
+		}
+
+		/**
+		 * Initialise settings form fields.
+		 */
+		public function init_form_fields(): void {
+			/* translators: %s: list of placeholders */
+			$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
+			$this->form_fields = array(
+				'enabled'            => array(
+					'title'   => __( 'Enable/Disable', 'woocommerce' ),
+					'type'    => 'checkbox',
+					'label'   => __( 'Enable this email notification', 'woocommerce' ),
+					'default' => 'yes',
+				),
+				'recipient'          => array(
+					'title'       => __( 'Recipient(s)', 'woocommerce' ),
+					'type'        => 'text',
+					/* translators: %s: WP admin email. */
+					'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ),
+					'placeholder' => '',
+					'default'     => '',
+					'desc_tip'    => true,
+				),
+				'subject'            => array(
+					'title'       => __( 'Subject', 'woocommerce' ),
+					'type'        => 'text',
+					'desc_tip'    => true,
+					'description' => $placeholder_text,
+					'placeholder' => $this->get_default_subject(),
+					'default'     => '',
+				),
+				'heading'            => array(
+					'title'       => __( 'Email heading', 'woocommerce' ),
+					'type'        => 'text',
+					'desc_tip'    => true,
+					'description' => $placeholder_text,
+					'placeholder' => $this->get_default_heading(),
+					'default'     => '',
+				),
+				'additional_content' => array(
+					'title'       => __( 'Additional content', 'woocommerce' ),
+					'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
+					'css'         => 'width:400px; height: 75px;',
+					'placeholder' => __( 'N/A', 'woocommerce' ),
+					'type'        => 'textarea',
+					'default'     => $this->get_default_additional_content(),
+					'desc_tip'    => true,
+				),
+				'email_type'         => array(
+					'title'       => __( 'Email type', 'woocommerce' ),
+					'type'        => 'select',
+					'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
+					'default'     => 'html',
+					'class'       => 'email_type wc-enhanced-select',
+					'options'     => $this->get_email_type_options(),
+					'desc_tip'    => true,
+				),
+			);
+
+			if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
+				$this->form_fields['cc']  = $this->get_cc_field();
+				$this->form_fields['bcc'] = $this->get_bcc_field();
+			}
+
+			if ( $this->block_email_editor_enabled ) {
+				$this->form_fields['preheader'] = $this->get_preheader_field();
+			}
+		}
+	}
+
+endif;
+
+return new WC_Email_Order_Withdrawal_Requested();
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/Emails/OrderWithdrawalEmailDataFormatter.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/Emails/OrderWithdrawalEmailDataFormatter.php
new file mode 100644
index 00000000000..02b4eca5a78
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/Emails/OrderWithdrawalEmailDataFormatter.php
@@ -0,0 +1,111 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\OrderWithdrawal\Emails;
+
+use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFormProcessor;
+
+/**
+ * Formats order withdrawal email data for templates.
+ *
+ * @internal Just for internal use.
+ */
+final class OrderWithdrawalEmailDataFormatter {
+
+	/**
+	 * Normalize and validate submitted withdrawal data before rendering emails.
+	 *
+	 * @param array<string,mixed> $data Form data.
+	 * @return array<string,string>|null
+	 */
+	public function normalize_withdrawal_data( array $data ): ?array {
+		$required_fields = array(
+			OrderWithdrawalFormProcessor::FIELD_FIRST_NAME,
+			OrderWithdrawalFormProcessor::FIELD_LAST_NAME,
+			OrderWithdrawalFormProcessor::FIELD_EMAIL,
+			OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER,
+			OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE,
+			OrderWithdrawalFormProcessor::FIELD_ADDITIONAL_DETAILS,
+		);
+
+		foreach ( $required_fields as $field ) {
+			if ( ! isset( $data[ $field ] ) || ! is_scalar( $data[ $field ] ) ) {
+				return null;
+			}
+		}
+
+		$normalized_data = array(
+			OrderWithdrawalFormProcessor::FIELD_FIRST_NAME => sanitize_text_field( (string) $data[ OrderWithdrawalFormProcessor::FIELD_FIRST_NAME ] ),
+			OrderWithdrawalFormProcessor::FIELD_LAST_NAME  => sanitize_text_field( (string) $data[ OrderWithdrawalFormProcessor::FIELD_LAST_NAME ] ),
+			OrderWithdrawalFormProcessor::FIELD_EMAIL      => sanitize_email( (string) $data[ OrderWithdrawalFormProcessor::FIELD_EMAIL ] ),
+			OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER => sanitize_text_field( (string) $data[ OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER ] ),
+			OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE => sanitize_text_field( (string) $data[ OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE ] ),
+			OrderWithdrawalFormProcessor::FIELD_ADDITIONAL_DETAILS => sanitize_textarea_field( (string) $data[ OrderWithdrawalFormProcessor::FIELD_ADDITIONAL_DETAILS ] ),
+		);
+
+		if ( '' === $normalized_data[ OrderWithdrawalFormProcessor::FIELD_EMAIL ] || ! is_email( $normalized_data[ OrderWithdrawalFormProcessor::FIELD_EMAIL ] ) ) {
+			return null;
+		}
+
+		if ( '' === $normalized_data[ OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER ] ) {
+			return null;
+		}
+
+		if ( ! in_array( $normalized_data[ OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE ], array( OrderWithdrawalFormProcessor::WITHDRAWAL_TYPE_FULL, OrderWithdrawalFormProcessor::WITHDRAWAL_TYPE_SPECIFIC ), true ) ) {
+			return null;
+		}
+
+		return $normalized_data;
+	}
+
+	/**
+	 * Get the customer's full name for display.
+	 *
+	 * @param array<string,string> $data Form data.
+	 */
+	public function get_customer_name( array $data ): string {
+		return trim( ( $data[ OrderWithdrawalFormProcessor::FIELD_FIRST_NAME ] ?? '' ) . ' ' . ( $data[ OrderWithdrawalFormProcessor::FIELD_LAST_NAME ] ?? '' ) );
+	}
+
+	/**
+	 * Get the label for a withdrawal type value.
+	 *
+	 * @param string $withdrawal_type Withdrawal type value.
+	 */
+	public function get_withdrawal_type_label( string $withdrawal_type ): string {
+		$options = array(
+			OrderWithdrawalFormProcessor::WITHDRAWAL_TYPE_FULL     => __( 'The full order', 'woocommerce' ),
+			OrderWithdrawalFormProcessor::WITHDRAWAL_TYPE_SPECIFIC => __( 'Specific items only', 'woocommerce' ),
+		);
+
+		return $options[ $withdrawal_type ] ?? '';
+	}
+
+	/**
+	 * Get order withdrawal detail rows for email templates.
+	 *
+	 * @param array<string,string> $data         Form data.
+	 * @param int                  $submitted_at Unix timestamp for the submission.
+	 * @return array<string,string>
+	 */
+	public function get_detail_rows( array $data, int $submitted_at ): array {
+		$date_format        = (string) get_option( 'date_format' );
+		$time_format        = (string) get_option( 'time_format' );
+		$additional_details = $data[ OrderWithdrawalFormProcessor::FIELD_ADDITIONAL_DETAILS ] ?? '';
+		$additional_details = '' === $additional_details ? __( 'None provided', 'woocommerce' ) : $additional_details;
+		$submitted_at_text  = wp_date( trim( $date_format . ' ' . $time_format ), $submitted_at );
+
+		if ( false === $submitted_at_text ) {
+			$submitted_at_text = '';
+		}
+
+		return array(
+			__( 'Submitted', 'woocommerce' )          => $submitted_at_text,
+			__( 'Name', 'woocommerce' )               => $this->get_customer_name( $data ),
+			__( 'Email address', 'woocommerce' )      => $data[ OrderWithdrawalFormProcessor::FIELD_EMAIL ] ?? '',
+			__( 'Order number', 'woocommerce' )       => $data[ OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER ] ?? '',
+			__( 'Withdrawing', 'woocommerce' )        => $this->get_withdrawal_type_label( $data[ OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE ] ?? '' ),
+			__( 'Additional details', 'woocommerce' ) => $additional_details,
+		);
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/Emails/OrderWithdrawalEmailPreview.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/Emails/OrderWithdrawalEmailPreview.php
new file mode 100644
index 00000000000..94d0d8cfba5
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/Emails/OrderWithdrawalEmailPreview.php
@@ -0,0 +1,69 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\OrderWithdrawal\Emails;
+
+use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFormProcessor;
+use Automattic\WooCommerce\Internal\RegisterHooksInterface;
+use WC_Email_Customer_Order_Withdrawal_Requested;
+use WC_Email_Order_Withdrawal_Requested;
+use WC_Order;
+
+/**
+ * Prepares order withdrawal emails for preview.
+ *
+ * @internal Just for internal use.
+ */
+final class OrderWithdrawalEmailPreview implements RegisterHooksInterface {
+
+	/**
+	 * Register email preview hooks.
+	 *
+	 * @since 11.2.0
+	 */
+	public function register(): void {
+		add_filter( 'woocommerce_prepare_email_for_preview', array( $this, 'prepare_email_for_preview' ), 10, 1 );
+	}
+
+	/**
+	 * Populate order withdrawal data for email previews.
+	 *
+	 * @param mixed $email Email being prepared for preview.
+	 * @return mixed
+	 *
+	 * @since 11.2.0
+	 */
+	public function prepare_email_for_preview( $email ) {
+		if (
+			(
+				! $email instanceof WC_Email_Customer_Order_Withdrawal_Requested
+				&& ! $email instanceof WC_Email_Order_Withdrawal_Requested
+			)
+			|| ! $email->object instanceof WC_Order
+		) {
+			return $email;
+		}
+
+		$order                                       = $email->object;
+		$order_date                                  = $order->get_date_created();
+		$email->withdrawal_data                      = array(
+			OrderWithdrawalFormProcessor::FIELD_FIRST_NAME => $order->get_billing_first_name(),
+			OrderWithdrawalFormProcessor::FIELD_LAST_NAME  => $order->get_billing_last_name(),
+			OrderWithdrawalFormProcessor::FIELD_EMAIL      => $order->get_billing_email(),
+			OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER => $order->get_order_number(),
+			OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE => OrderWithdrawalFormProcessor::WITHDRAWAL_TYPE_SPECIFIC,
+			OrderWithdrawalFormProcessor::FIELD_ADDITIONAL_DETAILS => __( 'I would like to withdraw the first item from this order.', 'woocommerce' ),
+		);
+		$email->submitted_at                         = $order_date ? $order_date->getTimestamp() : time();
+		$email->placeholders['{order_number}']       = $order->get_order_number();
+		$email->placeholders['{order_billing_name}'] = $order->get_formatted_billing_full_name();
+
+		if ( $email instanceof WC_Email_Order_Withdrawal_Requested ) {
+			$email->matched_order             = $order;
+			$email->outside_withdrawal_window = false;
+			$email->withdrawal_window_warning = '';
+		}
+
+		return $email;
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
index 74144d58f2b..8cdab00f4ea 100644
--- a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
@@ -4,6 +4,7 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Internal\OrderWithdrawal;

 use Automattic\WooCommerce\Internal\Features\FeaturesController;
+use Automattic\WooCommerce\Internal\OrderWithdrawal\Emails\OrderWithdrawalEmailPreview;
 use Automattic\WooCommerce\Internal\RegisterHooksInterface;
 use Automattic\WooCommerce\Utilities\FeaturesUtil;

@@ -40,20 +41,29 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 	 */
 	private OrderWithdrawalFeatureHighlightNotification $feature_highlight_notification;

+	/**
+	 * Email preview handler.
+	 *
+	 * @var OrderWithdrawalEmailPreview
+	 */
+	private OrderWithdrawalEmailPreview $email_preview;
+
 	/**
 	 * Initialize dependencies.
 	 *
 	 * @param OrderWithdrawalFormProcessor                $form_processor                 Form processor.
 	 * @param OrderWithdrawalFormView                     $form_view                      Form view.
 	 * @param OrderWithdrawalFeatureHighlightNotification $feature_highlight_notification Feature highlight notification.
+	 * @param OrderWithdrawalEmailPreview                 $email_preview                  Email preview handler.
 	 * @internal
 	 *
 	 * @since 11.1.0
 	 */
-	final public function init( OrderWithdrawalFormProcessor $form_processor, OrderWithdrawalFormView $form_view, OrderWithdrawalFeatureHighlightNotification $feature_highlight_notification ): void { // phpcs:ignore Generic.CodeAnalysis.UnnecessaryFinalModifier.Found -- Required by WooCommerce injection method rules.
+	final public function init( OrderWithdrawalFormProcessor $form_processor, OrderWithdrawalFormView $form_view, OrderWithdrawalFeatureHighlightNotification $feature_highlight_notification, OrderWithdrawalEmailPreview $email_preview ): void { // phpcs:ignore Generic.CodeAnalysis.UnnecessaryFinalModifier.Found -- Required by WooCommerce injection method rules.
 		$this->form_processor                 = $form_processor;
 		$this->form_view                      = $form_view;
 		$this->feature_highlight_notification = $feature_highlight_notification;
+		$this->email_preview                  = $email_preview;
 	}

 	/**
@@ -85,6 +95,8 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 		add_filter( 'woocommerce_endpoint_' . self::ENDPOINT_KEY . '_title', array( $this, 'get_endpoint_title' ), 10, 1 );
 		add_filter( 'woocommerce_settings_pages', array( $this, 'add_endpoint_setting' ), 10, 1 );
 		add_action( 'woocommerce_account_' . self::ENDPOINT_KEY . '_endpoint', array( $this, 'render_view' ) );
+
+		$this->email_preview->register();
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
index f8d6a61fb89..4e1feb693d4 100644
--- a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalFormProcessor.php
@@ -5,9 +5,12 @@ namespace Automattic\WooCommerce\Internal\OrderWithdrawal;

 use Automattic\WooCommerce\Admin\Notes\Note;
 use Automattic\WooCommerce\Admin\Notes\Notes;
+use Automattic\WooCommerce\Internal\OrderWithdrawal\Emails\OrderWithdrawalEmailDataFormatter;
 use Automattic\WooCommerce\Internal\Orders\OrderNoteGroup;
 use Automattic\WooCommerce\Utilities\OrderUtil;
 use Throwable;
+use WC_Email_Customer_Order_Withdrawal_Requested;
+use WC_Email_Order_Withdrawal_Requested;
 use WC_Geolocation;
 use WC_Order;
 use WC_Rate_Limiter;
@@ -47,6 +50,13 @@ final class OrderWithdrawalFormProcessor {
 	private const RATE_LIMIT_EMAIL_PREFIX             = 'order_withdrawal_email_';
 	private const RATE_LIMIT_DELAY                    = MINUTE_IN_SECONDS / 2;

+	/**
+	 * Data formatter used by email and note content.
+	 *
+	 * @var OrderWithdrawalEmailDataFormatter|null
+	 */
+	private ?OrderWithdrawalEmailDataFormatter $email_data_formatter = null;
+
 	/**
 	 * Process the current order withdrawal request.
 	 *
@@ -462,7 +472,7 @@ final class OrderWithdrawalFormProcessor {
 		$note = sprintf(
 			/* translators: %s: withdrawal type label. */
 			__( 'Order withdrawal requested. Withdrawal type: %s.', 'woocommerce' ),
-			$this->get_withdrawal_type_label( $data[ self::FIELD_WITHDRAWAL_TYPE ] )
+			$this->get_email_data_formatter()->get_withdrawal_type_label( $data[ self::FIELD_WITHDRAWAL_TYPE ] ?? '' )
 		);

 		try {
@@ -634,17 +644,17 @@ final class OrderWithdrawalFormProcessor {
 	 * @param int                  $submitted_at Unix timestamp for the submission.
 	 */
 	private function send_customer_order_withdrawal_email( array $data, int $submitted_at ): bool {
-		$subject = __( 'We received your withdrawal request', 'woocommerce' );
-		$heading = __( 'We received your withdrawal request', 'woocommerce' );
-		$body    = '<p>' . esc_html__( 'We have received your request to withdraw from the order below.', 'woocommerce' ) . '</p>';
-		$body   .= $this->get_email_details_html( $data, $submitted_at );
-		$body   .= '<p>' . esc_html__( 'We will review your request and contact you about next steps, including any refund due.', 'woocommerce' ) . '</p>';
-
-		return wc_mail(
-			$data[ self::FIELD_EMAIL ],
-			$subject,
-			$this->wrap_email_message( $heading, $body )
-		);
+		$email = WC()->mailer()->get_emails()['WC_Email_Customer_Order_Withdrawal_Requested'] ?? include WC_ABSPATH . 'includes/emails/class-wc-email-customer-order-withdrawal-requested.php';
+
+		if ( ! $email instanceof WC_Email_Customer_Order_Withdrawal_Requested ) {
+			return false;
+		}
+
+		if ( ! $email->is_enabled() ) {
+			return true;
+		}
+
+		return $email->trigger( $data, $submitted_at );
 	}

 	/**
@@ -655,150 +665,34 @@ final class OrderWithdrawalFormProcessor {
 	 * @param int                  $submitted_at  Unix timestamp for the submission.
 	 */
 	private function send_merchant_order_withdrawal_email( array $data, ?WC_Order $matched_order, int $submitted_at ): bool {
-		$recipient = sanitize_email( (string) get_option( 'admin_email' ) );
+		$email = WC()->mailer()->get_emails()['WC_Email_Order_Withdrawal_Requested'] ?? include WC_ABSPATH . 'includes/emails/class-wc-email-order-withdrawal-requested.php';

-		if ( '' === $recipient || ! is_email( $recipient ) ) {
+		if ( ! $email instanceof WC_Email_Order_Withdrawal_Requested ) {
 			return false;
 		}

-		$subject = sprintf(
-			/* translators: %s: order number. */
-			__( 'Order withdrawal request for order %s', 'woocommerce' ),
-			$data[ self::FIELD_ORDER_NUMBER ]
-		);
-		$heading = __( 'Order withdrawal request received', 'woocommerce' );
-		$body    = '<p>' . esc_html__( 'A customer submitted an order withdrawal request.', 'woocommerce' ) . '</p>';
-
-		if ( $matched_order instanceof WC_Order ) {
-			$body .= '<p>' . esc_html__( 'WooCommerce matched this request to an order and added an order note.', 'woocommerce' ) . '</p>';
-		} else {
-			$body .= '<p>' . esc_html__( 'WooCommerce could not match this request to an order automatically, so no order note was added.', 'woocommerce' ) . '</p>';
+		if ( ! $email->is_enabled() ) {
+			return true;
 		}

-		$body .= $this->get_email_details_html( $data, $submitted_at );
-
-		if ( $matched_order instanceof WC_Order ) {
-			$order_url = $matched_order->get_edit_order_url();
-
-			if ( $this->is_order_outside_withdrawal_window( $matched_order ) ) {
-				$body .= '<p>' . esc_html( $this->get_withdrawal_window_warning_message() ) . '</p>';
-			}
-
-			$body .= sprintf(
-				'<p>%s</p>',
-				sprintf(
-					/* translators: %d: order ID. */
-					esc_html__( 'Matched order ID: %d', 'woocommerce' ),
-					$matched_order->get_id()
-				)
-			);
-
-			if ( '' !== $order_url ) {
-				$body .= sprintf(
-					'<p><a href="%1$s">%2$s</a></p>',
-					esc_url( $order_url ),
-					esc_html__( 'View matched order', 'woocommerce' )
-				);
-			}
-		}
-
-		return wc_mail(
-			$recipient,
-			$subject,
-			$this->wrap_email_message( $heading, $body ),
-			$this->get_merchant_email_headers( $data )
+		return $email->trigger(
+			$data,
+			$matched_order,
+			$submitted_at,
+			$matched_order instanceof WC_Order && $this->is_order_outside_withdrawal_window( $matched_order ),
+			$matched_order instanceof WC_Order ? $this->get_withdrawal_window_warning_message() : ''
 		);
 	}

 	/**
-	 * Get merchant email headers.
-	 *
-	 * @param array<string,string> $data Form data.
-	 * @return string
-	 */
-	private function get_merchant_email_headers( array $data ): string {
-		$headers = array( 'Content-Type: text/html; charset=UTF-8' );
-		$name    = $this->get_customer_name( $data );
-		$email   = $data[ self::FIELD_EMAIL ];
-
-		if ( '' !== $name && is_email( $email ) ) {
-			$headers[] = sprintf( 'Reply-To: %1$s <%2$s>', $name, $email );
-		}
-
-		return implode( "\r\n", $headers );
-	}
-
-	/**
-	 * Wrap an email body in the WooCommerce email template.
-	 *
-	 * @param string $heading Email heading.
-	 * @param string $body    Email body.
+	 * Get the order withdrawal email data formatter.
 	 */
-	private function wrap_email_message( string $heading, string $body ): string {
-		return WC()->mailer()->wrap_message( $heading, $body );
-	}
-
-	/**
-	 * Get the email details list.
-	 *
-	 * @param array<string,string> $data         Form data.
-	 * @param int                  $submitted_at Unix timestamp for the submission.
-	 */
-	private function get_email_details_html( array $data, int $submitted_at ): string {
-		$date_format        = (string) get_option( 'date_format' );
-		$time_format        = (string) get_option( 'time_format' );
-		$additional_details = '' === $data[ self::FIELD_ADDITIONAL_DETAILS ] ? __( 'None provided', 'woocommerce' ) : $data[ self::FIELD_ADDITIONAL_DETAILS ];
-		$submitted_at_text  = wp_date( trim( $date_format . ' ' . $time_format ), $submitted_at );
-
-		if ( false === $submitted_at_text ) {
-			$submitted_at_text = '';
+	private function get_email_data_formatter(): OrderWithdrawalEmailDataFormatter {
+		if ( null === $this->email_data_formatter ) {
+			$this->email_data_formatter = new OrderWithdrawalEmailDataFormatter();
 		}

-		$rows = array(
-			__( 'Submitted', 'woocommerce' )          => $submitted_at_text,
-			__( 'Name', 'woocommerce' )               => $this->get_customer_name( $data ),
-			__( 'Email address', 'woocommerce' )      => $data[ self::FIELD_EMAIL ],
-			__( 'Order number', 'woocommerce' )       => $data[ self::FIELD_ORDER_NUMBER ],
-			__( 'Withdrawing', 'woocommerce' )        => $this->get_withdrawal_type_label( $data[ self::FIELD_WITHDRAWAL_TYPE ] ),
-			__( 'Additional details', 'woocommerce' ) => $additional_details,
-		);
-
-		$html = '<ul>';
-
-		foreach ( $rows as $label => $value ) {
-			$html .= sprintf(
-				'<li><strong>%1$s:</strong> %2$s</li>',
-				esc_html( $label ),
-				nl2br( esc_html( $value ) )
-			);
-		}
-
-		$html .= '</ul>';
-
-		return $html;
-	}
-
-	/**
-	 * Get the customer's full name for display.
-	 *
-	 * @param array<string,string> $data Form data.
-	 */
-	private function get_customer_name( array $data ): string {
-		return trim( $data[ self::FIELD_FIRST_NAME ] . ' ' . $data[ self::FIELD_LAST_NAME ] );
-	}
-
-	/**
-	 * Get the label for a withdrawal type value.
-	 *
-	 * @param string $withdrawal_type Withdrawal type value.
-	 */
-	private function get_withdrawal_type_label( string $withdrawal_type ): string {
-		$options = array(
-			self::WITHDRAWAL_TYPE_FULL     => __( 'The full order', 'woocommerce' ),
-			self::WITHDRAWAL_TYPE_SPECIFIC => __( 'Specific items only', 'woocommerce' ),
-		);
-
-		return $options[ $withdrawal_type ] ?? '';
+		return $this->email_data_formatter;
 	}

 	/**
diff --git a/plugins/woocommerce/templates/emails/admin-order-withdrawal-requested.php b/plugins/woocommerce/templates/emails/admin-order-withdrawal-requested.php
new file mode 100644
index 00000000000..28cd2bb7cf5
--- /dev/null
+++ b/plugins/woocommerce/templates/emails/admin-order-withdrawal-requested.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Admin order withdrawal request email
+ *
+ * This template can be overridden by copying it to yourtheme/woocommerce/emails/admin-order-withdrawal-requested.php.
+ *
+ * HOWEVER, on occasion WooCommerce will need to update template files and you
+ * (the theme developer) will need to copy the new files to your theme to
+ * maintain compatibility. We try to do this as little as possible, but it does
+ * happen. When this occurs the version of the template file will be bumped and
+ * the readme will list any important changes.
+ *
+ * @see https://woocommerce.com/document/template-structure/
+ * @package WooCommerce\Templates\Emails
+ * @version 11.2.0
+ *
+ * @var string               $email_heading             Email heading.
+ * @var string               $additional_content        Additional content below the body.
+ * @var array<string,string> $withdrawal_data           Withdrawal request data.
+ * @var array<string,string> $detail_rows               Withdrawal request detail rows.
+ * @var \WC_Order|null       $matched_order             Matched order, if found.
+ * @var bool                 $outside_withdrawal_window Whether the matched order is outside the withdrawal window.
+ * @var string               $withdrawal_window_warning Withdrawal window warning message.
+ * @var bool                 $sent_to_admin             Whether sent to admin.
+ * @var bool                 $plain_text                Whether plain-text variant.
+ * @var \WC_Email            $email                     Email object.
+ */
+
+use Automattic\WooCommerce\Utilities\FeaturesUtil;
+
+defined( 'ABSPATH' ) || exit;
+
+$email_improvements_enabled = FeaturesUtil::feature_is_enabled( 'email_improvements' );
+
+/**
+ * Fires to output the email header.
+ *
+ * @hooked WC_Emails::email_header()
+ * @since 3.7.0
+ */
+do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
+
+<?php echo $email_improvements_enabled ? '<div class="email-introduction">' : ''; ?>
+<p><?php esc_html_e( 'A customer submitted an order withdrawal request.', 'woocommerce' ); ?></p>
+<?php if ( $matched_order instanceof WC_Order ) : ?>
+	<p><?php esc_html_e( 'WooCommerce matched this request to an order and added an order note.', 'woocommerce' ); ?></p>
+<?php else : ?>
+	<p><?php esc_html_e( 'WooCommerce could not match this request to an order automatically, so no order note was added.', 'woocommerce' ); ?></p>
+<?php endif; ?>
+<?php echo $email_improvements_enabled ? '</div>' : ''; ?>
+
+<ul>
+	<?php foreach ( $detail_rows as $label => $value ) : ?>
+		<li><strong><?php echo esc_html( $label ); ?>:</strong> <?php echo nl2br( esc_html( $value ) ); ?></li>
+	<?php endforeach; ?>
+</ul>
+
+<?php if ( $matched_order instanceof WC_Order ) : ?>
+	<?php if ( $outside_withdrawal_window ) : ?>
+		<p><?php echo esc_html( $withdrawal_window_warning ); ?></p>
+	<?php endif; ?>
+	<p>
+		<?php
+		printf(
+			/* translators: %d: order ID. */
+			esc_html__( 'Matched order ID: %d', 'woocommerce' ),
+			absint( $matched_order->get_id() )
+		);
+		?>
+	</p>
+	<?php if ( '' !== $matched_order->get_edit_order_url() ) : ?>
+		<p><a href="<?php echo esc_url( $matched_order->get_edit_order_url() ); ?>"><?php esc_html_e( 'View matched order', 'woocommerce' ); ?></a></p>
+	<?php endif; ?>
+<?php endif; ?>
+
+<?php
+if ( $additional_content ) {
+	echo $email_improvements_enabled ? '<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"><tr><td class="email-additional-content">' : '';
+	echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) );
+	echo $email_improvements_enabled ? '</td></tr></table>' : '';
+}
+
+/**
+ * Fires to output the email footer.
+ *
+ * @hooked WC_Emails::email_footer()
+ * @since 3.7.0
+ */
+do_action( 'woocommerce_email_footer', $email );
diff --git a/plugins/woocommerce/templates/emails/customer-order-withdrawal-requested.php b/plugins/woocommerce/templates/emails/customer-order-withdrawal-requested.php
new file mode 100644
index 00000000000..68ae0c6ebb5
--- /dev/null
+++ b/plugins/woocommerce/templates/emails/customer-order-withdrawal-requested.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Customer order withdrawal request email
+ *
+ * This template can be overridden by copying it to yourtheme/woocommerce/emails/customer-order-withdrawal-requested.php.
+ *
+ * HOWEVER, on occasion WooCommerce will need to update template files and you
+ * (the theme developer) will need to copy the new files to your theme to
+ * maintain compatibility. We try to do this as little as possible, but it does
+ * happen. When this occurs the version of the template file will be bumped and
+ * the readme will list any important changes.
+ *
+ * @see https://woocommerce.com/document/template-structure/
+ * @package WooCommerce\Templates\Emails
+ * @version 11.2.0
+ *
+ * @var string              $email_heading      Email heading.
+ * @var string              $additional_content Additional content below the body.
+ * @var array<string,string> $withdrawal_data    Withdrawal request data.
+ * @var array<string,string> $detail_rows        Withdrawal request detail rows.
+ * @var bool                $sent_to_admin      Whether sent to admin.
+ * @var bool                $plain_text         Whether plain-text variant.
+ * @var \WC_Email           $email              Email object.
+ */
+
+use Automattic\WooCommerce\Utilities\FeaturesUtil;
+
+defined( 'ABSPATH' ) || exit;
+
+$email_improvements_enabled = FeaturesUtil::feature_is_enabled( 'email_improvements' );
+
+/**
+ * Fires to output the email header.
+ *
+ * @hooked WC_Emails::email_header()
+ * @since 3.7.0
+ */
+do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
+
+<?php echo $email_improvements_enabled ? '<div class="email-introduction">' : ''; ?>
+<p><?php esc_html_e( 'We have received your request to withdraw from the order below.', 'woocommerce' ); ?></p>
+<?php echo $email_improvements_enabled ? '</div>' : ''; ?>
+
+<ul>
+	<?php foreach ( $detail_rows as $label => $value ) : ?>
+		<li><strong><?php echo esc_html( $label ); ?>:</strong> <?php echo nl2br( esc_html( $value ) ); ?></li>
+	<?php endforeach; ?>
+</ul>
+
+<?php
+if ( $additional_content ) {
+	echo $email_improvements_enabled ? '<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"><tr><td class="email-additional-content email-additional-content-aligned">' : '';
+	echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) );
+	echo $email_improvements_enabled ? '</td></tr></table>' : '';
+}
+
+/**
+ * Fires to output the email footer.
+ *
+ * @hooked WC_Emails::email_footer()
+ * @since 3.7.0
+ */
+do_action( 'woocommerce_email_footer', $email );
diff --git a/plugins/woocommerce/templates/emails/plain/admin-order-withdrawal-requested.php b/plugins/woocommerce/templates/emails/plain/admin-order-withdrawal-requested.php
new file mode 100644
index 00000000000..87c40a913c1
--- /dev/null
+++ b/plugins/woocommerce/templates/emails/plain/admin-order-withdrawal-requested.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Admin order withdrawal request email (plain text)
+ *
+ * This template can be overridden by copying it to yourtheme/woocommerce/emails/plain/admin-order-withdrawal-requested.php.
+ *
+ * HOWEVER, on occasion WooCommerce will need to update template files and you
+ * (the theme developer) will need to copy the new files to your theme to
+ * maintain compatibility. We try to do this as little as possible, but it does
+ * happen. When this occurs the version of the template file will be bumped and
+ * the readme will list any important changes.
+ *
+ * @see https://woocommerce.com/document/template-structure/
+ * @package WooCommerce\Templates\Emails\Plain
+ * @version 11.2.0
+ *
+ * @var string               $email_heading             Email heading.
+ * @var string               $additional_content        Additional content below the body.
+ * @var array<string,string> $withdrawal_data           Withdrawal request data.
+ * @var array<string,string> $detail_rows               Withdrawal request detail rows.
+ * @var \WC_Order|null       $matched_order             Matched order, if found.
+ * @var bool                 $outside_withdrawal_window Whether the matched order is outside the withdrawal window.
+ * @var string               $withdrawal_window_warning Withdrawal window warning message.
+ * @var bool                 $sent_to_admin             Whether sent to admin.
+ * @var bool                 $plain_text                Whether plain-text variant.
+ * @var \WC_Email            $email                     Email object.
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
+echo esc_html( wp_strip_all_tags( $email_heading ) );
+echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
+
+echo esc_html__( 'A customer submitted an order withdrawal request.', 'woocommerce' ) . "\n\n";
+
+if ( $matched_order instanceof WC_Order ) {
+	echo esc_html__( 'WooCommerce matched this request to an order and added an order note.', 'woocommerce' ) . "\n\n";
+} else {
+	echo esc_html__( 'WooCommerce could not match this request to an order automatically, so no order note was added.', 'woocommerce' ) . "\n\n";
+}
+
+foreach ( $detail_rows as $label => $value ) {
+	echo esc_html( $label ) . ': ' . esc_html( $value ) . "\n";
+}
+
+if ( $matched_order instanceof WC_Order ) {
+	echo "\n";
+
+	if ( $outside_withdrawal_window ) {
+		echo esc_html( $withdrawal_window_warning ) . "\n\n";
+	}
+
+	printf(
+		/* translators: %d: order ID. */
+		esc_html__( 'Matched order ID: %d', 'woocommerce' ),
+		absint( $matched_order->get_id() )
+	);
+	echo "\n";
+
+	if ( '' !== $matched_order->get_edit_order_url() ) {
+		echo esc_url( $matched_order->get_edit_order_url() ) . "\n";
+	}
+}
+
+echo "\n----------------------------------------\n\n";
+
+if ( $additional_content ) {
+	echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) );
+	echo "\n\n----------------------------------------\n\n";
+}
+
+/**
+ * Filter the email footer text.
+ *
+ * @param string $footer_text The footer text.
+ * @since 2.3.0
+ */
+echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) );
diff --git a/plugins/woocommerce/templates/emails/plain/customer-order-withdrawal-requested.php b/plugins/woocommerce/templates/emails/plain/customer-order-withdrawal-requested.php
new file mode 100644
index 00000000000..90f581441eb
--- /dev/null
+++ b/plugins/woocommerce/templates/emails/plain/customer-order-withdrawal-requested.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Customer order withdrawal request email (plain text)
+ *
+ * This template can be overridden by copying it to yourtheme/woocommerce/emails/plain/customer-order-withdrawal-requested.php.
+ *
+ * HOWEVER, on occasion WooCommerce will need to update template files and you
+ * (the theme developer) will need to copy the new files to your theme to
+ * maintain compatibility. We try to do this as little as possible, but it does
+ * happen. When this occurs the version of the template file will be bumped and
+ * the readme will list any important changes.
+ *
+ * @see https://woocommerce.com/document/template-structure/
+ * @package WooCommerce\Templates\Emails\Plain
+ * @version 11.2.0
+ *
+ * @var string              $email_heading      Email heading.
+ * @var string              $additional_content Additional content below the body.
+ * @var array<string,string> $withdrawal_data    Withdrawal request data.
+ * @var array<string,string> $detail_rows        Withdrawal request detail rows.
+ * @var bool                $sent_to_admin      Whether sent to admin.
+ * @var bool                $plain_text         Whether plain-text variant.
+ * @var \WC_Email           $email              Email object.
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
+echo esc_html( wp_strip_all_tags( $email_heading ) );
+echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
+
+echo esc_html__( 'We have received your request to withdraw from the order below.', 'woocommerce' ) . "\n\n";
+
+foreach ( $detail_rows as $label => $value ) {
+	echo esc_html( $label ) . ': ' . esc_html( $value ) . "\n";
+}
+
+echo "\n----------------------------------------\n\n";
+
+if ( $additional_content ) {
+	echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) );
+	echo "\n\n----------------------------------------\n\n";
+}
+
+/**
+ * Filter the email footer text.
+ *
+ * @param string $footer_text The footer text.
+ * @since 2.3.0
+ */
+echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) );
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
index d73ec1f2822..a3c7842728d 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
@@ -5,12 +5,16 @@ namespace Automattic\WooCommerce\Tests\Internal\OrderWithdrawal;

 use Automattic\WooCommerce\Admin\Notes\Note;
 use Automattic\WooCommerce\Admin\Notes\Notes;
+use Automattic\WooCommerce\Internal\Admin\EmailPreview\EmailPreview;
 use Automattic\WooCommerce\Internal\Features\FeaturesController;
+use Automattic\WooCommerce\Internal\OrderWithdrawal\Emails\OrderWithdrawalEmailPreview;
 use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalController;
 use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFormProcessor;
 use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFormState;
 use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFormView;
 use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalFeatureHighlightNotification;
+use WC_Email_Customer_Order_Withdrawal_Requested;
+use WC_Email_Order_Withdrawal_Requested;
 use WC_Order;
 use WC_Rate_Limiter;
 use WC_Unit_Test_Case;
@@ -112,6 +116,10 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

+		$bootstrap = \WC_Unit_Tests_Bootstrap::instance();
+		require_once $bootstrap->plugin_dir . '/includes/emails/class-wc-email-customer-order-withdrawal-requested.php';
+		require_once $bootstrap->plugin_dir . '/includes/emails/class-wc-email-order-withdrawal-requested.php';
+
 		$this->sut                         = new OrderWithdrawalFormProcessor();
 		$this->original_post               = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
 		$this->had_request_method          = filter_has_var( INPUT_SERVER, 'REQUEST_METHOD' );
@@ -671,14 +679,9 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 	 * @testdox Should register cleanup hooks for HPOS and legacy order deletion.
 	 */
 	public function test_controller_registers_order_deletion_cleanup_hooks(): void {
-		$this->enable_feature();
-
-		$controller = new OrderWithdrawalController();
-		$controller->init(
-			$this->sut,
-			new OrderWithdrawalFormView(),
-			new OrderWithdrawalFeatureHighlightNotification()
-		);
+		$controller    = new OrderWithdrawalController();
+		$email_preview = new OrderWithdrawalEmailPreview();
+		$controller->init( $this->sut, new OrderWithdrawalFormView(), new OrderWithdrawalFeatureHighlightNotification(), $email_preview );

 		try {
 			$controller->register();
@@ -686,9 +689,11 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {

 			$this->assertNotFalse( has_action( 'woocommerce_before_delete_order', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ) ) );
 			$this->assertNotFalse( has_action( 'before_delete_post', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ) ) );
+			$this->assertFalse( has_filter( 'woocommerce_prepare_email_for_preview', array( $email_preview, 'prepare_email_for_preview' ) ) );
 		} finally {
-			remove_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $controller, 'maybe_flush_rewrite_rules' ), 10 );
 			remove_action( 'init', array( $controller, 'register_feature_hooks' ), 0 );
+			remove_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $controller, 'maybe_flush_rewrite_rules' ), 10 );
+			remove_filter( 'woocommerce_prepare_email_for_preview', array( $email_preview, 'prepare_email_for_preview' ), 10 );
 			remove_filter( 'woocommerce_get_query_vars', array( $controller, 'add_query_var' ), 10 );
 			remove_filter( 'woocommerce_endpoint_order-withdrawal_title', array( $controller, 'get_endpoint_title' ), 10 );
 			remove_filter( 'woocommerce_settings_pages', array( $controller, 'add_endpoint_setting' ), 10 );
@@ -699,33 +704,67 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		}
 	}

+	/**
+	 * @testdox Should use standard dummy order data in order withdrawal email previews.
+	 */
+	public function test_order_withdrawal_email_preview_uses_standard_dummy_order_data(): void {
+		$this->enable_feature();
+
+		$email_preview_handler = new OrderWithdrawalEmailPreview();
+		$email_preview_handler->register();
+
+		try {
+			WC()->mailer()->init();
+
+			$email_preview = new EmailPreview();
+			$email_preview->set_email_type( WC_Email_Customer_Order_Withdrawal_Requested::class );
+			$customer_email = $email_preview->get_email();
+
+			$this->assertSame( 'John', $customer_email->withdrawal_data[ OrderWithdrawalFormProcessor::FIELD_FIRST_NAME ] );
+			$this->assertSame( 'Doe', $customer_email->withdrawal_data[ OrderWithdrawalFormProcessor::FIELD_LAST_NAME ] );
+			$this->assertSame( 'john@company.com', $customer_email->withdrawal_data[ OrderWithdrawalFormProcessor::FIELD_EMAIL ] );
+			$this->assertSame( '12345', $customer_email->withdrawal_data[ OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER ] );
+			$this->assertSame( 'John Doe', $customer_email->placeholders['{order_billing_name}'] );
+			$this->assertGreaterThan( 0, $customer_email->submitted_at );
+
+			$email_preview->set_email_type( WC_Email_Order_Withdrawal_Requested::class );
+			$merchant_email = $email_preview->get_email();
+
+			$this->assertSame( $merchant_email->object, $merchant_email->matched_order );
+			$this->assertStringContainsString( '12345', $merchant_email->get_subject() );
+		} finally {
+			remove_filter( 'woocommerce_prepare_email_for_preview', array( $email_preview_handler, 'prepare_email_for_preview' ) );
+			$this->disable_feature();
+			WC()->mailer()->init();
+		}
+	}
+
 	/**
 	 * @testdox Should skip the feature highlight notification hooks when order withdrawal is enabled.
 	 */
 	public function test_controller_skips_feature_highlight_notification_hooks_when_feature_is_enabled(): void {
 		$this->enable_feature();

-		$controller   = new OrderWithdrawalController();
-		$notification = new OrderWithdrawalFeatureHighlightNotification();
+		$controller    = new OrderWithdrawalController();
+		$notification  = new OrderWithdrawalFeatureHighlightNotification();
+		$email_preview = new OrderWithdrawalEmailPreview();

-		$controller->init(
-			$this->sut,
-			new OrderWithdrawalFormView(),
-			$notification
-		);
+		$controller->init( $this->sut, new OrderWithdrawalFormView(), $notification, $email_preview );

 		try {
 			$controller->register();

-			$this->assertNotFalse( has_action( 'init', array( $controller, 'register_feature_hooks' ) ), 'The controller should defer feature-specific hook registration until init.' );
+			$this->assertNotFalse( has_action( 'init', array( $controller, 'register_feature_hooks' ) ), 'The controller should defer feature hook registration until init.' );

 			$controller->register_feature_hooks();

+			$this->assertNotFalse( has_filter( 'woocommerce_prepare_email_for_preview', array( $email_preview, 'prepare_email_for_preview' ) ), 'The controller should initialize email previews when the feature is enabled.' );
 			$this->assertFalse( has_action( 'update_option_woocommerce_coming_soon', array( $notification, 'maybe_add_note_when_store_goes_live' ) ), 'The feature highlight notification should not listen for coming-soon changes when the feature is enabled.' );
 			$this->assertFalse( has_action( 'wc_admin_daily', array( $notification, 'possibly_add_note' ) ), 'The feature highlight notification should not run daily when the feature is enabled.' );
 		} finally {
 			remove_action( 'init', array( $controller, 'register_feature_hooks' ), 0 );
 			remove_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $controller, 'maybe_flush_rewrite_rules' ), 10 );
+			remove_filter( 'woocommerce_prepare_email_for_preview', array( $email_preview, 'prepare_email_for_preview' ), 10 );
 			remove_filter( 'woocommerce_get_query_vars', array( $controller, 'add_query_var' ), 10 );
 			remove_filter( 'woocommerce_endpoint_order-withdrawal_title', array( $controller, 'get_endpoint_title' ), 10 );
 			remove_filter( 'woocommerce_settings_pages', array( $controller, 'add_endpoint_setting' ), 10 );
@@ -743,13 +782,15 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		$this->enable_feature();
 		$this->disable_feature();

-		$controller   = new OrderWithdrawalController();
-		$notification = new OrderWithdrawalFeatureHighlightNotification();
+		$controller    = new OrderWithdrawalController();
+		$notification  = new OrderWithdrawalFeatureHighlightNotification();
+		$email_preview = new OrderWithdrawalEmailPreview();

 		$controller->init(
 			$this->sut,
 			new OrderWithdrawalFormView(),
-			$notification
+			$notification,
+			$email_preview
 		);

 		try {
@@ -975,6 +1016,61 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		$this->assertSame( 'https://example.test/account/withdraw-order/', $args['form_action_url'], 'The view should expose the form action URL.' );
 	}

+	/**
+	 * @testdox Customer order withdrawal email should reject malformed trigger data.
+	 */
+	public function test_customer_order_withdrawal_email_rejects_malformed_trigger_data(): void {
+		$email   = new WC_Email_Customer_Order_Withdrawal_Requested();
+		$capture = $this->capture_wp_mail();
+
+		try {
+			$result = $email->trigger(
+				array_merge(
+					$this->get_valid_form_data(),
+					array(
+						OrderWithdrawalFormProcessor::FIELD_EMAIL           => 'not-an-email',
+						OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE => array( OrderWithdrawalFormProcessor::WITHDRAWAL_TYPE_FULL ),
+					)
+				),
+				time()
+			);
+
+			$this->assertFalse( $result, 'Malformed trigger data should prevent the customer email from sending.' );
+			$this->assertCount( 0, $capture['captures'], 'Malformed trigger data should not call wp_mail().' );
+		} finally {
+			$capture['remove']();
+		}
+	}
+
+	/**
+	 * @testdox Merchant order withdrawal email should reject malformed trigger data.
+	 */
+	public function test_merchant_order_withdrawal_email_rejects_malformed_trigger_data(): void {
+		$email   = new WC_Email_Order_Withdrawal_Requested();
+		$capture = $this->capture_wp_mail();
+
+		try {
+			$result = $email->trigger(
+				array_merge(
+					$this->get_valid_form_data(),
+					array(
+						OrderWithdrawalFormProcessor::FIELD_ORDER_NUMBER    => '',
+						OrderWithdrawalFormProcessor::FIELD_WITHDRAWAL_TYPE => array( OrderWithdrawalFormProcessor::WITHDRAWAL_TYPE_FULL ),
+					)
+				),
+				null,
+				time(),
+				false,
+				''
+			);
+
+			$this->assertFalse( $result, 'Malformed trigger data should prevent the merchant email from sending.' );
+			$this->assertCount( 0, $capture['captures'], 'Malformed trigger data should not call wp_mail().' );
+		} finally {
+			$capture['remove']();
+		}
+	}
+
 	/**
 	 * Prepare a form POST request.
 	 *