Commit 5a753b9e404 for woocommerce

commit 5a753b9e404c52eea590db04ecfbe9e9e6aed4aa
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Wed Oct 7 14:11:08 2026 +0200

    Prevent Cc/Bcc recipients on emails that carry a password reset or verification link (#69525)

    * Prevent Cc/Bcc recipients on emails that carry a password reset or verification link

    Co-authored-by: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>

    * Use empty Cc/Bcc values in the sidebar settings test fixtures

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Move WC_Email Cc/Bcc tests out of the legacy suite

    The legacy unit test suite must not receive new tests. The base-class
    Cc/Bcc tests now live in tests/php/includes/emails/class-wc-email-test.php,
    and the legacy email test class is back to its previous state.

    * Disable Cc/Bcc for the Back in stock verification email

    Its body carries a one-time verification link, like the Confirm email
    address email, so it follows the same rule as the other emails that
    opt out of Cc/Bcc.

    ---------

    Co-authored-by: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
    Co-authored-by: Rostislav Wolny <rosta.wolny@automattic.com>

diff --git a/plugins/woocommerce/changelog/fix-credential-emails-cc-bcc b/plugins/woocommerce/changelog/fix-credential-emails-cc-bcc
new file mode 100644
index 00000000000..df4db5fa023
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-credential-emails-cc-bcc
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent Cc/Bcc recipients from being configured for emails that carry a password reset key or a one-time verification link.
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/sidebar-settings.test.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/sidebar-settings.test.tsx
index d15cc7f7c4a..ad845ab57a9 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/sidebar-settings.test.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/sidebar-settings.test.tsx
@@ -98,8 +98,8 @@ import { modifySidebar } from '../sidebar_settings';

 const defaultWooCommerceData: WooCommerceData = {
 	recipient: 'merchant@example.com',
-	cc: null,
-	bcc: null,
+	cc: '',
+	bcc: '',
 	preheader: 'Order update preview',
 	email_type: 'new_order',
 	subject: 'New order',
@@ -314,14 +314,14 @@ describe( 'Email editor sidebar settings', () => {
 		mockEntityState.woocommerceData = {
 			...defaultWooCommerceData,
 			recipient: null,
-			cc: null,
-			bcc: null,
+			cc: '',
+			bcc: '',
 		};
 		mockEntityState.editedWooCommerceData = {
 			...defaultWooCommerceData,
 			recipient: null,
-			cc: null,
-			bcc: null,
+			cc: '',
+			bcc: '',
 			unrelated_setting: 'preserved',
 		};
 		const { rerender, SidebarSettings, EmailStatus } = renderSettings( {
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/sidebar_settings.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/sidebar_settings.tsx
index c481abc5199..0fb96c9ce03 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/sidebar_settings.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/sidebar_settings.tsx
@@ -86,6 +86,10 @@ const SidebarSettings = ( {
 	};

 	const previewTextLength = woocommerce_email_data?.preheader?.length ?? 0;
+	// null means the email does not support Cc/Bcc.
+	const supportsCcBcc =
+		woocommerce_email_data.cc !== null &&
+		woocommerce_email_data.bcc !== null;

 	if (
 		woocommerce_email_data.email_type ===
@@ -203,93 +207,97 @@ const SidebarSettings = ( {
 					) }
 				</BaseControl>
 			</PanelRow>
-			<PanelRow>
-				<BaseControl __nextHasNoMarginBottom>
-					<ToggleControl
-						__nextHasNoMarginBottom
-						name="add_cc"
-						checked={ addCC }
-						label={ __( 'Add CC', 'woocommerce' ) }
-						onChange={ ( value ) => {
-							setAddCC( value );
-							if ( ! value ) {
-								updateWooMailProperty( 'cc', '' );
-							}
-							recordEvent( 'email_cc_toggle_clicked', {
-								isEnabled: value,
-							} );
-						} }
-					/>
-				</BaseControl>
-			</PanelRow>
-			{ addCC && (
-				<PanelRow>
-					<BaseControl __nextHasNoMarginBottom>
-						<TextControl
-							__nextHasNoMarginBottom
-							__next40pxDefaultSize
-							data-testid="email_cc"
-							value={ woocommerce_email_data?.cc || '' }
-							onChange={ ( value ) => {
-								updateWooMailProperty( 'cc', value );
-								debouncedRecordEvent(
-									'email_cc_input_updated',
-									{
-										value,
+			{ supportsCcBcc && (
+				<>
+					<PanelRow>
+						<BaseControl __nextHasNoMarginBottom>
+							<ToggleControl
+								__nextHasNoMarginBottom
+								name="add_cc"
+								checked={ addCC }
+								label={ __( 'Add CC', 'woocommerce' ) }
+								onChange={ ( value ) => {
+									setAddCC( value );
+									if ( ! value ) {
+										updateWooMailProperty( 'cc', '' );
 									}
-								);
-							} }
-							help={ __(
-								'Add recipients who will receive a copy of the email. Separate multiple addresses with commas.',
-								'woocommerce'
-							) }
-						/>
-					</BaseControl>
-				</PanelRow>
-			) }
-			<PanelRow>
-				<BaseControl __nextHasNoMarginBottom>
-					<ToggleControl
-						__nextHasNoMarginBottom
-						name="add_bcc"
-						checked={ addBCC }
-						label={ __( 'Add BCC', 'woocommerce' ) }
-						onChange={ ( value ) => {
-							setAddBCC( value );
-							if ( ! value ) {
-								updateWooMailProperty( 'bcc', '' );
-							}
-							recordEvent( 'email_bcc_toggle_clicked', {
-								isEnabled: value,
-							} );
-						} }
-					/>
-				</BaseControl>
-			</PanelRow>
-			{ addBCC && (
-				<PanelRow>
-					<BaseControl __nextHasNoMarginBottom>
-						<TextControl
-							__nextHasNoMarginBottom
-							__next40pxDefaultSize
-							data-testid="email_bcc"
-							value={ woocommerce_email_data?.bcc || '' }
-							onChange={ ( value ) => {
-								updateWooMailProperty( 'bcc', value );
-								debouncedRecordEvent(
-									'email_bcc_input_updated',
-									{
-										value,
+									recordEvent( 'email_cc_toggle_clicked', {
+										isEnabled: value,
+									} );
+								} }
+							/>
+						</BaseControl>
+					</PanelRow>
+					{ addCC && (
+						<PanelRow>
+							<BaseControl __nextHasNoMarginBottom>
+								<TextControl
+									__nextHasNoMarginBottom
+									__next40pxDefaultSize
+									data-testid="email_cc"
+									value={ woocommerce_email_data?.cc || '' }
+									onChange={ ( value ) => {
+										updateWooMailProperty( 'cc', value );
+										debouncedRecordEvent(
+											'email_cc_input_updated',
+											{
+												value,
+											}
+										);
+									} }
+									help={ __(
+										'Add recipients who will receive a copy of the email. Separate multiple addresses with commas.',
+										'woocommerce'
+									) }
+								/>
+							</BaseControl>
+						</PanelRow>
+					) }
+					<PanelRow>
+						<BaseControl __nextHasNoMarginBottom>
+							<ToggleControl
+								__nextHasNoMarginBottom
+								name="add_bcc"
+								checked={ addBCC }
+								label={ __( 'Add BCC', 'woocommerce' ) }
+								onChange={ ( value ) => {
+									setAddBCC( value );
+									if ( ! value ) {
+										updateWooMailProperty( 'bcc', '' );
 									}
-								);
-							} }
-							help={ __(
-								'Add recipients who will receive a hidden copy of the email. Separate multiple addresses with commas.',
-								'woocommerce'
-							) }
-						/>
-					</BaseControl>
-				</PanelRow>
+									recordEvent( 'email_bcc_toggle_clicked', {
+										isEnabled: value,
+									} );
+								} }
+							/>
+						</BaseControl>
+					</PanelRow>
+					{ addBCC && (
+						<PanelRow>
+							<BaseControl __nextHasNoMarginBottom>
+								<TextControl
+									__nextHasNoMarginBottom
+									__next40pxDefaultSize
+									data-testid="email_bcc"
+									value={ woocommerce_email_data?.bcc || '' }
+									onChange={ ( value ) => {
+										updateWooMailProperty( 'bcc', value );
+										debouncedRecordEvent(
+											'email_bcc_input_updated',
+											{
+												value,
+											}
+										);
+									} }
+									help={ __(
+										'Add recipients who will receive a hidden copy of the email. Separate multiple addresses with commas.',
+										'woocommerce'
+									) }
+								/>
+							</BaseControl>
+						</PanelRow>
+					) }
+				</>
 			) }
 		</>
 	);
diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-new-account.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-new-account.php
index 02a16b85aae..3fb6cb06f48 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email-customer-new-account.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-new-account.php
@@ -69,13 +69,14 @@ if ( ! class_exists( 'WC_Email_Customer_New_Account', false ) ) {
 		 * Constructor.
 		 */
 		public function __construct() {
-			$this->id             = 'customer_new_account';
-			$this->customer_email = true;
-			$this->title          = __( 'New account', 'woocommerce' );
-			$this->email_group    = 'accounts';
-			$this->description    = __( 'Send an email to customers notifying them that they have created an account', 'woocommerce' );
-			$this->template_html  = 'emails/customer-new-account.php';
-			$this->template_plain = 'emails/plain/customer-new-account.php';
+			$this->id              = 'customer_new_account';
+			$this->customer_email  = true;
+			$this->supports_cc_bcc = false;
+			$this->title           = __( 'New account', 'woocommerce' );
+			$this->email_group     = 'accounts';
+			$this->description     = __( 'Send an email to customers notifying them that they have created an account', 'woocommerce' );
+			$this->template_html   = 'emails/customer-new-account.php';
+			$this->template_plain  = 'emails/plain/customer-new-account.php';
 			parent::__construct();

 			// Must be after parent's constructor which sets `block_email_editor_enabled` property.
diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-reset-password.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-reset-password.php
index 670012a2d17..d2b2d608ebc 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email-customer-reset-password.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-reset-password.php
@@ -63,8 +63,9 @@ if ( ! class_exists( 'WC_Email_Customer_Reset_Password', false ) ) :
 		 */
 		public function __construct() {

-			$this->id             = 'customer_reset_password';
-			$this->customer_email = true;
+			$this->id              = 'customer_reset_password';
+			$this->customer_email  = true;
+			$this->supports_cc_bcc = false;

 			$this->title       = __( 'Reset password', 'woocommerce' );
 			$this->description = __( 'Send an email to customers notifying them that their password has been reset', 'woocommerce' );
diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php
index 8cf6036cf50..0a6a380d602 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email.php
@@ -289,6 +289,18 @@ class WC_Email extends WC_Settings_API {
 	 */
 	public $block_email_editor_enabled;

+	/**
+	 * Whether Cc/Bcc recipients can be configured for this email.
+	 *
+	 * False for emails that carry a credential such as a password reset key.
+	 * The Cc/Bcc settings are then hidden and stored values are ignored.
+	 * The Cc/Bcc recipient filters still apply.
+	 *
+	 * @since 11.2.0
+	 * @var bool
+	 */
+	protected $supports_cc_bcc = true;
+


 	/**
@@ -334,7 +346,7 @@ class WC_Email extends WC_Settings_API {

 		$this->email_type = $this->get_option( 'email_type' );
 		$this->enabled    = $this->get_option( 'enabled' );
-		if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
+		if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) && $this->supports_cc_bcc() ) {
 			$this->cc  = $this->get_option( 'cc', '' );
 			$this->bcc = $this->get_option( 'bcc', '' );
 		}
@@ -636,6 +648,17 @@ class WC_Email extends WC_Settings_API {
 		return implode( ', ', $recipients );
 	}

+	/**
+	 * Whether Cc/Bcc recipients can be configured for this email.
+	 *
+	 * @since 11.2.0
+	 *
+	 * @return bool
+	 */
+	public function supports_cc_bcc() {
+		return $this->supports_cc_bcc;
+	}
+
 	/**
 	 * Get valid Cc recipients.
 	 *
@@ -646,11 +669,12 @@ class WC_Email extends WC_Settings_API {
 		 * Filter the Cc recipient for the email.
 		 *
 		 * @since 9.8.0
+		 * @since 11.2.0 The base value is empty when the email does not support Cc/Bcc.
 		 * @param string   $cc     Cc recipient.
 		 * @param object   $object The object (ie, product or order) this email relates to, if any.
 		 * @param WC_Email $email  WC_Email instance managing the email.
 		 */
-		$cc  = apply_filters( 'woocommerce_email_cc_recipient_' . $this->id, $this->cc, $this->object, $this );
+		$cc  = apply_filters( 'woocommerce_email_cc_recipient_' . $this->id, $this->supports_cc_bcc() ? $this->cc : '', $this->object, $this );
 		$ccs = array_map( 'trim', explode( ',', $cc ?? '' ) );
 		$ccs = array_filter( $ccs, 'is_email' );
 		$ccs = array_map( 'sanitize_email', $ccs );
@@ -667,11 +691,12 @@ class WC_Email extends WC_Settings_API {
 		 * Filter the Bcc recipient for the email.
 		 *
 		 * @since 9.8.0
+		 * @since 11.2.0 The base value is empty when the email does not support Cc/Bcc.
 		 * @param string   $bcc    Bcc recipient.
 		 * @param object   $object The object (ie, product or order) this email relates to, if any.
 		 * @param WC_Email $email  WC_Email instance managing the email.
 		 */
-		$bcc  = apply_filters( 'woocommerce_email_bcc_recipient_' . $this->id, $this->bcc, $this->object, $this );
+		$bcc  = apply_filters( 'woocommerce_email_bcc_recipient_' . $this->id, $this->supports_cc_bcc() ? $this->bcc : '', $this->object, $this );
 		$bccs = array_map( 'trim', explode( ',', $bcc ?? '' ) );
 		$bccs = array_filter( $bccs, 'is_email' );
 		$bccs = array_map( 'sanitize_email', $bccs );
@@ -1334,7 +1359,7 @@ class WC_Email extends WC_Settings_API {
 				'desc_tip'    => true,
 			),
 		);
-		if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
+		if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) && $this->supports_cc_bcc() ) {
 			$this->form_fields['cc']  = $this->get_cc_field();
 			$this->form_fields['bcc'] = $this->get_bcc_field();
 		}
diff --git a/plugins/woocommerce/src/Internal/CustomerEmailVerification/Emails/CustomerVerifyEmail.php b/plugins/woocommerce/src/Internal/CustomerEmailVerification/Emails/CustomerVerifyEmail.php
index 9843d5fe83b..c215786c63a 100644
--- a/plugins/woocommerce/src/Internal/CustomerEmailVerification/Emails/CustomerVerifyEmail.php
+++ b/plugins/woocommerce/src/Internal/CustomerEmailVerification/Emails/CustomerVerifyEmail.php
@@ -51,13 +51,14 @@ class CustomerVerifyEmail extends WC_Email {
 	 * Constructor.
 	 */
 	public function __construct() {
-		$this->id             = 'customer_verify_email';
-		$this->customer_email = true;
-		$this->title          = __( 'Confirm email address', 'woocommerce' );
-		$this->description    = __( 'Sent to customers with a link to confirm they own their account email address.', 'woocommerce' );
-		$this->template_html  = 'emails/customer-verify-email.php';
-		$this->template_plain = 'emails/plain/customer-verify-email.php';
-		$this->email_group    = 'accounts';
+		$this->id              = 'customer_verify_email';
+		$this->customer_email  = true;
+		$this->supports_cc_bcc = false;
+		$this->title           = __( 'Confirm email address', 'woocommerce' );
+		$this->description     = __( 'Sent to customers with a link to confirm they own their account email address.', 'woocommerce' );
+		$this->template_html   = 'emails/customer-verify-email.php';
+		$this->template_plain  = 'emails/plain/customer-verify-email.php';
+		$this->email_group     = 'accounts';

 		// Trigger.
 		add_action( 'woocommerce_customer_verify_email_notification', array( $this, 'trigger' ), 10, 2 );
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php b/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
index d1a0664f5bb..855073207d9 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
@@ -86,8 +86,8 @@ class EmailApiController {
 			'email_type'      => $email_type,
 			// Recipient is possible to set only for the specific type of emails. When the field `recipient` is set in the form fields, it means that the email type has a recipient field.
 			'recipient'       => array_key_exists( 'recipient', $form_fields ) ? $email->get_option( 'recipient', get_option( 'admin_email' ) ) : null,
-			'cc'              => $email->get_option( 'cc' ),
-			'bcc'             => $email->get_option( 'bcc' ),
+			'cc'              => $email->supports_cc_bcc() ? $email->get_option( 'cc' ) : null,
+			'bcc'             => $email->supports_cc_bcc() ? $email->get_option( 'bcc' ) : null,
 		);
 	}

@@ -136,11 +136,13 @@ class EmailApiController {
 		if ( array_key_exists( 'recipient', $data ) ) {
 			$email->update_option( 'recipient', $data['recipient'] );
 		}
-		if ( array_key_exists( 'cc', $data ) ) {
-			$email->update_option( 'cc', $data['cc'] );
-		}
-		if ( array_key_exists( 'bcc', $data ) ) {
-			$email->update_option( 'bcc', $data['bcc'] );
+		if ( $email->supports_cc_bcc() ) {
+			if ( array_key_exists( 'cc', $data ) ) {
+				$email->update_option( 'cc', $data['cc'] );
+			}
+			if ( array_key_exists( 'bcc', $data ) ) {
+				$email->update_option( 'bcc', $data['bcc'] );
+			}
 		}

 		return null;
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Emails/CustomerStockNotificationVerifyEmail.php b/plugins/woocommerce/src/Internal/StockNotifications/Emails/CustomerStockNotificationVerifyEmail.php
index 9baed3999a7..f9a4c81aea6 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Emails/CustomerStockNotificationVerifyEmail.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Emails/CustomerStockNotificationVerifyEmail.php
@@ -19,8 +19,9 @@ class CustomerStockNotificationVerifyEmail extends WC_Email {
 	 * Constructor.
 	 */
 	public function __construct() {
-		$this->id             = 'customer_stock_notification_verify';
-		$this->customer_email = true;
+		$this->id              = 'customer_stock_notification_verify';
+		$this->customer_email  = true;
+		$this->supports_cc_bcc = false;

 		$this->title       = __( 'Back in stock sign-up verification', 'woocommerce' );
 		$this->description = __( 'Verification e-mail sent to customers, as part of the double opt-in sign-up process.', 'woocommerce' );
diff --git a/plugins/woocommerce/tests/e2e/tests/api-tests/settings/settings-crud.test.ts b/plugins/woocommerce/tests/e2e/tests/api-tests/settings/settings-crud.test.ts
index 523dcf076be..f690eb90208 100644
--- a/plugins/woocommerce/tests/e2e/tests/api-tests/settings/settings-crud.test.ts
+++ b/plugins/woocommerce/tests/e2e/tests/api-tests/settings/settings-crud.test.ts
@@ -2966,39 +2966,11 @@ test.describe( 'Settings API tests: CRUD', () => {
 					} ),
 				] )
 			);
-			expect( responseJSON ).toEqual(
-				expect.arrayContaining( [
-					expect.objectContaining( {
-						id: 'cc',
-						label: 'Cc(s)',
-						description: expect.stringContaining(
-							'Enter Cc recipients (comma-separated) for this email.'
-						),
-						type: 'text',
-						default: '',
-						tip: expect.stringContaining(
-							'Enter Cc recipients (comma-separated) for this email.'
-						),
-						value: expect.any( String ),
-					} ),
-				] )
+			expect( responseJSON ).not.toContainEqual(
+				expect.objectContaining( { id: 'cc' } )
 			);
-			expect( responseJSON ).toEqual(
-				expect.arrayContaining( [
-					expect.objectContaining( {
-						id: 'bcc',
-						label: 'Bcc(s)',
-						description: expect.stringContaining(
-							'Enter Bcc recipients (comma-separated) for this email.'
-						),
-						type: 'text',
-						default: '',
-						tip: expect.stringContaining(
-							'Enter Bcc recipients (comma-separated) for this email.'
-						),
-						value: expect.any( String ),
-					} ),
-				] )
+			expect( responseJSON ).not.toContainEqual(
+				expect.objectContaining( { id: 'bcc' } )
 			);
 		} );
 	} );
@@ -3088,39 +3060,11 @@ test.describe( 'Settings API tests: CRUD', () => {
 					} ),
 				] )
 			);
-			expect( responseJSON ).toEqual(
-				expect.arrayContaining( [
-					expect.objectContaining( {
-						id: 'cc',
-						label: 'Cc(s)',
-						description: expect.stringContaining(
-							'Enter Cc recipients (comma-separated) for this email.'
-						),
-						type: 'text',
-						default: '',
-						tip: expect.stringContaining(
-							'Enter Cc recipients (comma-separated) for this email.'
-						),
-						value: expect.any( String ),
-					} ),
-				] )
+			expect( responseJSON ).not.toContainEqual(
+				expect.objectContaining( { id: 'cc' } )
 			);
-			expect( responseJSON ).toEqual(
-				expect.arrayContaining( [
-					expect.objectContaining( {
-						id: 'bcc',
-						label: 'Bcc(s)',
-						description: expect.stringContaining(
-							'Enter Bcc recipients (comma-separated) for this email.'
-						),
-						type: 'text',
-						default: '',
-						tip: expect.stringContaining(
-							'Enter Bcc recipients (comma-separated) for this email.'
-						),
-						value: expect.any( String ),
-					} ),
-				] )
+			expect( responseJSON ).not.toContainEqual(
+				expect.objectContaining( { id: 'bcc' } )
 			);
 		} );
 	} );
diff --git a/plugins/woocommerce/tests/php/includes/emails/class-wc-email-customer-reset-password-test.php b/plugins/woocommerce/tests/php/includes/emails/class-wc-email-customer-reset-password-test.php
new file mode 100644
index 00000000000..2249940cd3e
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/emails/class-wc-email-customer-reset-password-test.php
@@ -0,0 +1,85 @@
+<?php
+declare( strict_types = 1 );
+
+use Automattic\WooCommerce\Internal\Features\FeaturesController;
+
+/**
+ * WC_Email_Customer_Reset_Password test.
+ *
+ * @covers WC_Email_Customer_Reset_Password
+ */
+class WC_Email_Customer_Reset_Password_Test extends \WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var WC_Email_Customer_Reset_Password
+	 */
+	private $sut;
+
+	/**
+	 * Original value of the email_improvements feature flag.
+	 *
+	 * @var bool
+	 */
+	private $email_improvements_was_enabled;
+
+	/**
+	 * Enable email improvements so Cc/Bcc settings would normally be honoured, plant Cc/Bcc settings, and build the email.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$features_controller                  = wc_get_container()->get( FeaturesController::class );
+		$this->email_improvements_was_enabled = $features_controller->feature_is_enabled( 'email_improvements' );
+		$features_controller->change_feature_enable( 'email_improvements', true );
+
+		WC()->mailer();
+
+		update_option(
+			'woocommerce_customer_reset_password_settings',
+			array(
+				'cc'  => 'cc@example.com',
+				'bcc' => 'copy@example.com',
+			)
+		);
+		$this->sut = new WC_Email_Customer_Reset_Password();
+	}
+
+	/**
+	 * Restore the feature flag.
+	 */
+	public function tearDown(): void {
+		wc_get_container()->get( FeaturesController::class )->change_feature_enable( 'email_improvements', $this->email_improvements_was_enabled );
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Cc/Bcc are not configurable and stored values never reach the sent email.
+	 */
+	public function test_stored_cc_bcc_settings_are_ignored(): void {
+		$this->assertSame( 'copy@example.com', $this->sut->get_option( 'bcc' ), 'Fixture: stored value must be readable under this option key' );
+		$this->assertFalse( $this->sut->supports_cc_bcc() );
+		$this->assertArrayNotHasKey( 'cc', $this->sut->get_form_fields() );
+		$this->assertArrayNotHasKey( 'bcc', $this->sut->get_form_fields() );
+
+		$user = $this->factory()->user->create_and_get(
+			array(
+				'user_email' => 'owner@example.com',
+				'role'       => 'administrator',
+			)
+		);
+
+		$mailer = tests_retrieve_phpmailer_instance();
+		$before = count( $mailer->mock_sent );
+
+		$this->sut->trigger( $user->user_login, 'reset-key' );
+
+		$this->assertCount( $before + 1, $mailer->mock_sent, 'Exactly one email must be sent' );
+		$sent = $mailer->mock_sent[ $before ];
+		$this->assertSame( 'owner@example.com', $sent['to'][0][0] );
+		$this->assertEmpty( $sent['cc'], 'Stored Cc must be ignored' );
+		$this->assertEmpty( $sent['bcc'], 'Stored Bcc must be ignored' );
+	}
+}
diff --git a/plugins/woocommerce/tests/php/includes/emails/class-wc-email-test.php b/plugins/woocommerce/tests/php/includes/emails/class-wc-email-test.php
new file mode 100644
index 00000000000..81570119c99
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/emails/class-wc-email-test.php
@@ -0,0 +1,125 @@
+<?php
+declare( strict_types = 1 );
+
+use Automattic\WooCommerce\Internal\Features\FeaturesController;
+
+/**
+ * WC_Email test.
+ *
+ * @covers WC_Email
+ */
+class WC_Email_Test extends \WC_Unit_Test_Case {
+
+	/**
+	 * Original value of the email_improvements feature flag.
+	 *
+	 * @var bool
+	 */
+	private $email_improvements_was_enabled;
+
+	/**
+	 * Enable email improvements so Cc/Bcc settings would normally be honoured, and load the email classes.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$features_controller                  = wc_get_container()->get( FeaturesController::class );
+		$this->email_improvements_was_enabled = $features_controller->feature_is_enabled( 'email_improvements' );
+		$features_controller->change_feature_enable( 'email_improvements', true );
+
+		WC()->mailer();
+	}
+
+	/**
+	 * Restore the feature flag.
+	 */
+	public function tearDown(): void {
+		wc_get_container()->get( FeaturesController::class )->change_feature_enable( 'email_improvements', $this->email_improvements_was_enabled );
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Emails that do not support Cc/Bcc hide the fields and ignore stored or directly set values.
+	 */
+	public function test_cc_bcc_are_ignored_when_email_does_not_support_them(): void {
+		update_option(
+			'woocommerce_no_cc_bcc_test_settings',
+			array(
+				'cc'  => 'cc@example.com',
+				'bcc' => 'bcc@example.com',
+			)
+		);
+		$email = $this->create_email_without_cc_bcc_support();
+
+		$this->assertSame( 'cc@example.com', $email->get_option( 'cc' ), 'Fixture: stored value must be readable under this option key' );
+		$this->assertFalse( $email->supports_cc_bcc() );
+		$this->assertNull( $email->cc, 'Constructor must not load the stored Cc' );
+		$this->assertNull( $email->bcc, 'Constructor must not load the stored Bcc' );
+		$this->assertArrayNotHasKey( 'cc', $email->get_form_fields() );
+		$this->assertArrayNotHasKey( 'bcc', $email->get_form_fields() );
+		$this->assertStringNotContainsString( 'Cc:', $email->get_headers(), 'Stored Cc must be ignored' );
+		$this->assertStringNotContainsString( 'Bcc:', $email->get_headers(), 'Stored Bcc must be ignored' );
+
+		$email->cc  = 'cc@example.com';
+		$email->bcc = 'bcc@example.com';
+
+		$this->assertStringNotContainsString( 'Cc:', $email->get_headers(), 'Cc set on the object must be ignored' );
+		$this->assertStringNotContainsString( 'Bcc:', $email->get_headers(), 'Bcc set on the object must be ignored' );
+	}
+
+	/**
+	 * @testdox The Cc/Bcc recipient filters still apply to emails that do not support Cc/Bcc.
+	 *
+	 * @testWith ["cc", "Cc"]
+	 *           ["bcc", "Bcc"]
+	 *
+	 * @param string $type   Filter type: cc or bcc.
+	 * @param string $header Header name the filtered value must appear under.
+	 */
+	public function test_cc_bcc_filters_apply_when_email_does_not_support_them( string $type, string $header ): void {
+		add_filter(
+			"woocommerce_email_{$type}_recipient_no_cc_bcc_test",
+			function ( $value ) {
+				$this->assertSame( '', $value, 'Filter must not receive a stored value' );
+				return 'audit@example.com';
+			}
+		);
+		$email          = $this->create_email_without_cc_bcc_support();
+		$email->{$type} = 'stored@example.com';
+
+		$this->assertStringContainsString( "{$header}: audit@example.com", $email->get_headers() );
+	}
+
+	/**
+	 * @testdox Emails that carry a credential do not support Cc/Bcc.
+	 *
+	 * @testWith ["WC_Email_Customer_Reset_Password"]
+	 *           ["WC_Email_Customer_New_Account"]
+	 *           ["Automattic\\WooCommerce\\Internal\\CustomerEmailVerification\\Emails\\CustomerVerifyEmail"]
+	 *           ["Automattic\\WooCommerce\\Internal\\StockNotifications\\Emails\\CustomerStockNotificationVerifyEmail"]
+	 *
+	 * @param string $class_name Email class name.
+	 */
+	public function test_credential_emails_do_not_support_cc_bcc( string $class_name ): void {
+		$this->assertFalse( ( new $class_name() )->supports_cc_bcc() );
+	}
+
+	/**
+	 * Create an email that opts out of Cc/Bcc support.
+	 *
+	 * @return WC_Email
+	 */
+	private function create_email_without_cc_bcc_support(): WC_Email {
+		return new class() extends WC_Email {
+			/**
+			 * Constructor.
+			 */
+			public function __construct() {
+				$this->id              = 'no_cc_bcc_test';
+				$this->supports_cc_bcc = false;
+				parent::__construct();
+			}
+		};
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
index 02cbe5b06ff..b41e62d3a20 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
@@ -105,10 +105,11 @@ class EmailApiControllerTest extends \WC_Unit_Test_Case {
 				array( 'subject_partial', null, null ),
 				array( 'preheader', null, 'Test Preheader' ),
 				array( 'recipient', get_option( 'admin_email' ), 'admin@example.com' ),
-				array( 'cc', null, null ),
-				array( 'bcc', null, null ),
+				array( 'cc', null, 'cc@example.com' ),
+				array( 'bcc', null, 'bcc@example.com' ),
 			)
 		);
+		$mock_email->method( 'supports_cc_bcc' )->willReturn( true );
 		$mock_email->method( 'get_default_subject' )->willReturn( 'Default Subject' );
 		$mock_email->method( 'get_form_fields' )->willReturn(
 			array(
@@ -131,6 +132,8 @@ class EmailApiControllerTest extends \WC_Unit_Test_Case {
 		$this->assertEquals( 'Test Preheader', $result['preheader'] );
 		$this->assertEquals( $this->email_type, $result['email_type'] );
 		$this->assertEquals( 'admin@example.com', $result['recipient'] );
+		$this->assertEquals( 'cc@example.com', $result['cc'] );
+		$this->assertEquals( 'bcc@example.com', $result['bcc'] );
 	}

 	/**
@@ -164,6 +167,79 @@ class EmailApiControllerTest extends \WC_Unit_Test_Case {
 		$this->assertEquals( 'bcc@example.com', $option['bcc'] );
 	}

+	/**
+	 * @testdox get_email_data() returns null for Cc/Bcc when the email does not support them.
+	 */
+	public function test_get_email_data_returns_null_cc_bcc_when_unsupported(): void {
+		update_option(
+			'woocommerce_' . $this->email_type . '_settings',
+			array(
+				'cc'  => 'cc@example.com',
+				'bcc' => 'bcc@example.com',
+			)
+		);
+		$controller = $this->create_controller_with_email( $this->create_email_without_cc_bcc_support() );
+
+		$result = $controller->get_email_data( array( 'id' => $this->email_post->ID ) );
+
+		$this->assertNull( $result['cc'] );
+		$this->assertNull( $result['bcc'] );
+	}
+
+	/**
+	 * @testdox save_email_data() ignores Cc/Bcc when the email does not support them.
+	 */
+	public function test_save_email_data_ignores_cc_bcc_when_unsupported(): void {
+		$controller = $this->create_controller_with_email( $this->create_email_without_cc_bcc_support() );
+
+		$controller->save_email_data(
+			array(
+				'subject' => 'Updated Subject',
+				'cc'      => 'cc@example.com',
+				'bcc'     => 'bcc@example.com',
+			),
+			$this->email_post
+		);
+
+		$option = get_option( 'woocommerce_' . $this->email_type . '_settings' );
+		$this->assertEquals( 'Updated Subject', $option['subject'] );
+		$this->assertArrayNotHasKey( 'cc', $option );
+		$this->assertArrayNotHasKey( 'bcc', $option );
+	}
+
+	/**
+	 * Create a controller whose email registry contains only the given email.
+	 *
+	 * @param \WC_Email $email The email to register.
+	 * @return EmailApiController
+	 */
+	private function create_controller_with_email( \WC_Email $email ): EmailApiController {
+		$controller = $this->getMockBuilder( EmailApiController::class )
+			->onlyMethods( array( 'get_emails' ) )
+			->getMock();
+		$controller->method( 'get_emails' )
+			->willReturn( array( $email ) );
+		$controller->init();
+		return $controller;
+	}
+
+	/**
+	 * Create a test email that opts out of Cc/Bcc support.
+	 *
+	 * @return EmailStub
+	 */
+	private function create_email_without_cc_bcc_support(): EmailStub {
+		return new class() extends EmailStub {
+			/**
+			 * Constructor.
+			 */
+			public function __construct() {
+				$this->supports_cc_bcc = false;
+				parent::__construct();
+			}
+		};
+	}
+
 	/**
 	 * Test that the email data schema returns the expected schema.
 	 */
@@ -229,6 +305,7 @@ class EmailApiControllerTest extends \WC_Unit_Test_Case {
 				array( 'bcc', null, null ),
 			)
 		);
+		$mock_email->method( 'supports_cc_bcc' )->willReturn( true );
 		$mock_email->method( 'get_default_subject' )->willReturn( 'Default Subject' );
 		$mock_email->method( 'get_form_fields' )->willReturn(
 			array(
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php
index fe905bf0ae2..e2e6c12c9ab 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php
@@ -13,6 +13,7 @@ use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Settings\Emails\Controller
 use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Settings\Emails\Schema\EmailsSettingsSchema;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsGenerator;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;
+use Automattic\WooCommerce\Internal\Features\FeaturesController;
 use Automattic\WooCommerce\EmailEditor\Email_Editor_Container;
 use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
 use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tag;
@@ -313,6 +314,57 @@ class EmailsSettingsControllerTest extends WC_Unit_Test_Case {
 		$this->assertEmpty( $data );
 	}

+	/**
+	 * @testdox Cc/Bcc sent for an email that does not support them are dropped, while other emails still accept them.
+	 */
+	public function test_update_item_drops_cc_bcc_for_email_without_cc_bcc_support() {
+		$features_controller = wc_get_container()->get( FeaturesController::class );
+		$original_value      = $features_controller->feature_is_enabled( 'email_improvements' );
+		$features_controller->change_feature_enable( 'email_improvements', true );
+		$this->prev_options['woocommerce_customer_reset_password_settings'] = get_option( 'woocommerce_customer_reset_password_settings', null );
+		delete_option( 'woocommerce_customer_reset_password_settings' );
+		wp_set_current_user( self::$user_id );
+
+		$response = $this->put_values( self::SAMPLE_EMAIL_ID, array( 'cc' => 'copy@example.com' ) );
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertEquals( 'copy@example.com', $response->get_data()['values']['cc'], 'Control: an email that supports Cc must still accept it' );
+
+		$response = $this->put_values(
+			'customer_reset_password',
+			array(
+				'subject' => 'Reset subject',
+				'cc'      => 'cc@example.com',
+				'bcc'     => 'copy@example.com',
+			)
+		);
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertEquals( 'Reset subject', $data['values']['subject'] );
+		$this->assertArrayNotHasKey( 'cc', $data['values'] );
+		$this->assertArrayNotHasKey( 'bcc', $data['values'] );
+		$settings = get_option( 'woocommerce_customer_reset_password_settings', array() );
+		$this->assertEquals( 'Reset subject', $settings['subject'] );
+		$this->assertArrayNotHasKey( 'cc', $settings );
+		$this->assertArrayNotHasKey( 'bcc', $settings );
+
+		$features_controller->change_feature_enable( 'email_improvements', $original_value );
+	}
+
+	/**
+	 * Dispatch a PUT request updating the given values of an email.
+	 *
+	 * @param string $email_id Email ID.
+	 * @param array  $values   Settings values to update.
+	 * @return \WP_REST_Response
+	 */
+	private function put_values( string $email_id, array $values ) {
+		$request = new WP_REST_Request( 'PUT', '/wc/v4/settings/emails/' . $email_id );
+		$request->set_header( 'Content-Type', 'application/json' );
+		$request->set_body( wp_json_encode( array( 'values' => $values ) ) );
+		return $this->server->dispatch( $request );
+	}
+
 	/**
 	 * Test successfully updating email settings.
 	 */