Unverified Commit 01a729ab authored by alexpott's avatar alexpott

Issue #2936032 by gambry, nlisgo, alexpott, cwells, cilefen, Darvanen,...

Issue #2936032 by gambry, nlisgo, alexpott, cwells, cilefen, Darvanen, DamienGR: Sites named with special characters cannot send mail

(cherry picked from commit 53c24728)
parent 12b8136c
<?php
namespace Drupal\Component\Utility;
/**
* Provides helpers to ensure emails are compliant with RFCs.
*
* @ingroup utility
*/
class Mail {
/**
* RFC-2822 "specials" characters.
*/
const RFC_2822_SPECIALS = '()<>[]:;@\,."';
/**
* Return a RFC-2822 compliant "display-name" component.
*
* The "display-name" component is used in mail header "Originator" fields
* (From, Sender, Reply-to) to give a human-friendly description of the
* address, i.e. From: My Display Name <xyz@example.org>. RFC-822 and
* RFC-2822 define its syntax and rules. This method gets as input a string
* to be used as "display-name" and formats it to be RFC compliant.
*
* @param string $string
* A string to be used as "display-name".
*
* @return string
* A RFC compliant version of the string, ready to be used as
* "display-name" in mail originator header fields.
*/
public static function formatDisplayName($string) {
// Make sure we don't process html-encoded characters. They may create
// unneeded trouble if left encoded, besides they will be correctly
// processed if decoded.
$string = Html::decodeEntities($string);
// If string contains non-ASCII characters it must be (short) encoded
// according to RFC-2047. The output of a "B" (Base64) encoded-word is
// always safe to be used as display-name.
$safe_display_name = Unicode::mimeHeaderEncode($string, TRUE);
// Encoded-words are always safe to be used as display-name because don't
// contain any RFC 2822 "specials" characters. However
// Unicode::mimeHeaderEncode() encodes a string only if it contains any
// non-ASCII characters, and leaves its value untouched (un-encoded) if
// ASCII only. For this reason in order to produce a valid display-name we
// still need to make sure there are no "specials" characters left.
if (preg_match('/[' . preg_quote(Mail::RFC_2822_SPECIALS) . ']/', $safe_display_name)) {
// If string is already quoted, it may or may not be escaped properly, so
// don't trust it and reset.
if (preg_match('/^"(.+)"$/', $safe_display_name, $matches)) {
$safe_display_name = str_replace(['\\\\', '\\"'], ['\\', '"'], $matches[1]);
}
// Transform the string in a RFC-2822 "quoted-string" by wrapping it in
// double-quotes. Also make sure '"' and '\' occurrences are escaped.
$safe_display_name = '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $safe_display_name) . '"';
}
return $safe_display_name;
}
}
......@@ -5,7 +5,7 @@
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\Mail as MailHelper;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
......@@ -254,12 +254,8 @@ public function doMail($module, $key, $to, $langcode, $params = [], $reply = NUL
// Return-Path headers should have a domain authorized to use the
// originating SMTP server.
$headers['Sender'] = $headers['Return-Path'] = $site_mail;
// Headers are usually encoded in the mail plugin that implements
// \Drupal\Core\Mail\MailInterface::mail(), for example,
// \Drupal\Core\Mail\Plugin\Mail\PhpMail::mail(). The site name must be
// encoded here to prevent mail plugins from encoding the email address,
// which would break the header.
$headers['From'] = Unicode::mimeHeaderEncode($site_config->get('name'), TRUE) . ' <' . $site_mail . '>';
// Make sure the site-name is a RFC-2822 compliant 'display-name'.
$headers['From'] = MailHelper::formatDisplayName($site_config->get('name')) . ' <' . $site_mail . '>';
if ($reply) {
$headers['Reply-to'] = $reply;
}
......
......@@ -107,6 +107,32 @@ public function testFromAndReplyToHeader() {
$this->assertEquals('Drépal this is a very long test sentence to te <simpletest@example.com>', Unicode::mimeHeaderDecode($sent_message['headers']['From']), 'From header is correctly encoded.');
$this->assertFalse(isset($sent_message['headers']['Reply-to']), 'Message reply-to is not set if not specified.');
$this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
// Test RFC-2822 rules are respected for 'display-name' component of
// 'From:' header. Specials characters are not allowed, so randomly add one
// of them to the site name and check the string is wrapped in quotes. Also
// hardcode some double-quotes and backslash to validate these are escaped
// properly too.
$specials = '()<>[]:;@\,."';
$site_name = 'Drupal' . $specials[rand(0, strlen($specials) - 1)] . ' "si\te"';
$this->config('system.site')->set('name', $site_name)->save();
// Send an email and check that the From-header contains the site name
// within double-quotes. Also make sure double-quotes and "\" are escaped.
\Drupal::service('plugin.manager.mail')->mail('simpletest', 'from_test', 'from_test@example.com', $language);
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
$escaped_site_name = str_replace(['\\', '"'], ['\\\\', '\\"'], $site_name);
$this->assertEquals('"' . $escaped_site_name . '" <simpletest@example.com>', $sent_message['headers']['From'], 'From header is correctly quoted.');
// Make sure display-name is not quoted nor escaped if part on an encoding.
$site_name = 'Drépal, "si\te"';
$this->config('system.site')->set('name', $site_name)->save();
// Send an email and check that the From-header contains the site name.
\Drupal::service('plugin.manager.mail')->mail('simpletest', 'from_test', 'from_test@example.com', $language);
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
$this->assertEquals('=?UTF-8?B?RHLDqXBhbCwgInNpXHRlIg==?= <simpletest@example.com>', $sent_message['headers']['From'], 'From header is correctly encoded.');
$this->assertEquals($site_name . ' <simpletest@example.com>', Unicode::mimeHeaderDecode($sent_message['headers']['From']), 'From header is correctly encoded.');
}
/**
......
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Mail;
use PHPUnit\Framework\TestCase;
/**
* Test mail helpers implemented in Mail component.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Mail
*/
class MailTest extends TestCase {
/**
* Tests RFC-2822 'display-name' formatter.
*
* @dataProvider providerTestDisplayName
* @covers ::formatDisplayName
*/
public function testFormatDisplayName($string, $safe_display_name) {
$this->assertEquals($safe_display_name, Mail::formatDisplayName($string));
}
/**
* Data provider for testFormatDisplayName().
*
* @see testFormatDisplayName()
*
* @return array
* An array containing a string and its 'display-name' safe value.
*/
public function providerTestDisplayName() {
return [
// Simple ASCII characters.
['Test site', 'Test site'],
// ASCII with html entity.
['Test &amp; site', 'Test & site'],
// Non-ASCII characters.
['Tést site', '=?UTF-8?B?VMOpc3Qgc2l0ZQ==?='],
// Non-ASCII with special characters.
['Tést; site', '=?UTF-8?B?VMOpc3Q7IHNpdGU=?='],
// Non-ASCII with html entity.
['T&eacute;st; site', '=?UTF-8?B?VMOpc3Q7IHNpdGU=?='],
// ASCII with special characters.
['Test; site', '"Test; site"'],
// ASCII with special characters as html entity.
['Test &lt; site', '"Test < site"'],
// ASCII with special characters and '\'.
['Test; \ "site"', '"Test; \\\\ \"site\""'],
// String already RFC-2822 compliant.
['"Test; site"', '"Test; site"'],
// String already RFC-2822 compliant.
['"Test; \\\\ \"site\""', '"Test; \\\\ \"site\""'],
];
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment