Opened 15 years ago
Last modified 17 hours ago
#15448 reviewing feature request
wp_mail() sets Content-Type header twice for multipart emails
Reported by: |
|
Owned by: |
|
---|---|---|---|
Milestone: | Future Release | Priority: | normal |
Severity: | normal | Version: | |
Component: | Keywords: | has-patch has-unit-tests has-test-info reported-upstream | |
Focuses: | Cc: |
Description
When trying to send emails via wp_mail()
with a Content-Type of multipart/alternative, the Content-Type header will be set with $phpmailer->ContentType
, and again with $phpmailer->AddCustomHeader()
, which causes two Content-Type headers in the email:
Content-Type: multipart/alternative; boundary="example_boundary" Content-Type: multipart/alternative; charset=""
This appears to cause errors in Outlook, as there is no boundary on the latter.
The cause of this is PHPMailer::GetMailMIME()
, as it does not know that the email is a multipart email. The easiest way to achieve this appears to be to simply allow the user to set the AltBody via wp_mail()
. In order to achieve backwards compatibility, wp_mail()
should work out which part is the text/plain one and which is the text/html one based on the boundary.
I'll be working on a patch for this.
Attachments (11)
Change History (87)
#2
@
15 years ago
It sounds good to accept an array of alternative versions.
Treat the non-array case either as we do now or as text/plain
#3
@
15 years ago
If the body is a string, we'll use the current parsing (i.e. get Content-Type from the $headers parameter), otherwise, we'll ignore the Content-Type header from there and use the key for each item.
My only concern is adding custom headers to each type. We could accept something like this, I guess:
$body = array( 'text/plain' => array( 'headers' => array( 'X-Type' => 'text' ), 'body' => 'xyz' ) );
#4
@
15 years ago
Do people need type specific custom headers that often would be my question?
Does supporting multiple content types not remove the need for some of the custom headers?
#5
@
15 years ago
I agree. Perhaps more elegant would be to simply have a wp_mail_headers_$type
filter. Then, the $message
parameter we accept could be:
$message = array( 'text/plain' => 'xyz', 'text/html' => '<b>xyz</b>' );
I much prefer this, but it does involve adding an extra filter if people need custom headers.
#6
follow-up:
↓ 9
@
15 years ago
Turns out you *can't* have custom headers with PHPMailer anyway.
The patch I've included allows you to use an array such as the one I mentioned above. What I'd like to do is have the ability to add inline attachments (e.g. images), but I'm not sure how one would achieve that. With images, you're likely to have more than one, and if they're the same type, you're slightly screwed.
Perhaps allowing the following would be better:
$message = array( 'text/plain' => 'xyz', 'text/html' => '<b>xyz</b>', 'image/png' => array( 'file1.png', 'file2.png' ) );
But we still then have the issue of working of if it's inline or an attachment. We might be able to do this based on the type, but I'm thinking a filter would work well for it. I'll submit a patch to that effect later.
#9
in reply to:
↑ 6
@
15 years ago
Replying to rmccue:
Turns out you *can't* have custom headers with PHPMailer anyway.
The patch I've included allows you to use an array such as the one I mentioned above. What I'd like to do is have the ability to add inline attachments (e.g. images), but I'm not sure how one would achieve that. With images, you're likely to have more than one, and if they're the same type, you're slightly screwed.
Perhaps allowing the following would be better:
$message = array( 'text/plain' => 'xyz', 'text/html' => '<b>xyz</b>', 'image/png' => array( 'file1.png', 'file2.png' ) );But we still then have the issue of working of if it's inline or an attachment. We might be able to do this based on the type, but I'm thinking a filter would work well for it. I'll submit a patch to that effect later.
This is looking good.
I just checked the "Unit" Tests we have and found that we do have some for the wp_mail function :-)
Would be good to add some more test cases for your new functionality if you get time but you should at least check that the current ones pass :-D
#10
follow-up:
↓ 11
@
15 years ago
Looks good.
I've added a basic test for this new functionality in [UT322]
#12
in reply to:
↑ 11
@
15 years ago
Replying to rmccue:
Replying to westi:
I've added a basic test for this new functionality in [UT322]
FYI, it's
boundary
;)
Also, it doesn't appear as though the charset matches the blog's character set, but I'm not sure on that. Probably worth a unit test for that too.
:-) Spilling fail
Unit Tests++
#13
@
14 years ago
- Milestone changed from Future Release to 3.2
- Owner changed from rmccue to westi
- Status changed from assigned to reviewing
I would like to roll this small enhancement from GCI into 3.2
#14
@
14 years ago
I think the main check for the $message should be is_array() and else, rather than is_string() elseif is_array(). Patch looks good.
#16
@
14 years ago
Per "This appears to cause errors in Outlook," this sounds like an enhancement that fixes a bug? westi, your call on this one. Looked good a few weeks ago for me.
#18
follow-up:
↓ 19
@
14 years ago
Another bug with the wp_mail function in WordPress 3.2. It doesn't work if we write this:
wp_mail('Example <me@example.net>', 'The subject', 'The message');
We must write this instead:
wp_mail('me@example.net', 'The subject', 'The message');
#19
in reply to:
↑ 18
@
14 years ago
Replying to Kleor:
Another bug with the wp_mail function in WordPress 3.2. It doesn't work if we write this:
wp_mail('Example <me@example.net>', 'The subject', 'The message');
We must write this instead:
wp_mail('me@example.net', 'The subject', 'The message');
That has already been fixed what build are you testing with?
#22
@
14 years ago
- Keywords 3.3-early westi-likes added
- Milestone changed from 3.2 to Future Release
I think this still counts as an enhancement rather than a bug.
At the moment we don't really support multipart emails.
Marking for 3.3
#23
@
14 years ago
The two patches so far both have a major error. If wp_mail() is called twice, AltBody is not cleared. Suggest you add these two lines:
$phpmailer->ClearCCs(); $phpmailer->ClearCustomHeaders(); $phpmailer->ClearReplyTos(); + $phpmailer->Body= ''; + $phpmailer->AltBody= ''; // From email and name // If we don't have a name from the input headers if ( !isset( $from_name ) )
A test for the error in the patch is as follows: call wp_mail() with an array message to send an HTML email. Then call wp_mail() with a string message to send a different plain email. The second message will be send multi-part, with the previous message in one part and the current message in the other part. Security problem in some uses.
The patch needs to be updated anyway, in light of Bug #17305, so I would also suggest incorporating the improvement I suggested there to allow '<foo@…>' in $to and to clean up the regex:
// Break $recipient into name and address parts if in the format "Foo <bar@baz.com>" $recipient_name = ''; - if( preg_match( '/(.+)\s?<(.+)>/', $recipient, $matches ) ) { + if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) { if ( count( $matches ) == 3 ) { $recipient_name = $matches[1]; $recipient = $matches[2]; } } - $phpmailer->AddAddress( trim( $recipient ), $recipient_name); + $phpmailer->AddAddress( trim( $recipient ), trim( $recipient_name) ); } catch ( phpmailerException $e ) { continue; .................. // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>" $recipient_name = ''; - if( preg_match( '/(.+)\s?<(.+)>/', $recipient, $matches ) ) { + if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) { if ( count( $matches ) == 3 ) { $recipient_name = $matches[1]; $recipient = $matches[2]; } } - $phpmailer->AddCc( trim($recipient), $recipient_name ); + $phpmailer->AddCc( trim($recipient), trim($recipient_name) ); } catch ( phpmailerException $e ) { continue; .................. // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>" $recipient_name = ''; - if( preg_match( '/(.+)\s?<(.+)>/', $recipient, $matches ) ) { + if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) { if ( count( $matches ) == 3 ) { $recipient_name = $matches[1]; $recipient = $matches[2]; } } - $phpmailer->AddBcc( trim($recipient), $recipient_name ); + $phpmailer->AddBcc( trim($recipient), trim($recipient_name) ); } catch ( phpmailerException $e ) { continue;
#24
@
14 years ago
Security-wise, it would be best to also clear phpmailer at the end of wp_mail(), actually.
#26
@
12 years ago
This is still a problem. Constructing one's own multipart/mixed email and sending it through wp_mail() results in attachments not being read properly by Hotmail's e-mail program (Gmail and RoundCube work fine). This is due to wp_mail() adding headers so that e.g. in totality, the following headers and body are produced:
MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="==Multipart_Boundary_x{82b884f3b88a46c15d210eda4b90a8f7}x" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Content-Type: multipart/mixed; charset="" --==Multipart_Boundary_x{82b884f3b88a46c15d210eda4b90a8f7}x Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: 8bit Dear Recipient, ...
In this case, the lines
MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Content-Type: multipart/mixed; charset=""
are the ones added by wp_mail(), thus causing Hotmail to ignore any multiparts due to the second Content-Type definition overriding the first.
Could we not just add a flag to wp_mail() that desists from adding any headers other than the ones supplied to it by the function call?
#27
@
12 years ago
- Keywords needs-refresh added; 3.3-early removed
- Milestone changed from Future Release to 3.6
- Owner changed from westi to nacin
Marking as needs-refresh for now, as it may need parts backported from #18493. I should get to this within a day, if not, yell in my direction. :)
#28
@
12 years ago
Attaching updated diff from core code revision 686217.
Having looked at #18493, I feel some of those changes (i.e. default to require a template file) may break plugins currently using the wp_mail() function so I haven't back ported anything from that ticket.
#31
@
10 years ago
Just to highlight that sending multipart emails (html/text) with wp_mail() (to reduce chance of emails ending up in spam folders) will ironically result with your domain being blocked by Hotmail (and other Microsoft emails).
We are currently discussing this severe issue over at WPSE:
This issue has been pending for 5 years and it hasn't appeared as if there is any interest to implement a fix to core. Maybe it is about that time to invest and resolve this properly.
The patch posted above 15448_June2015.diff works.
Consider implementing this solution.
#33
@
9 years ago
Refresh for 4.5 Bug Scrub (just removes "Hunk succeeded" messages). 15448_Sept2015-unittests.diff still good.
This ticket was mentioned in Slack in #core by gitlost. View the logs.
9 years ago
#35
@
9 years ago
Per https://wordpress.slack.com/archives/core/p1454696774001887, applied coding standards to patch.
#36
@
9 years ago
How close are we to getting the patch added to core? Is there anything I can do to help? We need to get this fixed. Multipart emails is a basic necessity for a lot of WordPress plugin development.
#37
@
9 years ago
Gents,
is there any update on that? It is really crucial...
There is already possible solution, why don't include it in a core?
http://wordpress.stackexchange.com/questions/191923/sending-multipart-text-html-emails-via-wp-mail-will-likely-get-your-domain-b
#38
@
9 years ago
Refresh for WP 4.7, with unit tests included in the one patch. Added extra test to make sure the workarounds using phpmailer_init
mentioned in WPSE post still work, around.
#39
follow-up:
↓ 40
@
8 years ago
Is there anything else needed on this? Why hasn't this been merged?
#40
in reply to:
↑ 39
@
8 years ago
Replying to JeffMatson:
Is there anything else needed on this? Why hasn't this been merged?
Maybe some testing and for a committer to pick it up.
This ticket was mentioned in Slack in #core by jeffmatson. View the logs.
8 years ago
#44
in reply to:
↑ description
@
8 years ago
When trying to send emails via
wp_mail()
with a Content-Type of multipart/alternative, the Content-Type header will be set with$phpmailer->ContentType
, and again with$phpmailer->AddCustomHeader()
, which causes two Content-Type headers in the email:
Content-Type: multipart/alternative; boundary="example_boundary" Content-Type: multipart/alternative; charset=""
As far as I can tell, the original issue for this ticket, being that two Content-Type
headers would be added, was fixed in a PHPMailer update some time ago - I can't find when, although I'll note PHPMailer 5.2.10 added the This is a multi-part message in MIME format.
filler line which would've fixed the final Outlook-related parse failures.
I'll note that there's zero code examples posted on the ticket other than a unit test using MockMailer - without example code it's incredibly hard to actually verify if an issue still exists, and if so, if the proposed patches actually fix it.
Using the following code:
$html_message = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body><p>Hello World!</p></body></html>'; add_action( 'phpmailer_init', $alt_function = function( $mailer ) { $mailer->AltBody = strip_tags( $mailer->Body ); } ); wp_mail( $to, $subject, $html_message ); remove_action( 'phpmailer_init', $alt_function );
will result in the following email:
From: WordPress <wordpress@localhost.localdomain> Message-ID: <8bd7f4a25da2084a2a803d40a4c823e2@centos> X-Mailer: PHPMailer 5.2.22 (https://github.com/PHPMailer/PHPMailer) MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="b1_8bd7f4a25da2084a2a803d40a4c823e2" Content-Transfer-Encoding: 8bit This is a multi-part message in MIME format. --b1_8bd7f4a25da2084a2a803d40a4c823e2 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hello World! --b1_8bd7f4a25da2084a2a803d40a4c823e2 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 8bit <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body><p>Hello World!</p></body></html> --b1_8bd7f4a25da2084a2a803d40a4c823e2--
That being said, I don't mind the approaches taken here - extending the $message
parameter of wp_mail()
to accept an array of mime typed contents. However, with the above in light that I can't duplicate the original issue, I'm curious if the additional code in 15448_Sep2016.diff is actually needed anymore.
The interactions between this and #18493 could end up weird - however, I expect that this use-case would just override any future HTML email template functionality core added.
#45
follow-up:
↓ 46
@
8 years ago
Proof of concept:
function mail_check() {
$to = "recipient@example.com";
$subject = 'wp_mail testing multipart';
$message = 'Hello world! This is plain text...';
$headers = "MIME-Version: 1.0\r\n";
$headers .= "From: sender@example.com\r\n";
$headers .= 'Content-Type: multipart/alternative;boundary="----=_Part_18243133_1346573420.1408991447668"';
// send email
wp_mail( $to, $subject, $message, $headers );
}
add_action( 'shutdown', 'mail_check' );
Produces the following in WordPress 4.7.2:
From: WordPress <wordpress@localhost.localdomain> Message-ID: <183f1675f0731e7c2d3b822eb0370c1f> X-Mailer: PHPMailer 5.2.22 (https://github.com/PHPMailer/PHPMailer) MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----=_Part_18243133_1346573420.1408991447668" MIME-Version: 1.0 Content-Type: multipart/alternative; charset= Hello world! This is plain text...
#46
in reply to:
↑ 45
@
8 years ago
Replying to MattyRob:
Proof of concept:
Thanks @MattyRob, with that I was able to duplicate the scenario. I don't actually think it's a valid use of PHPMailer myself, but respect that people use all sorts of hacks to send email..
#47
@
8 years ago
@dd32
I agree - my PoC is very sub-optimal. Patching the wp_mail() function to handle multipart emails directly should make developers lives easier, account for sub-optimal use of wp_mail() and perhaps increase deliverability too.
Any chance of getting this in the 4.7.x branch at some point?
#48
@
8 years ago
Any chance of getting this in the 4.7.x branch at some point?
A bugfix to remove the duplicate headers - likely, although, I have a feeling that it should be fixed in PHPMailer instead of in WordPress (Based on the code I've read in PHPMailer, it looks like it's detecting the email type wrong - and it is, which is why the duplicate header is added).
Accepting text/html
+ text/plain
as args in wp_mail()
will probably be a 4.8 thing, just based on needing to have a much longer testing and bug-finding period being needed.
This ticket was mentioned in Slack in #core by jeffpaul. View the logs.
8 years ago
#50
@
8 years ago
- Milestone changed from 4.8 to 4.8.1
Per yesterday's bug scrub, we're going to punt this to 4.8.1.
This ticket was mentioned in Slack in #core by jeffpaul. View the logs.
8 years ago
This ticket was mentioned in Slack in #core by jeffpaul. View the logs.
8 years ago
#53
@
8 years ago
- Milestone changed from 4.8.1 to 4.9
Per today's bug scrub, we'll punt this as the focus for 4.8.1 is regressions only.
This ticket was mentioned in Slack in #core by jeffpaul. View the logs.
8 years ago
#55
@
8 years ago
- Milestone changed from 4.9 to Future Release
Punting to Future Release per today's 4.9 bug scrub.
This ticket was mentioned in Slack in #core by mattyrob. View the logs.
7 years ago
#60
@
6 years ago
There is so many bugs with patches that are not released/making it into core...
It's outrageous...
Just keep working on gutenberg and let the rest of the code rot.
Good job the core team. I'm switching to laravel.
#62
@
6 years ago
- Milestone changed from Future Release to 5.3
- Owner changed from nacin to SergeyBiryukov
#63
@
6 years ago
@SergeyBiryukov Can this be included in Beta 1 for version 5.3 today? If not, let's move this to 5.4.
#64
@
6 years ago
- Milestone changed from 5.3 to Future Release
With version 5.3 Beta 1 releasing shortly, the deadline for enhancements is now passed. This is being moved to Future Release
. @SergeyBiryukov if you feel this can be included in 5.4, feel free to move up the milestone.
#65
in reply to:
↑ description
@
5 years ago
What can we do to have this added in a future release?
The patches suggested are working for me and I've spent lots of hours until I found this ticket that actually the content-type: multipart/alternative is broken in wp_mail.
Thanks!
I.
This ticket was mentioned in PR #9063 on WordPress/wordpress-develop by @SirLouen.
2 weeks ago
#68
- Keywords has-unit-tests added
This is a refresh of 15448_Sep2019.11.diff
Creating this PR to pass the GH CI protocol
Check for further testing after this
Trac ticket: https://core.trac.wordpress.org/ticket/15448
#69
@
2 weeks ago
- Keywords has-test-info added; gci westi-likes needs-testing removed
- Type changed from enhancement to feature request
Combined Reproduction and Patch Test Report
Description
🟠 This report can't validate that the issue can be fully reproduced,
Patch tested: https://github.com/WordPress/wordpress-develop/pull/9063.diff
Environment
- WordPress: 6.9-alpha-60093-src
- PHP: 8.2.28
- Server: nginx/1.27.5
- Database: mysqli (Server: 8.4.5 / Client: mysqlnd 8.2.28)
- Browser: Chrome 137.0.0.0
- OS: Windows 10/11
- Theme: Twenty Twenty-One 2.5
- MU Plugins: None activated
- Plugins:
- Multipart Email Sender 1.0.0
- Test Reports 1.2.0
- WP_Mail Setup 1.0.0
Reproduction Setup
- Setup the email server, in my case, my PR 8555 with
wordpress-develop
and Mailhog - Setup in WP the connection with PHPMailer to the local server
- Add the code in supplemental artifacts, anywhere where it can be executed (including wp-load.php and executing such function)
- ❌ The email is received, but no trace of two
multipart
sections as reported - 🐞 The email is not multipart by default, this is the real problem.
Expected Results
- A multipart/alternative email, hence one side Plain Text and the other side, HTML
Actual Results with the Patch
- ✅ The patch works correctly.
Additional Comments
I've been playing around with the last patch with my refresh
Also reading through the whole post and stumbled into the WP Stack Exchange topic.
The thing is that using the provided code in the WP SE topic
Content-Type: multipart/alternative; charset= Date: Wed, 25 Jun 2025 20:13:36 +0000 From: Foo <foo@bar.com> MIME-Version: 1.0 Message-ID: <QisEmVtRxTD2dTUEwnWxR2FyFGEYjvJhaaykSKl0@localhost> Received: from localhost by mailhog.example (MailHog) id xLmoK23xx_Mp7ymSnM-PsnFPgWVR4czzoTFUatwetos=@mailhog.example; Wed, 25 Jun 2025 20:13:36 +0000 Return-Path: <foo@bar.com> Subject: wp_mail testing multipart To: YourEmail@hotmail.com X-Mailer: PHPMailer 6.9.3 (https://github.com/PHPMailer/PHPMailer) ------=_Part_18243133_1346573420.1408991447668 Content-Type: text/plain; charset=UTF-8 Hello world! This is plain text... ------=_Part_18243133_1346573420.1408991447668 Content-Type: text/html; charset=UTF-8 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <p>Hello World! This is HTML...</p> </body> </html> ------=_Part_18243133_1346573420.1408991447668--
This is the source of the sent email (and received mail in Mailhog)
Content-Type: multipart/alternative; charset= Date: Wed, 25 Jun 2025 20:13:36 +0000 From: Foo <foo@bar.com> MIME-Version: 1.0 Message-ID: <QisEmVtRxTD2dTUEwnWxR2FyFGEYjvJhaaykSKl0@localhost> Received: from localhost by mailhog.example (MailHog) id xLmoK23xx_Mp7ymSnM-PsnFPgWVR4czzoTFUatwetos=@mailhog.example; Wed, 25 Jun 2025 20:13:36 +0000 Return-Path: <foo@bar.com> Subject: wp_mail testing multipart To: YourEmail@hotmail.com X-Mailer: PHPMailer 6.9.3 (https://github.com/PHPMailer/PHPMailer) ------=_Part_18243133_1346573420.1408991447668 Content-Type: text/plain; charset=UTF-8 Hello world! This is plain text... ------=_Part_18243133_1346573420.1408991447668 Content-Type: text/html; charset=UTF-8 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <p>Hello World! This is HTML...</p> </body> </html> ------=_Part_18243133_1346573420.1408991447668--
True is, that there is no multiparted received mail as reported: it's just a regular plain text email.
On the other side I can't see that "double" multipart thing:
Content-Type: multipart/alternative; boundary="example_boundary" Content-Type: multipart/alternative; charset=""
That some reporters have been commenting in the early days, like @rmccue and @christinecooper
They were talking about Outlook, Hotmail, … I wonder if these clients added this section or something because I can't really see this in the Original content in Mailhog. Or maybe the latest versions of PHPMailer have sorted this out.
If any of the reporters could throw more light on this "double" multipart section, it would be great.
Furthermore, I can comment, that the workaround with $phpmailer->AltBody
is definitely working well before the patch.
needs-code-review
This is just a testing report. I've also reviewed the patch, and looks good. I've also fixed the Unit Tests to fit to the current PHPUnit formatting we are using.
But before taking a final conclusion, I need to dig a bit more into the behaviour of PHPMailer. Many users, including @dd32 and some in the post of Stack Exchange, have reported that PHPMailer is the culprit in this history.
Supplemental Artifacts
Test code
// Set $to to an hotmail.com or outlook.com email $to = "YourEmail@hotmail.com"; $subject = 'wp_mail testing multipart'; $message = '------=_Part_18243133_1346573420.1408991447668 Content-Type: text/plain; charset=UTF-8 Hello world! This is plain text... ------=_Part_18243133_1346573420.1408991447668 Content-Type: text/html; charset=UTF-8 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <p>Hello World! This is HTML...</p> </body> </html> ------=_Part_18243133_1346573420.1408991447668--'; $headers = "MIME-Version: 1.0\r\n"; $headers .= "From: Foo <foo@bar.com>\r\n"; $headers .= 'Content-Type: multipart/alternative;boundary="----=_Part_18243133_1346573420.1408991447668"'; // send email wp_mail( $to, $subject, $message, $headers );
#70
follow-ups:
↓ 71
↓ 72
@
2 weeks ago
True is, that there is no multiparted received mail as reported: it's just a regular plain text email.
It's actually a broken multipart email, missing the boundary in the header. Note the Content-Type in your code:
Content-Type: multipart/alternative; charset=
Meanwhile, the body contains the multiple parts; your boundary is ----=_Part_18243133_1346573420.1408991447668
.
This is the initial content-type header added by $phpmailer->ContentType
here: https://github.com/WordPress/wordpress-develop/blob/6ea383f14783604934af91c60fee9471a40974df/src/wp-includes/pluggable.php#L483
The correct header is attempted to be added within the headers check: https://github.com/WordPress/wordpress-develop/blob/6ea383f14783604934af91c60fee9471a40974df/src/wp-includes/pluggable.php#L517-L519
Note that due to the if ( ! empty( $headers ) )
guard around it, this will only be triggered if another header is added, and one which doesn't have special handling (from, content-type, cc, bcc, reply-to). The example code on the linked SE thread should do that via the MIME-Version header, but maybe it's not triggering properly. (You may also need to test with a "real" sendmail rather than MailHog.)
In any case, it's arguable whether this is a core bug or phpmailer (WP's the one setting the content-type incorrectly and adding an extra header), but either way the functionality is broken :)
If it's helpful, a simplified example might be:
$body = '--TestBoundary Content-Type: text/plain; charset=UTF-8 This is a test email body. --TestBoundary Content-Type: text/html; charset=UTF-8 <html><body>This is a test email body.</body></html> --TestBoundary-- '; $headers = [ 'Example-Custom: value', 'Content-Type: multipart/alternative; boundary="TestBoundary"', ]; wp_mail( 'test@example.com', 'Test', $body, $headers );
Note that this patch goes beyond just fixing that bug to actually introduce multipart handling as a core capability.
Aside: We're now so far down the track with this function as a pluggable one that retrofitting the ecosystem is going to be difficult. At the time I wrote the ticket, WP's market share was closer to 10% and there were many fewer drop-in replacements for wp_mail()
. At this point it might be worth considering a new wp_mail_multi()
instead.
#71
in reply to:
↑ 70
@
2 weeks ago
Replying to rmccue:
Note that due to the
if ( ! empty( $headers ) )
guard around it, this will only be triggered if another header is added, and one which doesn't have special handling (from, content-type, cc, bcc, reply-to). The example code on the linked SE thread should do that via the MIME-Version header, but maybe it's not triggering properly. (You may also need to test with a "real" sendmail rather than MailHog.)
My test report was my initial, very superfluous review.
I've tested again, with Mailhog and your example, and I can't still see the double multipart
. Maybe some more testing with sendmail
to some other clients (other than basic Mailhog catchall client) is required.
Note that this patch goes beyond just fixing that bug to actually introduce multipart handling as a core capability.
Aside: We're now so far down the track with this function as a pluggable one that retrofitting the ecosystem is going to be difficult. At the time I wrote the ticket, WP's market share was closer to 10% and there were many fewer drop-in replacements for
wp_mail()
. At this point it might be worth considering a newwp_mail_multi()
instead.
I've been doing further code review both in WP, PHPMailer, and the patch submitted, here are my conclusions:
There are two main functions in PHPMailer that build the multipart/alternative
SetMessageType and GetMailMIME
The first one decides if the message will be sent as a multipart/alternative
if an AltBody
exists. This was already covered in tests like [35617] and [54529]. So using phpmailer_init
to set $phpmailer
attributes has not been alien to WordPress in the past 10 years (although, it's true, that this post hast 15 years in total). So for some people who have been moaning because this is not fully supported have no full reason for this, it's completely possible to achieve this with a simple workaround. Obviously, a full support is always more comfortable, but using the workaround is not the end of the world.
For anyone coming up new, a quick patch recap
What the current patch is simply doing, is the following:
First, is taking advantage of the fact that wp_mail
already has access to $phpmailer
, and setting AltBody
accordingly for multipart/alt
or attachments
for mixed
or related
(which is set in PHPMailer)
Then, it checks if the content of the email is a string or an array. If it's a string (like the test code in my example above) or the code provided by @rmccue in the previous message, it will simply set it as text/plain and will probably not work (for now, check in the conclusion, maybe it could get fixed, but I believe this should be done in the PHPMailer side)
But the real interesting thing that this patch does, is adding the possibility of passing an array to wp_mail
$message
which adds a new level of comfortability because building multipart emails this way is very convenient.
The PHPMailer dilemma
If something could be sorted in PHPMailer, is the default switch
condition at PHPMailer seems to be the fallback reason of why a possible "double Content-Type" is appearing: The problem is that once set the Header, it will be sent as-is but Mailhog will add, again, another broken header, with the default
condition, which is the multipart, without the boundary.
This double-content type, could be sorted in PHPMailer, as @dd32 was suggesting for sure, if instead of using that default condition, it allowed some logic for headers completely defined by the user (considering that we are not passing the AltBody
by default, in the current code). But it seems that PHPMailer, for some reason as a default behaviour, is expecting, the AltBody to be present, if we want to send a multipart.
Final Conclusion
PHPMailer works perfectly for anything multipart. Proof of this is that the thing is working with phpmailer_init
action hook. So at this point, I won't fully blame PHPMailer considering we have a very valid solution in the table.
I have added a report upstream, to support the string multipart. But the current array implementation adds a extra degree of correctness. We could obviously, always parse the string content and set the AltBody accordingly, but I truly believe that this adds an extra layer of unnecesary complexity to the function. As @rmccue this could be content for a very specific wp_mail_multi
function, which I think is an overkill (specially if we can happen to find a solution upstream).
The thing, here, is considering whether this want to be supported natively or not with the array alternative. We have the patch, we have the testing (further testing will be ridden), we have multiple code reviews, and we have good unit tests and everything is passing. I can't really think of a reason on why not supporting this at this point for 6.9 with a nice dev-note
as a new feature. I will put this on my reviewing list and let's see if we can onboard it again, for the next Milestone.
#73
@
2 weeks ago
Patch Testing Instructions
- First follow the regular bug reproduction instructions
- After you have everything setup, and failing to create a multipart email with the regular string version, its time to add the patch
- Create an array version. The array version must provides the following:
- Instead of providing the
$message
as a single string it must provide the $message as an key-value array. - The keys should be the Content-Type parts of the multipart. For example,
text/plain
andtext/html
. You could also try to add an attachment if you want to do a great test - The value should be the content for each of the parts.
- The rest can be the same as the original example
If everything went right and patch is working as expected, you should see an HTML/Text multipart/alternative Email, or with attachments a multipart/mixed
One thought on how to achieve this was passing the body as an array instead of a string to
wp_mail()
, with keys being the Content-Type for each. Then, if only text/html and text/plain are set, useAltBody
for the text andBody
for the HTML. If anything else is set, we'll use PHPMailer's attachment system fully instead.Thoughts?