From 831f7992f683669cbdd4d017a8f2cc8d39915606 Mon Sep 17 00:00:00 2001 From: Lee Rowlands <lee.rowlands@previousnext.com.au> Date: Mon, 20 Feb 2023 10:49:24 +1000 Subject: [PATCH] Issue #3226117 by MegaChriz, marthinal, longwave, rschwab, sinn, dcam, cilefen, ankithashetty, itaran, larowlan, catch: [Needs backport] Uncaught RfcComplianceException when email From name contains a comma --- .../Drupal/Core/Mail/Plugin/Mail/PhpMail.php | 45 +++++++-- .../system/tests/src/Kernel/Mail/MailTest.php | 21 ++-- .../tests/src/Kernel/UserMailNotifyTest.php | 2 + .../Core/Action/EmailActionTest.php | 2 + .../Core/Mail/Plugin/Mail/PhpMailTest.php | 96 +++++++++++++++++++ 5 files changed, 151 insertions(+), 15 deletions(-) create mode 100644 core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php diff --git a/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php b/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php index 138257b8da48..cae954c49e00 100644 --- a/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php +++ b/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php @@ -87,7 +87,9 @@ public function mail(array $message) { $headers = new Headers(); foreach ($message['headers'] as $name => $value) { if (in_array(strtolower($name), self::MAILBOX_LIST_HEADERS, TRUE)) { - $value = explode(',', $value); + // Split values by comma, but ignore commas encapsulated in double + // quotes. + $value = str_getcsv($value, ','); } $headers->addHeader($name, $value); } @@ -104,12 +106,7 @@ public function mail(array $message) { $mail_headers = str_replace("\r\n", "\n", $headers->toString()); $mail_subject = str_replace("\r\n", "\n", $mail_subject); - $request = \Drupal::request(); - - // We suppress warnings and notices from mail() because of issues on some - // hosts. The return value of this method will still indicate whether mail - // was sent successfully. - if (!$request->server->has('WINDIR') && strpos($request->server->get('SERVER_SOFTWARE'), 'Win32') === FALSE) { + if (substr(PHP_OS, 0, 3) != 'WIN') { // On most non-Windows systems, the "-f" option to the sendmail command // is used to set the Return-Path. There is no space between -f and // the value of the return path. @@ -117,7 +114,7 @@ public function mail(array $message) { // we assume to be safe. $site_mail = $this->configFactory->get('system.site')->get('mail'); $additional_headers = isset($message['Return-Path']) && ($site_mail === $message['Return-Path'] || static::_isShellSafe($message['Return-Path'])) ? '-f' . $message['Return-Path'] : ''; - $mail_result = @mail( + $mail_result = $this->doMail( $message['to'], $mail_subject, $mail_body, @@ -130,7 +127,7 @@ public function mail(array $message) { // Return-Path header. $old_from = ini_get('sendmail_from'); ini_set('sendmail_from', $message['Return-Path']); - $mail_result = @mail( + $mail_result = $this->doMail( $message['to'], $mail_subject, $mail_body, @@ -142,6 +139,36 @@ public function mail(array $message) { return $mail_result; } + /** + * Wrapper around PHP's mail() function. + * + * We suppress warnings and notices from mail() because of issues on some + * hosts. The return value of this method will still indicate whether mail was + * sent successfully. + * + * @param string $to + * Receiver, or receivers of the mail. + * @param string $subject + * Subject of the email to be sent. + * @param string $message + * Message to be sent. + * @param array $additional_headers + * (optional) Array to be inserted at the end of the email header. + * @param string $additional_params + * (optional) Can be used to pass additional flags as command line options. + * + * @see mail() + */ + protected function doMail(string $to, string $subject, string $message, $additional_headers = [], string $additional_params = ''): bool { + return @mail( + $to, + $subject, + $message, + $additional_headers, + $additional_params + ); + } + /** * Disallows potentially unsafe shell characters. * diff --git a/core/modules/system/tests/src/Kernel/Mail/MailTest.php b/core/modules/system/tests/src/Kernel/Mail/MailTest.php index 1eed7c500777..76167feaa394 100644 --- a/core/modules/system/tests/src/Kernel/Mail/MailTest.php +++ b/core/modules/system/tests/src/Kernel/Mail/MailTest.php @@ -40,6 +40,12 @@ protected function setUp(): void { parent::setUp(); $this->installEntitySchema('user'); $this->installEntitySchema('file'); + + // Set required site configuration. + $this->config('system.site') + ->set('mail', 'mailtest@example.com') + ->set('name', 'Drupal') + ->save(); } /** @@ -115,12 +121,6 @@ public function testCancelMessage() { public function testFromAndReplyToHeader() { $language = \Drupal::languageManager()->getCurrentLanguage(); - // Set required site configuration. - $this->config('system.site') - ->set('mail', 'mailtest@example.com') - ->set('name', 'Drupal') - ->save(); - // Reset the state variable that holds sent messages. \Drupal::state()->set('system.test_mail_collector', []); // Send an email with a reply-to address specified. @@ -153,6 +153,15 @@ public function testFromAndReplyToHeader() { // Errors-to header must not be set, it is deprecated. $this->assertFalse(isset($sent_message['headers']['Errors-To'])); + // Test that From names containing commas work as expected. + $this->config('system.site')->set('name', 'Foo, Bar, and Baz')->save(); + // Send an email and check that the From-header contains the site name. + \Drupal::service('plugin.manager.mail')->mail('mail_cancel_test', 'from_test', 'from_test@example.com', $language); + $captured_emails = \Drupal::state()->get('system.test_mail_collector'); + $sent_message = end($captured_emails); + // From header contains the quoted site name with commas. + $this->assertEquals('"Foo, Bar, and Baz" <mailtest@example.com>', $sent_message['headers']['From']); + // 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 diff --git a/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php b/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php index 2038d18c0ec0..a8ac6824847b 100644 --- a/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php +++ b/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php @@ -82,6 +82,7 @@ public function userMailsProvider() { */ public function testUserMailsSent($op, array $mail_keys) { $this->installConfig('user'); + $this->config('system.site')->set('mail', 'test@example.com')->save(); $this->config('user.settings')->set('notify.' . $op, TRUE)->save(); $return = _user_mail_notify($op, $this->createUser()); $this->assertTrue($return); @@ -173,6 +174,7 @@ public function testUserRecoveryMailLanguage() { // Recovery email should respect user preferred langcode by default if // langcode not set. + $this->config('system.site')->set('mail', 'test@example.com')->save(); $params['account'] = $user; $default_email = \Drupal::service('plugin.manager.mail')->mail('user', 'password_reset', $user->getEmail(), $preferredLangcode, $params); $this->assertTrue($default_email['result']); diff --git a/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php b/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php index 81d8aad8956e..f71fcd34a7fc 100644 --- a/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php @@ -31,6 +31,8 @@ protected function setUp(): void { * Tests the email action plugin. */ public function testEmailAction() { + $this->config('system.site')->set('mail', 'test@example.com')->save(); + /** @var \Drupal\Core\Action\ActionManager $plugin_manager */ $plugin_manager = $this->container->get('plugin.manager.action'); $configuration = [ diff --git a/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php b/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php new file mode 100644 index 000000000000..bdf97280cc0e --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php @@ -0,0 +1,96 @@ +<?php + +namespace Drupal\Tests\Core\Mail\Plugin\Mail; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Mail\Plugin\Mail\PhpMail; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Mail\Plugin\Mail\PhpMail + * @group Mail + */ +class PhpMailTest extends UnitTestCase { + + /** + * The configuration factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit\Framework\MockObject\MockObject + */ + protected $configFactory; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Use the provided config for system.mail.interface settings. + $this->configFactory = $this->getConfigFactoryStub([ + 'system.mail' => [ + 'interface' => [], + ], + 'system.site' => [ + 'mail' => 'test@example.com', + ], + ]); + + $container = new ContainerBuilder(); + $container->set('config.factory', $this->configFactory); + \Drupal::setContainer($container); + } + + /** + * Creates a mocked PhpMail object. + * + * The method "doMail()" gets overridden to avoid a mail() call in tests. + * + * @return \Drupal\Core\Mail\Plugin\Mail\PhpMail|\PHPUnit\Framework\MockObject\MockObject + * A PhpMail instance. + */ + protected function createPhpMailInstance(): PhpMail { + $mailer = $this->getMockBuilder(PhpMail::class) + ->onlyMethods(['doMail']) + ->getMock(); + + $mailer->expects($this->once())->method('doMail') + ->willReturn(TRUE); + + return $mailer; + } + + /** + * Tests sending a mail using a From address with a comma in it. + * + * @covers ::testMail + */ + public function testMail() { + // Setup a mail message. + $message = [ + 'id' => 'example_key', + 'module' => 'example', + 'key' => 'key', + 'to' => 'to@example.org', + 'from' => 'from@example.org', + 'reply-to' => 'from@example.org', + 'langcode' => 'en', + 'params' => [], + 'send' => TRUE, + 'subject' => '', + 'body' => '', + 'headers' => [ + 'MIME-Version' => '1.0', + 'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes', + 'Content-Transfer-Encoding' => '8Bit', + 'X-Mailer' => 'Drupal', + 'Return-Path' => 'from@example.org', + 'From' => '"Foo, Bar, and Baz" <from@example.org>', + 'Reply-to' => 'from@example.org', + ], + ]; + + $mailer = $this->createPhpMailInstance(); + $this->assertTrue($mailer->mail($message)); + } + +} -- GitLab