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.
*/