Problem/Motivation
In \Drupal\Core\Mail\Plugin\Mail\PhpMail::format() there is a conversion from HTML to text which occurs without checking if the input is an instance of MarkupInterface. This causes corruption of a mail that was in fact plain text and contains text that resembles HTML.
E.g.
Today I learnt Greek letters alphaβ they are hard to write.
In HTML, ampersand must be written as &.
I saw your house and <wow> it is great.
If a<b and b<c then a<c.
Steps to reproduce
Install a contrib module that allows sending of messages in plain text format. An example is simplenews. Type a message like the example above.
Proposed resolution
Only call htmlToText() if the input implements MarkupInterface.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
Release notes snippet
None.
Comments
Comment #2
mxr576Hm, I have uploaded a patch to the referenced https://www.drupal.org/project/drupal/issues/2223967 which I believe does exactly the same that you describe but I got a push back on it.
https://www.drupal.org/project/drupal/issues/2223967#comment-13602092
These are exactly the same patches.
Comment #3
adamps commentedThanks @mxr576 however that patch is not the same as this issue.
The patch alters MessageViewBuilder.php in the contact module. The issue summary proposes to alter
PhpMail::format().Comment #4
NitinLama commentedComment #6
NitinLama commentedComment #8
snehalgaikwad commentedComment #9
snehalgaikwad commentedComment #10
adamps commentedThanks. However it's more complex than that. The message body is an array, and each entry in the array is either MarkupInterface or a string. So something like this (not tested):
Comment #11
kishor_kolekar commentedplease review the patch.
Comment #13
kishor_kolekar commentedComment #14
adamps commentedThanks. Please see
MailInterface::format()for a very clear description of what this patch needs to do.The loop must go before the call to implode. Then you don't need this if line.
This needs a test before it can be committed.
Comment #15
snehalgaikwad commentedI have tried to change the process, converted the message first and then joined lines into single string. Please check and let me know if this way is correct. Also test cases are still need to add. If the approach is correct then we can move on to tests.Thank you.
Comment #16
adamps commentedThanks it is getting better each time. Please delete this line and the else branch.
Html::Escape($part);The comment says to do this if the output is HTML. However PHPMail always generates plain text output.
Comment #17
snehalgaikwad commentedThank you for suggestions! Updated patch as per #16.
Comment #18
kapilv commentedComment #19
adamps commentedCode looks good. I think you don't need this line now.
+use Drupal\Component\Utility\Html;Next step is to write tests - back to "Needs work" for that.
Comment #20
kapilv commentedRemoved "use Drupal\Component\Utility\Html" Class.
Comment #21
kapilv commentedRemoved "use Drupal\Component\Utility\Html" Class.
Comment #24
NitinLama commentedComment #25
NitinLama commentedUpdated patch as per #19 suggestion.
Comment #27
adamps commentedThe patch is good, but it needs a test. The test should fail with an "only tests" patch. The test can send the example text from the issue summary.
Add another case inside
mail_html_test_mail(), something like thisAdd another test in MailTest.php like this
Comment #28
vsujeetkumar commentedRe-roll patch created for 9.3.x.
Comment #29
vsujeetkumar commentedAdded tests, As advised in #27, Please have a look.
Comment #30
adamps commentedGreat thanks that's good progress. Here are my comments:
1. You added the new test to
testRenderedElementsUseAbsolutePaths()but it's not really related. I suggest to make a new test, maybe like this:2. In MailTest.php this code is not needed - we know that $message is a plain string.
3. I see that there is the same bug with the mail subject - please can we fix that here too? The fix is in
MailManager::doMail(). Instead of this codeDo this:
Then add a test for the subject also. You can take one of the four sentences out of $message and put it in the subject instead.
Comment #31
adamps commentedComment #32
ankithashettyUpdated patch in #29 addressing all the points made in #30, kindly review.
Thanks!
Comment #34
vsujeetkumar commentedFixed fail tests, Please have a look.
Comment #35
adamps commentedThanks to you both.
1.
No I don't think we can do that - the test is correct, so it means that the code is now broken. The problem is that the contact module is passing markup in something that isn't MarkupInterface. It looks like the problem is in
contact_mail(): the concatenation operator (.=) forces the result back to a string.In contact.module line 142 instead of this line:
do this:
2. In
testResembleHtml(), maybe better to take the sentence that's now in $subject out of $message so it's like this:Comment #36
vsujeetkumar commentedChanges has been done according to #35, Please have a look.
@AdamPS In contact.module I have done the changes on line# 161 in place of 142, Now the tests is pass.
Comment #37
adamps commentedThanks, looks good. In contact.module I think we need to change all three cases: lines 142, 151 and 161. You are right, only one is needed to pass the automatic tests, but they all have the same bug.
Then the next step is to upload an "Only tests" patch: it has the changes to tests and nothing else. The automatic tests should fail for this one of course.
Comment #38
vsujeetkumar commentedChanges has been done according to #37. Test only patch also added, Please have a look.
Comment #39
berdirI don't think the subject can ever be HTML? Why would we bother to check for that or expect it to be Markup? At best that seems like a side-effect of using t().
This was added in #2572597: Replace !placeholder with @placeholder in mail code
Comment #40
berdirI think the change does make sense, but it's also an edge case between a bugfix and a change. People that are sending plain text mails might currently not pass in a proper Markup object and this change means that suddenly, their HTML will be full of HTML tags.
We did already several steps towards encouraging Markup objects, but so far, it was only to be able to actually send HTML mails.
I expect this is going to break mails for quite some custom code out there :-/.
Comment #41
berdirThinking out loud, could we turn it around and instead somehow flag message bodies to explicitly be plain text?
Comment #43
adamps commentedThanks @Berdir
Importantly, there is all the code that realised it needed to pass in Markup to avoid corruption:-) For example see
simplenews_token_replace_subject(). Without that code, this patch would be definitely non-BC, and almost impossible for contrib modules to write code that works accurately with multiple core versions. A second category is anyone who callsToken::replace()which outputs HTML (although annoyingly not as a MarkupInterface, which is a separate bug, #2580723: Fix token system confusion, with new function Token::replacePlain()).Could be, but the interface documentation is strongly in favour of this patch:
hook_mail()body:The array may contain either strings or objects implementing \Drupal\Component\Render\MarkupInterface.
MailInterface::format():The message body is received as an array of lines that are either strings or objects implementing \Drupal\Component\Render MarkupInterface
Furthermore, the popular swiftmailer module contains code exactly like this patch without any obvious sign of complaint. So perhaps there won't be as much broken contrib code as you fear??
Finally, I don't see how we can ever fix this bug without getting the people who are passing markup but not as MarkupInterface to change their code.
Maybe, but it seems pretty tricky/messy. The body is an array and, at least in theory, each part could separately be markup or not. I guess the flag could be "please trust me and only call
htmlToText()if I passMarkupInterface". Then maybe we deprecate not passing the flag to give people a warning. And in D10 we deprecate the flag and everyone has to change their code again??I propose that we should try to get comments from more key people. How about if we set this to RTBC with some appropriate issue tags and see what people say?
Comment #44
adamps commentedI am struggling to imagine plasible contrib code that will be hit by this issue - can anyone give an example?
To hit the bug you need a string containing markup but not implementing
MarkupInterface. Generally core would not generate such a thing so the contrib code would have to make it - which suggest that the coder has some awareness that they are making such a thing. Then to see the bug, pass this string as the body of a mail message - expecting the markup to be converted to plain.Why would anyone create such a string containing markup only to pass it to a function that they want to remove the markup??
Comment #45
adamps commentedComment #46
adamps commentedComment #47
berdir> Could be, but the interface documentation is strongly in favour of this patch:
IMHO, that documentation just documents that it is allowed to use strings or Markup objects, not what that specifically means. We should probably extend that documentation at least and make it clear what will happen, although that's then in the end at least partially up to the implementation.
> Why would anyone create such a string containing markup only to pass it to a function that they want to remove the markup??
I guess I'm less talking about contrib but custom code. What I imagine is that some code renders something or does token replacement or whatever that results in some HTML and currently, that HTML would be removed and converted to a plaintext version if passed in as a string. This is much less of an issue with swiftmailer, because if people are using Swiftmailer then they likely want to send HTML mails anyway. It might be somewhat far-fetched, but it _is_ a behavior change.
I have pondered about it a bit whether I should leave it at RTBC or not, but going to set back to needs work to update the documentation and have a change record. The mail API documentation is notoriously bad about what it expects and supports exactly, we both have been plagued by that. Lets make this a small step toward improving it :) Beside that, I agree with your suggestion, to set it to RTBC and get feedback from a framework maintainer. I don't think the mail component/subsystem has a dedicated maintainer though.
Btw, your thoughts would be welcome in #1803948: [META] Adopt the symfony mailer component in case you're not already following that. On how to approach moving swiftmailer/symfony mailer component into core with the long-term goal of replacing our current awkward hook based mailing system.
Comment #48
adamps commentedThanks Berdir
OK here's a more specific statement, from the comment on
MailInterface::format(). Does this one seem any more convincing?The conversion process consists of the following steps:
- If the output is HTML then convert any input line that is a string using \Drupal\Component\Utility\Html\Html::Escape().
- If the output is plain text then convert any input line that is markup using \Drupal\Core\Mail\MailFormatHelper::htmlToText().
- Join the input lines into a single string.
- Wrap long lines using \Drupal\Core\Mail\MailFormatHelper::wrapMail().
I believe the same problem exists if using swiftmailer. If some code creates markup in a plain string and passes it to swiftmailer then their mail will be corrupted. If it is an HTML mail, then attributes will be double-escaped. If it is a plain text mail, then markup will appear in the mail.
Makes sense, will do.
I guess if #3165762: Add symfony/mailer into core seems like the direction and the key people are not comfortable with this patch then we can just ignore the bug in this issue and ensure that the replacement system doesn't have the same bug.
Comment #49
adamps commentedChange record done - back to RTBC as per #47.
Comment #59
sanduhrsThere's no subsystem maintainer for mail listed in MAINTAINERS.txt.
Comment #60
larowlanI'd like to see a reply here from @Berdir to indicate he agrees with the reply in #47
Comment #63
catchI kind of think we should do that, there's not really an easy way to communicate the change of behaviour here, whereas with a brand new API we could fix the bug as a side-effect and people will consciously need to switch over and (hopefully) check output then. Going to tentatively mark this postponed on that issue.
Comment #66
kim.pepper#3165762: Add symfony/mailer into core is in so setting back to active. Might be closed now?
Comment #67
adamps commentedI think yes, we should close it. The bug is fixed in the new Symfony Mailer @Mail plug-in. PhpMail still contains the bug, but I don't think it makes sense to fix that - it creates BC problems and the code will be removed when Symfony Mailer is adopted.
Comment #68
adamps commented