From d071ddf4aa35a5800524e73248dad1ae7a3c3d5c Mon Sep 17 00:00:00 2001 From: SirLouen Date: Wed, 25 Jun 2025 00:44:54 +0200 Subject: [PATCH 1/5] Refreshing 15448_Sep2019.11.diff --- src/wp-includes/pluggable.php | 100 +++++++++++++---------- tests/phpunit/tests/pluggable/wpMail.php | 73 +++++++++++++++++ 2 files changed, 131 insertions(+), 42 deletions(-) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 1dbac5e1d707c..2de894bccc90e 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -155,6 +155,11 @@ function cache_users( $user_ids ) { * However, you can set the content type of the email by using the * {@see 'wp_mail_content_type'} filter. * + * If $message is an array, the key of each is used to add as an attachment + * with the value used as the body. The 'text/plain' element is used as the + * text version of the body, with the 'text/html' element used as the HTML + * version of the body. All other types are added as attachments. + * * The default charset is based on the charset used on the blog. The charset can * be set using the {@see 'wp_mail_charset'} filter. * @@ -166,7 +171,7 @@ function cache_users( $user_ids ) { * * @param string|string[] $to Array or comma-separated list of email addresses to send message. * @param string $subject Email subject. - * @param string $message Message contents. + * @param string|array $message Message contents * @param string|string[] $headers Optional. Additional headers. * @param string|string[] $attachments Optional. Paths to files to attach. * @return bool Whether the email was sent successfully. @@ -318,6 +323,10 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() } break; case 'content-type': + if ( is_array( $message ) ) { + // Multipart email, ignore the content-type header + break; + } if ( str_contains( $content, ';' ) ) { list( $type, $charset_content ) = explode( ';', $content ); $content_type = trim( $type ); @@ -417,10 +426,6 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() return false; } - // Set mail's subject and body. - $phpmailer->Subject = $subject; - $phpmailer->Body = $message; - // Set destination addresses, using appropriate methods for handling addresses. $address_headers = compact( 'to', 'cc', 'bcc', 'reply_to' ); @@ -461,32 +466,7 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() } } - // Set to use PHP's mail(). - $phpmailer->isMail(); - - // Set Content-Type and charset. - - // If we don't have a Content-Type from the input headers. - if ( ! isset( $content_type ) ) { - $content_type = 'text/plain'; - } - - /** - * Filters the wp_mail() content type. - * - * @since 2.3.0 - * - * @param string $content_type Default wp_mail() content type. - */ - $content_type = apply_filters( 'wp_mail_content_type', $content_type ); - - $phpmailer->ContentType = $content_type; - - // Set whether it's plaintext, depending on $content_type. - if ( 'text/html' === $content_type ) { - $phpmailer->isHTML( true ); - } - + //Set a charset. // If we don't have a charset from the input headers. if ( ! isset( $charset ) ) { $charset = get_bloginfo( 'charset' ); @@ -501,22 +481,58 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() */ $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset ); - // Set custom headers. - if ( ! empty( $headers ) ) { - foreach ( (array) $headers as $name => $content ) { - // Only add custom headers not added automatically by PHPMailer. - if ( ! in_array( $name, array( 'MIME-Version', 'X-Mailer' ), true ) ) { - try { - $phpmailer->addCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) ); - } catch ( PHPMailer\PHPMailer\Exception $e ) { - continue; - } - } + // Set mail's subject and body + $phpmailer->Subject = $subject; + + if ( is_string( $message ) ) { + $phpmailer->Body = $message; + // Set Content-Type + // If we don't have a content-type from the input headers + if ( ! isset( $content_type ) ) { + $content_type = 'text/plain'; } + /** + * Filters the wp_mail() content type. + * + * @since 2.3.0 + * + * @param string $content_type Default wp_mail() content type. + */ + $content_type = apply_filters( 'wp_mail_content_type', $content_type ); + $phpmailer->ContentType = $content_type; + // Set whether it's plaintext, depending on $content_type + if ( 'text/html' === $content_type ) { + $phpmailer->isHTML( true ); + } + + // For backwards compatibility, new multipart emails should use + // the array style $message. This never really worked well anyway if ( false !== stripos( $content_type, 'multipart' ) && ! empty( $boundary ) ) { $phpmailer->addCustomHeader( sprintf( 'Content-Type: %s; boundary="%s"', $content_type, $boundary ) ); } + } elseif ( is_array( $message ) ) { + foreach ( $message as $type => $bodies ) { + foreach ( (array) $bodies as $body ) { + if ( 'text/html' === $type ) { + $phpmailer->Body = $body; + } elseif ( 'text/plain' === $type ) { + $phpmailer->AltBody = $body; + } else { + $phpmailer->addAttachment( $body, '', 'base64', $type ); + } + } + } + } + + // Set to use PHP's mail() + $phpmailer->isMail(); + + // Set custom headers + if ( ! empty( $headers ) ) { + foreach ( (array) $headers as $name => $content ) { + $phpmailer->addCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) ); + } } if ( ! empty( $attachments ) ) { diff --git a/tests/phpunit/tests/pluggable/wpMail.php b/tests/phpunit/tests/pluggable/wpMail.php index 7b88d739add22..88ee39a63005a 100644 --- a/tests/phpunit/tests/pluggable/wpMail.php +++ b/tests/phpunit/tests/pluggable/wpMail.php @@ -554,4 +554,77 @@ public function test_wp_mail_resets_properties() { $phpmailer = $GLOBALS['phpmailer']; $this->assertNotSame( 'user1', $phpmailer->AltBody ); } + + /** + * Test that wp_mail() can send a multipart/alternative email with plain text and html versions. + * + * @ticket 15448 + */ + public function test_wp_mail_plain_and_html() { + $to = 'user@example.com'; + $subject = 'Test email with plain text and html versions'; + $messages = array( + 'text/plain' => 'Here is some plain text.', + 'text/html' => 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)', + ); + + wp_mail( $to, $subject, $messages ); + + preg_match( '/boundary="(.*)"/', $GLOBALS['phpmailer']->mock_sent[0]['header'], $matches ); + $boundary = $matches[1]; + $body = '--' . $boundary . "\r\n"; + $body .= 'Content-Type: text/plain; charset=us-ascii' . "\r\n"; + $body .= "\r\n"; + $body .= 'Here is some plain text.' . "\r\n"; + $body .= "\r\n"; + $body .= '--' . $boundary . "\r\n"; + $body .= 'Content-Type: text/html; charset=UTF-8' . "\r\n"; + $body .= 'Content-Transfer-Encoding: 8bit' . "\r\n"; + $body .= "\r\n"; + $body .= 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)' . "\r\n"; + $body .= "\r\n"; + $body .= "\r\n"; + $body .= '--' . $boundary . '--' . "\r\n"; + + // We need some better assertions here but these test the behaviour for now. + $this->assertEquals( $body, $GLOBALS['phpmailer']->mock_sent[0]['body'], 'The body is not as expected.' ); + $this->assertSame( + 1, + substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type: multipart/alternative;' ), + 'The multipart / alternative header is not present.' + ); + $this->assertSame( + 1, + substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type:' ), + 'The Content-Type header is not present.' + ); + } + + /* + * 'phpmailer_init' action for test_wp_mail_plain_and_html_workaround(). + */ + public function wp_mail_set_alt_body( $mailer ) { + $mailer->AltBody = strip_tags( $mailer->Body ); + } + + /** + * Check workarounds using phpmailer_init still work around. + * + * @ticket 15448 + */ + public function test_wp_mail_plain_and_html_workaround() { + $to = 'user@example.com'; + $subject = 'Test email with plain text derived from html version'; + $message = '

Hello World! γειά σου Κόσμε

'; + + add_action( 'phpmailer_init', array( $this, 'wp_mail_set_alt_body' ) ); + wp_mail( $to, $subject, $message ); + remove_action( 'phpmailer_init', array( $this, 'wp_mail_set_alt_body' ) ); + + $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type: multipart/alternative;' ) ); + $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type:' ) ); + $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['body'], 'Content-Type: text/plain; charset=UTF-8' ) ); + $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['body'], 'Content-Type: text/html; charset=UTF-8' ) ); + $this->assertSame( 2, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['body'], 'Content-Type:' ) ); + } } From 3effbcf694ac9eb1bc7c5c797b8e6678ecebdf01 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Wed, 25 Jun 2025 01:10:17 +0200 Subject: [PATCH 2/5] Fixing errors with tests --- src/wp-includes/pluggable.php | 23 +++++++++++++-------- tests/phpunit/tests/pluggable/wpMail.php | 26 ++++++++++++------------ 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 2de894bccc90e..5b4c1913b57bd 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -481,13 +481,13 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() */ $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset ); - // Set mail's subject and body + // Set mail's subject and body. $phpmailer->Subject = $subject; if ( is_string( $message ) ) { $phpmailer->Body = $message; - // Set Content-Type - // If we don't have a content-type from the input headers + // Set Content-Type. + // If we don't have a content-type from the input headers. if ( ! isset( $content_type ) ) { $content_type = 'text/plain'; } @@ -501,13 +501,13 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() */ $content_type = apply_filters( 'wp_mail_content_type', $content_type ); $phpmailer->ContentType = $content_type; - // Set whether it's plaintext, depending on $content_type + // Set whether it's plaintext, depending on $content_type. if ( 'text/html' === $content_type ) { $phpmailer->isHTML( true ); } // For backwards compatibility, new multipart emails should use - // the array style $message. This never really worked well anyway + // the array style $message. This never really worked well anyway. if ( false !== stripos( $content_type, 'multipart' ) && ! empty( $boundary ) ) { $phpmailer->addCustomHeader( sprintf( 'Content-Type: %s; boundary="%s"', $content_type, $boundary ) ); } @@ -525,13 +525,20 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() } } - // Set to use PHP's mail() + // Set to use PHP's mail(). $phpmailer->isMail(); - // Set custom headers + // Set custom headers. if ( ! empty( $headers ) ) { foreach ( (array) $headers as $name => $content ) { - $phpmailer->addCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) ); + // Only add custom headers not added automatically by PHPMailer. + if ( ! in_array( $name, array( 'MIME-Version', 'X-Mailer' ), true ) ) { + try { + $phpmailer->addCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) ); + } catch ( PHPMailer\PHPMailer\Exception $e ) { + continue; + } + } } } diff --git a/tests/phpunit/tests/pluggable/wpMail.php b/tests/phpunit/tests/pluggable/wpMail.php index 88ee39a63005a..e42b39b430fc1 100644 --- a/tests/phpunit/tests/pluggable/wpMail.php +++ b/tests/phpunit/tests/pluggable/wpMail.php @@ -572,19 +572,19 @@ public function test_wp_mail_plain_and_html() { preg_match( '/boundary="(.*)"/', $GLOBALS['phpmailer']->mock_sent[0]['header'], $matches ); $boundary = $matches[1]; - $body = '--' . $boundary . "\r\n"; - $body .= 'Content-Type: text/plain; charset=us-ascii' . "\r\n"; - $body .= "\r\n"; - $body .= 'Here is some plain text.' . "\r\n"; - $body .= "\r\n"; - $body .= '--' . $boundary . "\r\n"; - $body .= 'Content-Type: text/html; charset=UTF-8' . "\r\n"; - $body .= 'Content-Transfer-Encoding: 8bit' . "\r\n"; - $body .= "\r\n"; - $body .= 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)' . "\r\n"; - $body .= "\r\n"; - $body .= "\r\n"; - $body .= '--' . $boundary . '--' . "\r\n"; + $body = '--' . $boundary . "\n"; + $body .= 'Content-Type: text/plain; charset=us-ascii' . "\n"; + $body .= "\n"; + $body .= 'Here is some plain text.' . "\n"; + $body .= "\n"; + $body .= '--' . $boundary . "\n"; + $body .= 'Content-Type: text/html; charset=UTF-8' . "\n"; + $body .= 'Content-Transfer-Encoding: 8bit' . "\n"; + $body .= "\n"; + $body .= 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)' . "\n"; + $body .= "\n"; + $body .= "\n"; + $body .= '--' . $boundary . '--' . "\n"; // We need some better assertions here but these test the behaviour for now. $this->assertEquals( $body, $GLOBALS['phpmailer']->mock_sent[0]['body'], 'The body is not as expected.' ); From ee11fd0cc6c4ea7ee475cd31b23a853a3e571150 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Wed, 25 Jun 2025 01:19:42 +0200 Subject: [PATCH 3/5] Regression on Tests --- tests/phpunit/tests/pluggable/wpMail.php | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/phpunit/tests/pluggable/wpMail.php b/tests/phpunit/tests/pluggable/wpMail.php index e42b39b430fc1..88ee39a63005a 100644 --- a/tests/phpunit/tests/pluggable/wpMail.php +++ b/tests/phpunit/tests/pluggable/wpMail.php @@ -572,19 +572,19 @@ public function test_wp_mail_plain_and_html() { preg_match( '/boundary="(.*)"/', $GLOBALS['phpmailer']->mock_sent[0]['header'], $matches ); $boundary = $matches[1]; - $body = '--' . $boundary . "\n"; - $body .= 'Content-Type: text/plain; charset=us-ascii' . "\n"; - $body .= "\n"; - $body .= 'Here is some plain text.' . "\n"; - $body .= "\n"; - $body .= '--' . $boundary . "\n"; - $body .= 'Content-Type: text/html; charset=UTF-8' . "\n"; - $body .= 'Content-Transfer-Encoding: 8bit' . "\n"; - $body .= "\n"; - $body .= 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)' . "\n"; - $body .= "\n"; - $body .= "\n"; - $body .= '--' . $boundary . '--' . "\n"; + $body = '--' . $boundary . "\r\n"; + $body .= 'Content-Type: text/plain; charset=us-ascii' . "\r\n"; + $body .= "\r\n"; + $body .= 'Here is some plain text.' . "\r\n"; + $body .= "\r\n"; + $body .= '--' . $boundary . "\r\n"; + $body .= 'Content-Type: text/html; charset=UTF-8' . "\r\n"; + $body .= 'Content-Transfer-Encoding: 8bit' . "\r\n"; + $body .= "\r\n"; + $body .= 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)' . "\r\n"; + $body .= "\r\n"; + $body .= "\r\n"; + $body .= '--' . $boundary . '--' . "\r\n"; // We need some better assertions here but these test the behaviour for now. $this->assertEquals( $body, $GLOBALS['phpmailer']->mock_sent[0]['body'], 'The body is not as expected.' ); From 2e7de23b44d1f51dec778355564488df5d2e5c33 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Wed, 25 Jun 2025 13:02:01 +0200 Subject: [PATCH 4/5] Testing GHA with str_replace --- tests/phpunit/tests/pluggable/wpMail.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/pluggable/wpMail.php b/tests/phpunit/tests/pluggable/wpMail.php index 88ee39a63005a..ebcd3a1d0e37b 100644 --- a/tests/phpunit/tests/pluggable/wpMail.php +++ b/tests/phpunit/tests/pluggable/wpMail.php @@ -587,7 +587,11 @@ public function test_wp_mail_plain_and_html() { $body .= '--' . $boundary . '--' . "\r\n"; // We need some better assertions here but these test the behaviour for now. - $this->assertEquals( $body, $GLOBALS['phpmailer']->mock_sent[0]['body'], 'The body is not as expected.' ); + $this->assertEquals( + str_replace( "\r\n", "\n", $body ), + str_replace( "\r\n", "\n", $GLOBALS['phpmailer']->mock_sent[0]['body'] ), + 'The body is not as expected.' + ); $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type: multipart/alternative;' ), From 74855c0f3b70a974baab59eb23343c0f2cb49dd1 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Wed, 25 Jun 2025 13:13:16 +0200 Subject: [PATCH 5/5] Normalizing Tests for GHA --- tests/phpunit/tests/pluggable/wpMail.php | 73 ++++++++++++------------ 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/tests/phpunit/tests/pluggable/wpMail.php b/tests/phpunit/tests/pluggable/wpMail.php index ebcd3a1d0e37b..d980f69e13b3b 100644 --- a/tests/phpunit/tests/pluggable/wpMail.php +++ b/tests/phpunit/tests/pluggable/wpMail.php @@ -569,38 +569,29 @@ public function test_wp_mail_plain_and_html() { ); wp_mail( $to, $subject, $messages ); + $mailer = tests_retrieve_phpmailer_instance(); - preg_match( '/boundary="(.*)"/', $GLOBALS['phpmailer']->mock_sent[0]['header'], $matches ); + preg_match( '/boundary="(.*)"/', $mailer->get_sent()->header, $matches ); $boundary = $matches[1]; - $body = '--' . $boundary . "\r\n"; - $body .= 'Content-Type: text/plain; charset=us-ascii' . "\r\n"; - $body .= "\r\n"; - $body .= 'Here is some plain text.' . "\r\n"; - $body .= "\r\n"; - $body .= '--' . $boundary . "\r\n"; - $body .= 'Content-Type: text/html; charset=UTF-8' . "\r\n"; - $body .= 'Content-Transfer-Encoding: 8bit' . "\r\n"; - $body .= "\r\n"; - $body .= 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)' . "\r\n"; - $body .= "\r\n"; - $body .= "\r\n"; - $body .= '--' . $boundary . '--' . "\r\n"; - - // We need some better assertions here but these test the behaviour for now. - $this->assertEquals( - str_replace( "\r\n", "\n", $body ), - str_replace( "\r\n", "\n", $GLOBALS['phpmailer']->mock_sent[0]['body'] ), - 'The body is not as expected.' - ); - $this->assertSame( - 1, - substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type: multipart/alternative;' ), - 'The multipart / alternative header is not present.' - ); - $this->assertSame( - 1, - substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type:' ), - 'The Content-Type header is not present.' + $body = '--' . $boundary . "\n"; + $body .= 'Content-Type: text/plain; charset=us-ascii' . "\n"; + $body .= "\n"; + $body .= 'Here is some plain text.' . "\n"; + $body .= "\n"; + $body .= '--' . $boundary . "\n"; + $body .= 'Content-Type: text/html; charset=UTF-8' . "\n"; + $body .= 'Content-Transfer-Encoding: 8bit' . "\n"; + $body .= "\n"; + $body .= 'Here is the HTML with UTF-8 γειά σου Κόσμε;-)' . "\n"; + $body .= "\n"; + $body .= "\n"; + $body .= '--' . $boundary . '--' . "\n"; + + $this->assertSameIgnoreEOL( $body, $mailer->get_sent()->body, 'The body is not as expected.' ); + $this->assertStringContainsString( + 'Content-Type: multipart/alternative;', + $mailer->get_sent()->header, + 'The multipart/alternative header is not present.' ); } @@ -625,10 +616,22 @@ public function test_wp_mail_plain_and_html_workaround() { wp_mail( $to, $subject, $message ); remove_action( 'phpmailer_init', array( $this, 'wp_mail_set_alt_body' ) ); - $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type: multipart/alternative;' ) ); - $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['header'], 'Content-Type:' ) ); - $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['body'], 'Content-Type: text/plain; charset=UTF-8' ) ); - $this->assertSame( 1, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['body'], 'Content-Type: text/html; charset=UTF-8' ) ); - $this->assertSame( 2, substr_count( $GLOBALS['phpmailer']->mock_sent[0]['body'], 'Content-Type:' ) ); + $mailer = tests_retrieve_phpmailer_instance(); + + $this->assertStringContainsString( + 'Content-Type: multipart/alternative;', + $mailer->get_sent()->header, + 'The multipart/alternative header is not present.' + ); + $this->assertStringContainsString( + 'Content-Type: text/plain; charset=UTF-8', + $mailer->get_sent()->body, + 'The text/plain Content-Type header is not present.' + ); + $this->assertStringContainsString( + 'Content-Type: text/html; charset=UTF-8', + $mailer->get_sent()->body, + 'The text/html Content-Type header is not present.' + ); } }