Skip to content

Conversation

@SirLouen
Copy link
Member

This is the new version that solves all the problems without having to do any upgrade + not having to modify anything in the PHPMailer part. More info and testing info below.

Trac ticket: https://core.trac.wordpress.org/ticket/15448

This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

github-actions bot commented Aug 17, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props sirlouen, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Comment on lines -551 to -553
if ( false !== stripos( $content_type, 'multipart' ) && ! empty( $boundary ) ) {
$phpmailer->addCustomHeader( sprintf( 'Content-Type: %s; boundary="%s"', $content_type, $boundary ) );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried restoring this block of code and the tests still pass. Is that expected?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, should there be a test that checks for only one Content-Type header being present?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the original issue. I'm not sure if this ever worked, but for long time, everyone thought this was doing something. I've been playing a lot with the headers both in other tickets and PHPMailer, and then I got back to this ticket and I noted that this was not been triggered at all on multipart, because if you check the logic above, the default condition of the switch case, is never reached (because the Content-Type is already set in the corresponding case).

If anything happens to drops into the default it is not going to be a Content-Type and for this reason, this is never going to be triggered.

So it can be safely removed, it's useless.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added 2111d68 to ensure there is only one header sent. Without this removed, the assertion now fails with:

1) Tests_Pluggable_wpMail::test_wp_mail_custom_boundaries
Expected only one Content-Type header to be sent. Saw:
multipart/mixed; boundary="----=_Part_4892_25692638.1192452070893"; boundary="----=_Part_4892_25692638.1192452070893"
multipart/mixed; boundary="----=_Part_4892_25692638.1192452070893"; charset=
Failed asserting that actual size 2 matches expected size 1.

