From 16feff8e8d8f202001c512e46e127cb3eaab5c3c Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Thu, 19 Oct 2023 16:54:31 +0200
Subject: [PATCH] Issue #3165762 by znerol, Berdir, jungle, AdamPS, longwave,
 smustgrave, imclean, catch, dpi: Add symfony/mailer into core

---
 composer.json                                 |   1 +
 composer.lock                                 |  82 ++++++++-
 .../Metapackage/DevDependencies/composer.json |   1 +
 .../PinnedDevDependencies/composer.json       |   1 +
 core/core.services.yml                        |   3 +
 .../Core/Mail/Plugin/Mail/SymfonyMailer.php   | 164 ++++++++++++++++++
 .../Core/Test/FunctionalTestSetupTrait.php    |   1 +
 .../MigrateUpgradeExecuteTestBase.php         |   4 +
 .../system/config/install/system.mail.yml     |   1 +
 .../system/config/schema/system.schema.yml    |   3 +
 .../system/migrations/d7_system_mail.yml      |   6 +
 core/modules/system/system.post_update.php    |   8 +
 .../Update/MailDsnSettingsUpdateTest.php      |  36 ++++
 .../d7/MigrateSystemConfigurationTest.php     |   1 +
 .../Installer/InstallerTestBase.php           |   1 +
 .../Drupal/KernelTests/KernelTestBase.php     |   1 +
 .../Tests/Core/Mail/MailManagerTest.php       |   1 +
 .../Core/Mail/Plugin/Mail/PhpMailTest.php     |   1 +
 .../Mail/Plugin/Mail/SymfonyMailerTest.php    | 141 +++++++++++++++
 19 files changed, 456 insertions(+), 1 deletion(-)
 create mode 100644 core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php
 create mode 100644 core/modules/system/tests/src/Functional/Update/MailDsnSettingsUpdateTest.php
 create mode 100644 core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/SymfonyMailerTest.php

diff --git a/composer.json b/composer.json
index e551ea3eafc2..3242b5e5fee5 100644
--- a/composer.json
+++ b/composer.json
@@ -41,6 +41,7 @@
         "symfony/filesystem": "^6.3",
         "symfony/finder": "^6.3",
         "symfony/lock": "^6.3",
+        "symfony/mailer": "^6.3",
         "symfony/phpunit-bridge": "^6.3",
         "symfony/var-dumper": "^6.3"
     },
diff --git a/composer.lock b/composer.lock
index 18e4389daf98..45f224e5e158 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "1f03bd567e9e6a240f6bec2e76b7f448",
+    "content-hash": "93842a6c87cec9f144cd50dab29818e7",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -9138,6 +9138,86 @@
             ],
             "time": "2023-04-21T12:19:45+00:00"
         },
+        {
+            "name": "symfony/mailer",
+            "version": "v6.3.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/mailer.git",
+                "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/mailer/zipball/d89611a7830d51b5e118bca38e390dea92f9ea06",
+                "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06",
+                "shasum": ""
+            },
+            "require": {
+                "egulias/email-validator": "^2.1.10|^3|^4",
+                "php": ">=8.1",
+                "psr/event-dispatcher": "^1",
+                "psr/log": "^1|^2|^3",
+                "symfony/event-dispatcher": "^5.4|^6.0",
+                "symfony/mime": "^6.2",
+                "symfony/service-contracts": "^2.5|^3"
+            },
+            "conflict": {
+                "symfony/http-client-contracts": "<2.5",
+                "symfony/http-kernel": "<5.4",
+                "symfony/messenger": "<6.2",
+                "symfony/mime": "<6.2",
+                "symfony/twig-bridge": "<6.2.1"
+            },
+            "require-dev": {
+                "symfony/console": "^5.4|^6.0",
+                "symfony/http-client": "^5.4|^6.0",
+                "symfony/messenger": "^6.2",
+                "symfony/twig-bridge": "^6.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Mailer\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Helps sending emails",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/mailer/tree/v6.3.5"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-09-06T09:47:15+00:00"
+        },
         {
             "name": "symfony/phpunit-bridge",
             "version": "v6.3.0",
diff --git a/composer/Metapackage/DevDependencies/composer.json b/composer/Metapackage/DevDependencies/composer.json
index 0dae0f6f6013..d67c501890ac 100644
--- a/composer/Metapackage/DevDependencies/composer.json
+++ b/composer/Metapackage/DevDependencies/composer.json
@@ -33,6 +33,7 @@
         "symfony/filesystem": "^6.3",
         "symfony/finder": "^6.3",
         "symfony/lock": "^6.3",
+        "symfony/mailer": "^6.3",
         "symfony/phpunit-bridge": "^6.3",
         "symfony/var-dumper": "^6.3"
     }
diff --git a/composer/Metapackage/PinnedDevDependencies/composer.json b/composer/Metapackage/PinnedDevDependencies/composer.json
index 7b3931e62c46..35e44726d472 100644
--- a/composer/Metapackage/PinnedDevDependencies/composer.json
+++ b/composer/Metapackage/PinnedDevDependencies/composer.json
@@ -86,6 +86,7 @@
         "symfony/filesystem": "v6.3.0",
         "symfony/finder": "v6.3.0",
         "symfony/lock": "v6.3.0",
+        "symfony/mailer": "v6.3.5",
         "symfony/phpunit-bridge": "v6.3.0",
         "symfony/polyfill-php82": "v1.27.0",
         "theseer/tokenizer": "1.2.1",
diff --git a/core/core.services.yml b/core/core.services.yml
index 0b093c466998..8e2439ed9e12 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -500,6 +500,9 @@ services:
   logger.channel.security:
     parent: logger.channel_base
     arguments: ['security']
+  logger.channel.mail:
+    parent: logger.channel_base
+    arguments: ['mail']
   logger.channel.menu:
     parent: logger.channel_base
     arguments: ['menu']
diff --git a/core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php b/core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php
new file mode 100644
index 000000000000..f99fb86a8f6d
--- /dev/null
+++ b/core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace Drupal\Core\Mail\Plugin\Mail;
+
+use Drupal\Component\Render\MarkupInterface;
+use Drupal\Core\Mail\MailFormatHelper;
+use Drupal\Core\Mail\MailInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Utility\Error;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Mailer\Mailer;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mailer\Transport;
+use Symfony\Component\Mime\Email;
+
+/**
+ * Defines an experimental mail backend, based on the Symfony mailer component.
+ *
+ * This mail plugin acts as a drop-in replacement for the current default PHP
+ * mail plugin. Mail delivery is based on the Symfony mailer component. Hence,
+ * all transports registered by default in the Symfony mailer transport factory
+ * are available via configurable DSN.
+ *
+ * By default, this plugin uses `sendmail://default` as the transport DSN. I.e.,
+ * it attempts to use `/usr/sbin/sendmail -bs` in order to submit a message to
+ * the MTA. Sites hosted on operating systems without a working MTA (e.g.,
+ * Windows) need to configure a suitable DSN.
+ *
+ * The DSN can be set via the `mailer_dsn` key of the `system.mailer` config.
+ *
+ * The following example shows how to switch the default mail plugin to the
+ * experimental Symfony mailer plugin with a custom DSN using config overrides
+ * in `settings.php`:
+ *
+ * @code
+ *   $config['system.mail']['interface'] = [ 'default' => 'symfony_mailer' ];
+ *   $config['system.mail']['mailer_dsn'] = 'smtp://user:pass@smtp.example.com:25';
+ * @endcode
+ *
+ * Note that special characters in the mailer_dsn need to be URL encoded.
+ *
+ * @see https://symfony.com/doc/current/mailer.html#using-built-in-transports
+ *
+ * @Mail(
+ *   id = "symfony_mailer",
+ *   label = @Translation("Symfony mailer (Experimental)"),
+ * )
+ *
+ * @internal
+ */
+class SymfonyMailer implements MailInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * A list of headers that can contain multiple email addresses.
+   *
+   * @see \Symfony\Component\Mime\Header\Headers::HEADER_CLASS_MAP
+   */
+  protected const MAILBOX_LIST_HEADERS = ['from', 'to', 'reply-to', 'cc', 'bcc'];
+
+  /**
+   * List of headers to skip copying from the message array.
+   *
+   * Symfony mailer sets Content-Type and Content-Transfer-Encoding according to
+   * the actual body content. Note that format=flowed is not supported by
+   * Symfony.
+   *
+   * @see \Symfony\Component\Mime\Part\TextPart
+   */
+  protected const SKIP_HEADERS = ['content-type', 'content-transfer-encoding'];
+
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $container->get('logger.channel.mail')
+    );
+  }
+
+  /**
+   * Symfony mailer constructor.
+   *
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger service.
+   * @param \Symfony\Component\Mailer\MailerInterface $mailer
+   *   The mailer service. Only specify an instance in unit tests, pass NULL in
+   *   production.
+   */
+  public function __construct(
+    protected LoggerInterface $logger,
+    protected ?MailerInterface $mailer = NULL) {
+  }
+
+  public function format(array $message) {
+    // Convert any HTML to plain-text.
+    foreach ($message['body'] as &$part) {
+      if ($part instanceof MarkupInterface) {
+        $part = MailFormatHelper::htmlToText($part);
+      }
+      else {
+        $part = MailFormatHelper::wrapMail($part);
+      }
+    }
+
+    // Join the body array into one string.
+    $message['body'] = implode("\n\n", $message['body']);
+
+    return $message;
+  }
+
+  public function mail(array $message) {
+    try {
+      $email = new Email();
+
+      $headers = $email->getHeaders();
+      foreach ($message['headers'] as $name => $value) {
+        if (!in_array(strtolower($name), self::SKIP_HEADERS, TRUE)) {
+          if (in_array(strtolower($name), self::MAILBOX_LIST_HEADERS, TRUE)) {
+            // Split values by comma, but ignore commas encapsulated in double
+            // quotes.
+            $value = str_getcsv($value, ',');
+          }
+          $headers->addHeader($name, $value);
+        }
+      }
+
+      $email
+        ->to($message['to'])
+        ->subject($message['subject'])
+        ->text($message['body']);
+
+      $mailer = $this->getMailer();
+      $mailer->send($email);
+      return TRUE;
+    }
+    catch (\Exception $e) {
+      Error::logException($this->logger, $e);
+      return FALSE;
+    }
+  }
+
+  /**
+   * Returns a minimalistic Symfony mailer service.
+   */
+  protected function getMailer(): MailerInterface {
+    if (!isset($this->mailer)) {
+      $dsn = \Drupal::config('system.mail')->get('mailer_dsn');
+
+      // Symfony Mailer and Transport classes both optionally depend on the
+      // event dispatcher. When provided, a MessageEvent is fired whenever an
+      // email is prepared before sending.
+      //
+      // The MessageEvent will likely play an important role in an upcoming mail
+      // API. However, emails handled by this plugin already were processed by
+      // hook_mail and hook_mail_alter. Firing the MessageEvent would leak those
+      // mails into the code path (i.e., event subscribers) of the new API.
+      // Therefore, this plugin deliberately refrains from injecting the event
+      // dispatcher.
+      $transport = Transport::fromDsn($dsn, logger: $this->logger);
+      $this->mailer = new Mailer($transport);
+    }
+
+    return $this->mailer;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
index c4014bf27bd1..9985fffde337 100644
--- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
+++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
@@ -327,6 +327,7 @@ protected function initConfig(ContainerInterface $container) {
     // some tests expect to be able to test mail system implementations.
     $config->getEditable('system.mail')
       ->set('interface.default', 'test_mail_collector')
+      ->set('mailer_dsn', 'null://null')
       ->save();
 
     // By default, verbosely display all errors and disable all production
diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php
index 3f1d0276a437..10d6305d24b3 100644
--- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php
+++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php
@@ -65,6 +65,10 @@ public function useTestMailCollector() {
       'value' => 'test_mail_collector',
       'required' => TRUE,
     ];
+    $settings['config']['system.mail']['mailer_dsn'] = (object) [
+      'value' => 'null://null',
+      'required' => TRUE,
+    ];
     $this->writeSettings($settings);
   }
 
diff --git a/core/modules/system/config/install/system.mail.yml b/core/modules/system/config/install/system.mail.yml
index 0ea09c3812e5..67c1e29f92a4 100644
--- a/core/modules/system/config/install/system.mail.yml
+++ b/core/modules/system/config/install/system.mail.yml
@@ -1,2 +1,3 @@
 interface:
   default: 'php_mail'
+mailer_dsn: "sendmail://default"
diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml
index c0da210a39a1..11301fb9f520 100644
--- a/core/modules/system/config/schema/system.schema.yml
+++ b/core/modules/system/config/schema/system.schema.yml
@@ -302,6 +302,9 @@ system.mail:
       sequence:
         type: string
         label: 'Interface'
+    mailer_dsn:
+      type: string
+      label: 'Symfony mailer transport DSN'
 
 system.theme.global:
   type: theme_settings
diff --git a/core/modules/system/migrations/d7_system_mail.yml b/core/modules/system/migrations/d7_system_mail.yml
index 5bb46f4941d4..d2038043e925 100644
--- a/core/modules/system/migrations/d7_system_mail.yml
+++ b/core/modules/system/migrations/d7_system_mail.yml
@@ -15,6 +15,12 @@ process:
     map:
       DefaultMailSystem: php_mail
       MailTestCase: test_mail_collector
+  'mailer_dsn':
+    plugin: static_map
+    source: 'mail_system/default-system'
+    map:
+      DefaultMailSystem: 'sendmail://default'
+      MailTestCase: 'null://null'
 destination:
   plugin: config
   config_name: system.mail
diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php
index 14fd7bfc01ba..bdd6916fa6f0 100644
--- a/core/modules/system/system.post_update.php
+++ b/core/modules/system/system.post_update.php
@@ -158,3 +158,11 @@ function system_post_update_set_blank_log_url_to_null() {
       ->save(TRUE);
   }
 }
+
+/**
+ * Add new default mail transport dsn.
+ */
+function system_post_update_mailer_dsn_settings() {
+  $config = \Drupal::configFactory()->getEditable('system.mail');
+  $config->set('mailer_dsn', 'sendmail://default')->save();
+}
diff --git a/core/modules/system/tests/src/Functional/Update/MailDsnSettingsUpdateTest.php b/core/modules/system/tests/src/Functional/Update/MailDsnSettingsUpdateTest.php
new file mode 100644
index 000000000000..42542cf46db8
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Update/MailDsnSettingsUpdateTest.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Update;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests creation of default mail transport dsn settings.
+ *
+ * @see system_post_update_mailer_dsn_settings()
+ *
+ * @group Update
+ */
+class MailDsnSettingsUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../fixtures/update/drupal-9.4.0.bare.standard.php.gz',
+    ];
+  }
+
+  /**
+   * Tests system_post_update_mailer_dsn_settings().
+   */
+  public function testSystemPostUpdateMailerDsnSettings() {
+    $this->runUpdates();
+
+    // Confirm that config was created.
+    $config = $this->config('system.mail');
+    $this->assertEquals('sendmail://default', $config->get('mailer_dsn'));
+  }
+
+}
diff --git a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php
index 0d3b2fafd053..6af738f5251e 100644
--- a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php
+++ b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php
@@ -59,6 +59,7 @@ class MigrateSystemConfigurationTest extends MigrateDrupal7TestBase {
       'interface' => [
         'default' => 'php_mail',
       ],
+      'mailer_dsn' => 'sendmail://default',
     ],
     'system.maintenance' => [
       // langcode is not handled by the migration.
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
index c750816d1b6c..8bd28ac0e966 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
@@ -197,6 +197,7 @@ protected function setUp(): void {
       $this->container->get('config.factory')
         ->getEditable('system.mail')
         ->set('interface.default', 'test_mail_collector')
+        ->set('mailer_dsn', 'null://null')
         ->save();
 
       $this->installDefaultThemeFromClassProperty($this->container);
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index 5544dc324708..9a0702b28c3c 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -430,6 +430,7 @@ protected function bootKernel() {
     // While this should be enforced via settings.php prior to installation,
     // some tests expect to be able to test mail system implementations.
     $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
+    $GLOBALS['config']['system.mail']['mailer_dsn'] = 'null://null';
 
     // Manually configure the default file scheme so that modules that use file
     // functions don't have to install system and its configuration.
diff --git a/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php b/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
index b87260d5673a..3c2004a7b614 100644
--- a/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
@@ -119,6 +119,7 @@ protected function setUpMailManager($interface = []) {
     $this->configFactory = $this->getConfigFactoryStub([
       'system.mail' => [
         'interface' => $interface,
+        'mailer_dsn' => 'null://null',
       ],
       'system.site' => [
         'mail' => 'test@example.com',
diff --git a/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php b/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php
index fb4006e6f93a..ec216092c7af 100644
--- a/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php
+++ b/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php
@@ -46,6 +46,7 @@ protected function setUp(): void {
     $this->configFactory = $this->getConfigFactoryStub([
       'system.mail' => [
         'interface' => [],
+        'mailer_dsn' => 'null://null',
       ],
       'system.site' => [
         'mail' => 'test@example.com',
diff --git a/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/SymfonyMailerTest.php b/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/SymfonyMailerTest.php
new file mode 100644
index 000000000000..99ec411a754f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/SymfonyMailerTest.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Drupal\Tests\Core\Mail\Plugin\Mail;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Mail\MailFormatHelper;
+use Drupal\Core\Mail\Plugin\Mail\SymfonyMailer;
+use Drupal\Tests\UnitTestCase;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mime\Email;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Mail\Plugin\Mail\SymfonyMailer
+ * @group Mail
+ */
+class SymfonyMailerTest extends UnitTestCase {
+
+  /**
+   * Tests that mixed plain text and html body is converted correctly.
+   *
+   * @covers ::format
+   */
+  public function testFormatResemblesHtml() {
+    // Populate global $base_path to avoid notices generated by
+    // MailFormatHelper::htmlToMailUrls()
+    global $base_path;
+    $original_base_path = $base_path;
+    $base_path = '/';
+
+    $variables = [
+      '@form-url' => 'https://www.example.com/contact',
+      '@sender-url' => 'https://www.example.com/user/123',
+      '@sender-name' => $this->randomString(),
+    ];
+
+    $plain = "In HTML, ampersand must be written as &amp;.\nI saw your house and <wow> it is great. There is too much to say about that beautiful building, it will never fit on one line of text.\nIf a<b and b<c then a<c.";
+    $template = "@sender-name (@sender-url) sent a message using the contact form at @form-url.";
+    $markup = new FormattableMarkup($template, $variables);
+
+    $message = [
+      'body' => [
+        $plain,
+        $markup,
+      ],
+    ];
+
+    /** @var \Symfony\Component\Mailer\MailerInterface|\PHPUnit\Framework\MockObject\MockObject */
+    $mailer = $this->getMockBuilder(MailerInterface::class)->getMock();
+
+    /** @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+    $logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
+
+    $plugin = new SymfonyMailer($logger, $mailer);
+    $message = $plugin->format($message);
+
+    $expect = MailFormatHelper::wrapMail($plain . "\n\n" . strtr($template, $variables) . "\n");
+    $this->assertEquals($expect, $message['body']);
+
+    $base_path = $original_base_path;
+  }
+
+  /**
+   * Tests sending a mail using a From address with a comma in it.
+   *
+   * @covers ::mail
+   */
+  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' => "test\r\nsubject",
+      'body' => '',
+      'headers' => [
+        'MIME-Version' => '1.0',
+        'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
+        'Content-Transfer-Encoding' => '8Bit',
+        'X-Mailer' => 'Drupal',
+        'From' => '"Foo, Bar, and Baz" <from@example.org>',
+        'Reply-to' => 'from@example.org',
+        'Return-Path' => 'from@example.org',
+      ],
+    ];
+
+    // Verify we use line endings consistent with the PHP mail() function, which
+    // changed with PHP 8. See:
+    // - https://www.drupal.org/node/3270647
+    // - https://bugs.php.net/bug.php?id=81158
+    $line_end = "\r\n";
+
+    /** @var \Symfony\Component\Mailer\MailerInterface|\PHPUnit\Framework\MockObject\MockObject */
+    $mailer = $this->getMockBuilder(MailerInterface::class)->getMock();
+    $mailer->expects($this->once())->method('send')
+      ->with(
+        $this->logicalAnd(
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->get('mime-version')->getBodyAsString() === '1.0'
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->has('content-type') === FALSE
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->has('content-transfer-encoding') === FALSE
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->get('x-mailer')->getBodyAsString() === 'Drupal'
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->get('from')->getBodyAsString() === '"Foo, Bar, and Baz" <from@example.org>'
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->get('reply-to')->getBodyAsString() === 'from@example.org'
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->get('to')->getBodyAsString() === 'to@example.org'
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getHeaders()->get('subject')->getBodyAsString() === "=?utf-8?Q?test?=$line_end =?utf-8?Q?subject?="
+          ),
+          $this->callback(fn (Email $email) =>
+            $email->getTextBody() === ''
+          )
+        )
+      );
+
+    /** @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+    $logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
+
+    $plugin = new SymfonyMailer($logger, $mailer);
+    $this->assertTrue($plugin->mail($message));
+  }
+
+}
-- 
GitLab