@SirLouen SirLouen requested a review from westonruter November 10, 2025 23:37
$this->assertArrayHasKey( 'Content-Type', $headers, 'Expected Content-Type header to be sent.' );
$content_type_headers = (array) $headers['Content-Type'];
$this->assertCount( 1, $content_type_headers, "Expected only one Content-Type header to be sent. Saw:\n" . implode( "\n", $content_type_headers ) );
$this->assertSame( 'multipart/mixed; boundary="----=_Part_4892_25692638.1192452070893"; charset=', $content_type_headers[0], 'Expected Content-Type to match.' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I don't get is why the Content-Type header ends in charset=. I guess PHPMailer adds this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini seems to have explained it well:

I've finished reviewing PHPMailer.php. The Content-Type header is constructed in the getMailMIME() method, which is called by createHeader().

In the test, wp_mail passes a raw MIME message to PHPMailer's $body. PHPMailer's setMessageType() method then determines the message_type as 'plain' because it sees a single body, not a parsed multipart structure.

For a 'plain' message, getMailMIME() constructs the Content-Type header as: 'Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet.

In wp_mail, the provided Content-Type header (multipart/mixed; boundary="...") is parsed. Since there's no charset, wp_mail sets $phpmailer->CharSet to ''.

Therefore, the final header becomes: Content-Type: multipart/mixed; boundary="..."; charset=, which is what the test asserts. This is an intentional behavior in PHPMailer for pre-formatted MIME messages, and the test correctly verifies it.

And the charset is not really relevant in the top-level header for the multipart/mixed content type, since the boundary is all that matters. The actual charset is set on one of the parts.

Copy link
Member

@westonruter westonruter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an expert on mail headers, but from all I can tell this is right.

Gemini also likes it:

Here is my revised review of the changes:

This is an excellent set of changes that improves the handling of multipart Content-Type headers in wp_mail() and adds robust, valuable test coverage.

src/wp-includes/pluggable.php

  • Good Refactor: The logic for handling multipart Content-Type headers has been simplified and made more robust. Moving away from addCustomHeader to directly setting the $content_type variable which is then passed to PHPMailer is a cleaner implementation.
  • Improved Consistency: The new logic to normalize the multipart/ subtype to lowercase and correctly quote the boundary enhances consistency and reduces the chance of malformed headers.

tests/phpunit/tests/pluggable/wpMail.php

  • Precise Assertions: The updated assertions in test_wp_mail_custom_boundaries() are a significant improvement. Using assertSame() provides a much stricter and more reliable test than the previous assertStringContainsString().
  • Excellent Test Coverage: The two new tests, test_wp_mail_plain_and_html() and test_wp_mail_plain_and_html_workaround(), are great additions. They cover common, real-world scenarios for sending multipart emails.
  • Modern PHP: The use of a static function (a static closure) in test_wp_mail_plain_and_html_workaround() is a nice touch. It's a modern, clean way to handle the action callback, keeping the logic scoped to the test itself. The WP_PHPMailer type hint is also good practice.

This is a high-quality contribution. The refactoring is sound, and the tests are well-written and thorough. This looks good to go.

pento pushed a commit that referenced this pull request Nov 11, 2025
This improves how `wp_mail()` handles `Content-Type` headers for multipart messages, preventing cases where the header could be duplicated.

Developed in #9500

Props SirLouen, gitlost, rmccue, westi, MattyRob, bgermann, nacin, SergeyBiryukov, dd32, MikeHansenMe, Kleor, kitchin, JeffMatson, abcd95, westonruter, christinecooper, JohnVieth, dawidadach, imokweb, ayeshrajans, lakshyajeet, tusharbharti, sajjad67.
Fixes #15448.


git-svn-id: https://develop.svn.wordpress.org/trunk@61201 602fd350-edb4-49c9-b593-d223f7449a82
@github-actions
Copy link

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 61201
GitHub commit: 024bbfc

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions bot closed this Nov 11, 2025
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Nov 11, 2025
This improves how `wp_mail()` handles `Content-Type` headers for multipart messages, preventing cases where the header could be duplicated.

Developed in WordPress/wordpress-develop#9500

Props SirLouen, gitlost, rmccue, westi, MattyRob, bgermann, nacin, SergeyBiryukov, dd32, MikeHansenMe, Kleor, kitchin, JeffMatson, abcd95, westonruter, christinecooper, JohnVieth, dawidadach, imokweb, ayeshrajans, lakshyajeet, tusharbharti, sajjad67.
Fixes #15448.

Built from https://develop.svn.wordpress.org/trunk@61201


git-svn-id: http://core.svn.wordpress.org/trunk@60537 1a063a9b-81f0-0310-95a4-ce76da25c4cd
github-actions bot pushed a commit to platformsh/wordpress-performance that referenced this pull request Nov 11, 2025
This improves how `wp_mail()` handles `Content-Type` headers for multipart messages, preventing cases where the header could be duplicated.

Developed in WordPress/wordpress-develop#9500

Props SirLouen, gitlost, rmccue, westi, MattyRob, bgermann, nacin, SergeyBiryukov, dd32, MikeHansenMe, Kleor, kitchin, JeffMatson, abcd95, westonruter, christinecooper, JohnVieth, dawidadach, imokweb, ayeshrajans, lakshyajeet, tusharbharti, sajjad67.
Fixes #15448.

Built from https://develop.svn.wordpress.org/trunk@61201


git-svn-id: https://core.svn.wordpress.org/trunk@60537 1a063a9b-81f0-0310-95a4-ce76da25c4cd
adamziel added a commit to WordPress/wordpress-playground that referenced this pull request Nov 12, 2025
…st PRs (#2868)

## Motivation for the change, related issues

Adds support for previewing branches, not just PRs in the Import PR
modal:

<img width="2288" height="1500" alt="CleanShot 2025-11-05 at 15 13
58@2x"
src="https://github.com/user-attachments/assets/d7efc465-43bc-4786-a66d-493d70ec60b8"
/>

@SirLouen asked how to preview the latest trunk trunk, not a PR, and I
didn't have a good answer. This change makes it easy.

## Implementation details

* Website: adds `?core-branch` and `?gutenberg-branch` Query API params
to indicate which branch to preview.
* Plugin proxy: Adds a `?branch=` query API param that's an alternative
to `?pr=`.

There's no artifact validation when previewing a branch. If the build
for the most recent commit is not ready yet, the next available build is
used.

## Testing Instructions (or ideally a Blueprint)

Try these locally and confirm they do the right thing. You can see the
WordPress version number at the bottom of wp-admin and also use the file
browser to confirm the files changed in specific PRs are present in the
filesystem:

* http://127.0.0.1:5400/website-server/?core-branch=trunk
* http://127.0.0.1:5401/website-server/?core-pr=9500 (see
WordPress/wordpress-develop#9500)
* http://127.0.0.1:5400/website-server/?gutenberg-branch=trunk
* http://127.0.0.1:5401/website-server/?gutenberg-pr=73010 (see
https://github.com/WordPress/wordpress-develop/pull/73010)

---------

Co-authored-by: Copilot <[email protected]>
mattyrob pushed a commit to ClassicPress/ClassicPress that referenced this pull request Nov 12, 2025
This improves how `wp_mail()` handles `Content-Type` headers for multipart messages, preventing cases where the header could be duplicated.

Developed in WordPress/wordpress-develop#9500

WP:Props SirLouen, gitlost, rmccue, westi, MattyRob, bgermann, nacin, SergeyBiryukov, dd32, MikeHansenMe, Kleor, kitchin, JeffMatson, abcd95, westonruter, christinecooper, JohnVieth, dawidadach, imokweb, ayeshrajans, lakshyajeet, tusharbharti, sajjad67.
Fixes https://core.trac.wordpress.org/ticket/15448.

---

Merges https://core.trac.wordpress.org/changeset/61201 / WordPress/wordpress-develop@024bbfcc4c to ClassicPress.
mattyrob added a commit to ClassicPress/ClassicPress that referenced this pull request Nov 16, 2025
* WP-r61201: Mail: Improve multipart message handling in `wp_mail()`.

This improves how `wp_mail()` handles `Content-Type` headers for multipart messages, preventing cases where the header could be duplicated.

Developed in WordPress/wordpress-develop#9500

WP:Props SirLouen, gitlost, rmccue, westi, MattyRob, bgermann, nacin, SergeyBiryukov, dd32, MikeHansenMe, Kleor, kitchin, JeffMatson, abcd95, westonruter, christinecooper, JohnVieth, dawidadach, imokweb, ayeshrajans, lakshyajeet, tusharbharti, sajjad67.
Fixes https://core.trac.wordpress.org/ticket/15448.

---

Merges https://core.trac.wordpress.org/changeset/61201 / WordPress/wordpress-develop@024bbfcc4c to ClassicPress.

* Update callback type hint to MockPHPMailer in test

Changed the type hint from WP_PHPMailer to MockPHPMailer in the $set_alt_body callback within the wpMail test. This ensures the callback matches the actual mock object used in the test environment.

---------

Co-authored-by: Weston Ruter <[email protected]>

CP:Props mattyrob, xxsimoxx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants