]> source.dussan.org Git - nextcloud-server.git/commitdiff
Migrate to Symfony Mailer
authorCôme Chilliet <come.chilliet@nextcloud.com>
Mon, 20 Dec 2021 10:35:06 +0000 (11:35 +0100)
committerArthur Schiwon <blizzz@arthur-schiwon.de>
Thu, 2 Feb 2023 09:30:06 +0000 (10:30 +0100)
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
14 files changed:
3rdparty
apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
apps/settings/lib/Controller/MailSettingsController.php
apps/settings/lib/Settings/Admin/Mail.php
apps/settings/templates/settings/admin/additional-mail.php
apps/settings/tests/Controller/MailSettingsControllerTest.php
apps/settings/tests/Settings/Admin/MailTest.php
config/config.sample.php
lib/private/Mail/Attachment.php
lib/private/Mail/Mailer.php
lib/private/Mail/Message.php
lib/public/Mail/IMailer.php
tests/lib/Mail/MailerTest.php
tests/lib/Mail/MessageTest.php

index 1d53ed4d3282427854fca7ee6ecbb945304272db..b31aba0505a3daf84b016f52873794b618694dff 160000 (submodule)
--- a/3rdparty
+++ b/3rdparty
@@ -1 +1 @@
-Subproject commit 1d53ed4d3282427854fca7ee6ecbb945304272db
+Subproject commit b31aba0505a3daf84b016f52873794b618694dff
index be238ae5afbdaefeeb4daf6b0b3b0c6b1a57166a..50390549570dcea05593f7b77b70d3f89facd228 100644 (file)
@@ -172,9 +172,14 @@ class IMipPlugin extends SabreIMipPlugin {
                        return;
                }
 
-               $senderName = $iTipMessage->senderName ?: null;
                $recipientName = $iTipMessage->recipientName ?: null;
 
+               /** @var Parameter|string|null $senderName */
+               $senderName = $iTipMessage->senderName ?: null;
+               if($senderName instanceof Parameter) {
+                       $senderName = $senderName->getValue() ?? null;
+               }
+
                if ($senderName === null || empty(trim($senderName))) {
                        $senderName = $this->userManager->getDisplayName($this->userId);
                }
index 22c0622a0722d4775c6cd4b3aa001776277ae303..2df79b67731331529ffa11738ca389a7681ab8b3 100644 (file)
@@ -85,7 +85,6 @@ class MailSettingsController extends Controller {
         * @param string $mail_smtpmode
         * @param string $mail_smtpsecure
         * @param string $mail_smtphost
-        * @param string $mail_smtpauthtype
         * @param int $mail_smtpauth
         * @param string $mail_smtpport
         * @return DataResponse
@@ -95,7 +94,6 @@ class MailSettingsController extends Controller {
                                                                        $mail_smtpmode,
                                                                        $mail_smtpsecure,
                                                                        $mail_smtphost,
-                                                                       $mail_smtpauthtype,
                                                                        $mail_smtpauth,
                                                                        $mail_smtpport,
                                                                        $mail_sendmailmode) {
index f4c546b27a42f4c32ebd6050faf709e780627ce6..1cdb7315713c6f1a388e4339929df872fe343ef6 100644 (file)
@@ -61,7 +61,6 @@ class Mail implements IDelegatedSettings {
                        'mail_smtpsecure' => $this->config->getSystemValue('mail_smtpsecure', ''),
                        'mail_smtphost' => $this->config->getSystemValue('mail_smtphost', ''),
                        'mail_smtpport' => $this->config->getSystemValue('mail_smtpport', ''),
-                       'mail_smtpauthtype' => $this->config->getSystemValue('mail_smtpauthtype', ''),
                        'mail_smtpauth' => $this->config->getSystemValue('mail_smtpauth', false),
                        'mail_smtpname' => $this->config->getSystemValue('mail_smtpname', ''),
                        'mail_smtppassword' => $this->config->getSystemValue('mail_smtppassword', ''),
index 6b85fcbe867530c3ea114f6d425edd41359c7c08..6b31ebe879cfd9db791b38d16ddeb97652276459 100644 (file)
 /** @var \OCP\IL10N $l */
 /** @var array $_ */
 
-$mail_smtpauthtype = [
-       '' => $l->t('None'),
-       'LOGIN' => $l->t('Login'),
-       'PLAIN' => $l->t('Plain'),
-       'NTLM' => $l->t('NT LAN Manager'),
-];
-
 $mail_smtpsecure = [
        '' => $l->t('None'),
-       'ssl' => $l->t('SSL/TLS'),
-       'tls' => $l->t('STARTTLS'),
+       'ssl' => $l->t('SSL/TLS')
 ];
 
 $mail_smtpmode = [
@@ -112,26 +104,7 @@ $mail_sendmailmode = [
                                   value="<?php p($_['mail_domain']) ?>" />
                </p>
 
-               <p id="setting_smtpauth" <?php if ($_['mail_smtpmode'] !== 'smtp') {
-                                               print_unescaped(' class="hidden"');
-                                       } ?>>
-                       <label for="mail_smtpauthtype"><?php p($l->t('Authentication method')); ?></label>
-                       <select name="mail_smtpauthtype" id="mail_smtpauthtype">
-                               <?php foreach ($mail_smtpauthtype as $authtype => $name):
-                                       $selected = '';
-                                       if ($authtype == $_['mail_smtpauthtype']):
-                                               $selected = 'selected="selected"';
-                                       endif; ?>
-                                       <option value="<?php p($authtype)?>" <?php p($selected) ?>><?php p($name) ?></option>
-                               <?php endforeach;?>
-                       </select>
-
-                       <input type="checkbox" name="mail_smtpauth" id="mail_smtpauth" class="checkbox" value="1"
-                               <?php if ($_['mail_smtpauth']) {
-                                               print_unescaped('checked="checked"');
-                                       } ?> />
-                       <label for="mail_smtpauth"><?php p($l->t('Authentication required')); ?></label>
-               </p>
+<!--lo-->
 
                <p id="setting_smtphost" <?php if ($_['mail_smtpmode'] !== 'smtp') {
                                                print_unescaped(' class="hidden"');
@@ -145,7 +118,7 @@ $mail_sendmailmode = [
                </p>
        </form>
        <form class="mail_settings" id="mail_credentials_settings">
-               <p id="mail_credentials" <?php if (!$_['mail_smtpauth'] || $_['mail_smtpmode'] !== 'smtp') {
+               <p id="mail_credentials" <?php if ($_['mail_smtpmode'] !== 'smtp') {
                                                print_unescaped(' class="hidden"');
                                        } ?>>
                        <label for="mail_smtpname"><?php p($l->t('Credentials')); ?></label>
index 54461201201a010e1d9e8b117821e7dcf235e9f8..db6043030d3cb1275dacb951cf9471a4c8894c4d 100644 (file)
@@ -91,7 +91,6 @@ class MailSettingsControllerTest extends \Test\TestCase {
                                        'mail_smtpmode' => 'smtp',
                                        'mail_smtpsecure' => 'ssl',
                                        'mail_smtphost' => 'mx.nextcloud.org',
-                                       'mail_smtpauthtype' => 'NTLM',
                                        'mail_smtpauth' => 1,
                                        'mail_smtpport' => '25',
                                        'mail_sendmailmode' => null,
@@ -102,7 +101,6 @@ class MailSettingsControllerTest extends \Test\TestCase {
                                        'mail_smtpmode' => 'smtp',
                                        'mail_smtpsecure' => 'ssl',
                                        'mail_smtphost' => 'mx.nextcloud.org',
-                                       'mail_smtpauthtype' => 'NTLM',
                                        'mail_smtpauth' => null,
                                        'mail_smtpport' => '25',
                                        'mail_smtpname' => null,
@@ -118,7 +116,6 @@ class MailSettingsControllerTest extends \Test\TestCase {
                        'smtp',
                        'ssl',
                        'mx.nextcloud.org',
-                       'NTLM',
                        1,
                        '25',
                        null
@@ -132,7 +129,6 @@ class MailSettingsControllerTest extends \Test\TestCase {
                        'smtp',
                        'ssl',
                        'mx.nextcloud.org',
-                       'NTLM',
                        0,
                        '25',
                        null
index 7a70065ff5022488d17681dfb98821a9cbc19ddc..c3b08a9d509062fa4df69f32d469add5377d2619 100644 (file)
@@ -64,7 +64,6 @@ class MailTest extends TestCase {
                                ['mail_smtpsecure', '', true],
                                ['mail_smtphost', '', 'smtp.nextcloud.com'],
                                ['mail_smtpport', '', 25],
-                               ['mail_smtpauthtype', '', 'login'],
                                ['mail_smtpauth', false, true],
                                ['mail_smtpname', '', 'smtp.sender.com'],
                                ['mail_smtppassword', '', 'mypassword'],
@@ -82,7 +81,6 @@ class MailTest extends TestCase {
                                'mail_smtpsecure' => true,
                                'mail_smtphost' => 'smtp.nextcloud.com',
                                'mail_smtpport' => 25,
-                               'mail_smtpauthtype' => 'login',
                                'mail_smtpauth' => true,
                                'mail_smtpname' => 'smtp.sender.com',
                                'mail_smtppassword' => '********',
index f7861c3c94c47f5193010f3a10267f99f607c2cb..806345092f263d2e66fb6e96a3fa533cb3606a1d 100644 (file)
@@ -463,14 +463,17 @@ $CONFIG = [
 'mail_smtptimeout' => 10,
 
 /**
- * This depends on ``mail_smtpmode``. Specify when you are using ``ssl`` for SSL/TLS or
- * ``tls`` for STARTTLS, or leave empty for no encryption.
+ * This depends on ``mail_smtpmode``. Specify ``ssl`` when you are using SSL/TLS. Any other value will be ignored.
+ *
+ * If the server advertises STARTTLS capabilities, they might be used, but they cannot be enforced by
+ * this config option.
  *
  * Defaults to ``''`` (empty string)
  */
 'mail_smtpsecure' => '',
 
 /**
+ *
  * This depends on ``mail_smtpmode``. Change this to ``true`` if your mail
  * server requires authentication.
  *
@@ -478,14 +481,6 @@ $CONFIG = [
  */
 'mail_smtpauth' => false,
 
-/**
- * This depends on ``mail_smtpmode``. If SMTP authentication is required, choose
- * the authentication type as ``LOGIN`` or ``PLAIN``.
- *
- * Defaults to ``LOGIN``
- */
-'mail_smtpauthtype' => 'LOGIN',
-
 /**
  * This depends on ``mail_smtpauth``. Specify the username for authenticating to
  * the SMTP server.
@@ -1190,14 +1185,14 @@ $CONFIG = [
 'preview_office_cl_parameters' =>
        ' --headless --nologo --nofirststartwizard --invisible --norestore '.
        '--convert-to png --outdir ',
-       
+
 /**
  * custom path for ffmpeg binary
- * 
+ *
  * Defaults to ``null`` and falls back to searching ``avconv`` and ``ffmpeg`` in the configured ``PATH`` environment
  */
-'preview_ffmpeg_path' => '/usr/bin/ffmpeg',    
-       
+'preview_ffmpeg_path' => '/usr/bin/ffmpeg',
+
 /**
  * Set the URL of the Imaginary service to send image previews to.
  * Also requires the ``OC\Preview\Imaginary`` provider to be enabled.
index 12f71df86d9f3185a3e0301eefb8933731fcf08f..5bfe0dd0522b366df9075e1cd5d5142ff51e87f8 100644 (file)
@@ -27,6 +27,7 @@ declare(strict_types=1);
 namespace OC\Mail;
 
 use OCP\Mail\IAttachment;
+use Symfony\Component\Mime\Email;
 
 /**
  * Class Attachment
@@ -35,11 +36,21 @@ use OCP\Mail\IAttachment;
  * @since 13.0.0
  */
 class Attachment implements IAttachment {
-       /** @var \Swift_Mime_Attachment */
-       protected $swiftAttachment;
+       private ?string $body;
+       private ?string $name;
+       private ?string $contentType;
+       private ?string $path;
 
-       public function __construct(\Swift_Mime_Attachment $attachment) {
-               $this->swiftAttachment = $attachment;
+       public function __construct(
+               ?string $body,
+               ?string $name,
+               ?string $contentType,
+               ?string $path = null
+       ) {
+               $this->body = $body;
+               $this->name = $name;
+               $this->contentType = $contentType;
+               $this->path = $path;
        }
 
        /**
@@ -48,7 +59,7 @@ class Attachment implements IAttachment {
         * @since 13.0.0
         */
        public function setFilename(string $filename): IAttachment {
-               $this->swiftAttachment->setFilename($filename);
+               $this->name = $filename;
                return $this;
        }
 
@@ -58,7 +69,7 @@ class Attachment implements IAttachment {
         * @since 13.0.0
         */
        public function setContentType(string $contentType): IAttachment {
-               $this->swiftAttachment->setContentType($contentType);
+               $this->contentType = $contentType;
                return $this;
        }
 
@@ -68,14 +79,15 @@ class Attachment implements IAttachment {
         * @since 13.0.0
         */
        public function setBody(string $body): IAttachment {
-               $this->swiftAttachment->setBody($body);
+               $this->body = $body;
                return $this;
        }
 
-       /**
-        * @return \Swift_Mime_Attachment
-        */
-       public function getSwiftAttachment(): \Swift_Mime_Attachment {
-               return $this->swiftAttachment;
+       public function attach(Email $symfonyEmail): void {
+               if ($this->path !== null) {
+                       $symfonyEmail->attachFromPath($this->path, $this->name, $this->contentType);
+               } else {
+                       $symfonyEmail->attach($this->body, $this->name, $this->contentType);
+               }
        }
 }
index d0c3b04eacbd7bf30094cc64b0ee9f90214b1b50..05ef0bf513962cf3be98fcb7519f6c26bb5ca90b 100644 (file)
@@ -50,6 +50,15 @@ use OCP\Mail\IEMailTemplate;
 use OCP\Mail\IMailer;
 use OCP\Mail\IMessage;
 use Psr\Log\LoggerInterface;
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
+use Symfony\Component\Mailer\Mailer as SymfonyMailer;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mailer\Transport\SendmailTransport;
+use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
+use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Exception\InvalidArgumentException;
+use Symfony\Component\Mime\Exception\RfcComplianceException;
 
 /**
  * Class Mailer provides some basic functions to create a mail message that can be used in combination with
@@ -70,12 +79,10 @@ use Psr\Log\LoggerInterface;
  * @package OC\Mail
  */
 class Mailer implements IMailer {
-       /** @var \Swift_Mailer Cached mailer */
-       private $instance = null;
+       private ?MailerInterface $instance = null;
        private IConfig $config;
        private LoggerInterface $logger;
-       /** @var Defaults */
-       private $defaults;
+       private Defaults $defaults;
        private IURLGenerator $urlGenerator;
        private IL10N $l10n;
        private IEventDispatcher $dispatcher;
@@ -100,11 +107,11 @@ class Mailer implements IMailer {
        /**
         * Creates a new message object that can be passed to send()
         *
-        * @return IMessage
+        * @return Message
         */
-       public function createMessage(): IMessage {
+       public function createMessage(): Message {
                $plainTextOnly = $this->config->getSystemValue('mail_send_plaintext_only', false);
-               return new Message(new \Swift_Message(), $plainTextOnly);
+               return new Message(new Email(), $plainTextOnly);
        }
 
        /**
@@ -115,7 +122,7 @@ class Mailer implements IMailer {
         * @since 13.0.0
         */
        public function createAttachment($data = null, $filename = null, $contentType = null): IAttachment {
-               return new Attachment(new \Swift_Attachment($data, $filename, $contentType));
+               return new Attachment($data, $filename, $contentType);
        }
 
        /**
@@ -125,7 +132,7 @@ class Mailer implements IMailer {
         * @since 13.0.0
         */
        public function createAttachmentFromPath(string $path, $contentType = null): IAttachment {
-               return new Attachment(\Swift_Attachment::fromPath($path, $contentType));
+               return new Attachment(null, null, $contentType, $path);
        }
 
        /**
@@ -162,49 +169,82 @@ class Mailer implements IMailer {
         * Send the specified message. Also sets the from address to the value defined in config.php
         * if no-one has been passed.
         *
-        * @param IMessage|Message $message Message to send
-        * @return string[] Array with failed recipients. Be aware that this depends on the used mail backend and
-        * therefore should be considered
-        * @throws \Exception In case it was not possible to send the message. (for example if an invalid mail address
-        * has been supplied.)
+        * If sending failed, the recipients that failed will be returned (to, cc and bcc).
+        * Will output additional debug info if 'mail_smtpdebug' => 'true' is set in config.php
+        *
+        * @param IMessage $message Message to send
+        * @return string[] $failedRecipients
         */
        public function send(IMessage $message): array {
                $debugMode = $this->config->getSystemValue('mail_smtpdebug', false);
 
+               if (!($message instanceof Message)) {
+                       throw new InvalidArgumentException('Object not of type ' . Message::class);
+               }
+
                if (empty($message->getFrom())) {
                        $message->setFrom([\OCP\Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
                }
 
-               $failedRecipients = [];
-
                $mailer = $this->getInstance();
 
-               // Enable logger if debug mode is enabled
-               if ($debugMode) {
-                       $mailLogger = new \Swift_Plugins_Loggers_ArrayLogger();
-                       $mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($mailLogger));
-               }
+               $this->dispatcher->dispatchTyped(new BeforeMessageSent($message));
 
+               try {
+                       $message->setRecipients();
+               } catch (InvalidArgumentException|RfcComplianceException $e) {
+                       $logMessage = sprintf(
+                               'Could not send mail to "%s" with subject "%s" as validation for address failed',
+                               print_r(array_merge($message->getTo(), $message->getCc(), $message->getBcc()), true),
+                               $message->getSubject()
+                       );
+                       $this->logger->debug($logMessage, ['app' => 'core', 'exception' => $e]);
+                       $recipients = array_merge($message->getTo(), $message->getCc(), $message->getBcc());
+                       $failedRecipients = [];
+
+                       array_walk($recipients, function ($value, $key) use (&$failedRecipients) {
+                               if (is_numeric($key)) {
+                                       $failedRecipients[] = $value;
+                               } else {
+                                       $failedRecipients[] = $key;
+                               }
+                       });
 
-               $this->dispatcher->dispatchTyped(new BeforeMessageSent($message));
+                       return $failedRecipients;
+               }
 
-               $mailer->send($message->getSwiftMessage(), $failedRecipients);
+               try {
+                       $mailer->send($message->getSymfonyEmail());
+               } catch (TransportExceptionInterface $e) {
+                       $logMessage = sprintf('Sending mail to "%s" with subject "%s" failed', print_r($message->getTo(), true), $message->getSubject());
+                       $this->logger->debug($logMessage, ['app' => 'core', 'exception' => $e]);
+                       if ($debugMode) {
+                               $this->logger->debug($e->getDebug(), ['app' => 'core']);
+                       }
+                       $recipients = array_merge($message->getTo(), $message->getCc(), $message->getBcc());
+                       $failedRecipients = [];
+
+                       array_walk($recipients, function ($value, $key) use (&$failedRecipients) {
+                               if (is_numeric($key)) {
+                                       $failedRecipients[] = $value;
+                               } else {
+                                       $failedRecipients[] = $key;
+                               }
+                       });
+
+                       return $failedRecipients;
+               }
 
                // Debugging logging
                $logMessage = sprintf('Sent mail to "%s" with subject "%s"', print_r($message->getTo(), true), $message->getSubject());
-               if (!empty($failedRecipients)) {
-                       $logMessage .= sprintf(' (failed for "%s")', print_r($failedRecipients, true));
-               }
                $this->logger->debug($logMessage, ['app' => 'core']);
-               if ($debugMode && isset($mailLogger)) {
-                       $this->logger->debug($mailLogger->dump(), ['app' => 'core']);
-               }
 
-               return $failedRecipients;
+               return [];
        }
 
        /**
-        * Checks if an e-mail address is valid
+        * @deprecated 26.0.0 Implicit validation is done in \OC\Mail\Message::setRecipients
+        *                    via \Symfony\Component\Mime\Address::__construct
         *
         * @param string $email Email address to be validated
         * @return bool True if the mail address is valid, false otherwise
@@ -217,28 +257,10 @@ class Mailer implements IMailer {
                $validator = new EmailValidator();
                $validation = new RFCValidation();
 
-               return $validator->isValid($this->convertEmail($email), $validation);
+               return $validator->isValid($email, $validation);
        }
 
-       /**
-        * SwiftMailer does currently not work with IDN domains, this function therefore converts the domains
-        *
-        * FIXME: Remove this once SwiftMailer supports IDN
-        *
-        * @param string $email
-        * @return string Converted mail address if `idn_to_ascii` exists
-        */
-       protected function convertEmail(string $email): string {
-               if (!function_exists('idn_to_ascii') || !defined('INTL_IDNA_VARIANT_UTS46') || strpos($email, '@') === false) {
-                       return $email;
-               }
-
-               [$name, $domain] = explode('@', $email, 2);
-               $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
-               return $name.'@'.$domain;
-       }
-
-       protected function getInstance(): \Swift_Mailer {
+       protected function getInstance(): MailerInterface {
                if (!is_null($this->instance)) {
                        return $this->instance;
                }
@@ -255,31 +277,47 @@ class Mailer implements IMailer {
                                break;
                }
 
-               return new \Swift_Mailer($transport);
+               return new SymfonyMailer($transport);
        }
 
        /**
         * Returns the SMTP transport
         *
-        * @return \Swift_SmtpTransport
+        * Only supports ssl/tls
+        * starttls is not enforcable with Symfony Mailer but might be available
+        * via the automatic config (Symfony Mailer internal)
+        *
+        * @return EsmtpTransport
         */
-       protected function getSmtpInstance(): \Swift_SmtpTransport {
-               $transport = new \Swift_SmtpTransport();
-               $transport->setTimeout($this->config->getSystemValue('mail_smtptimeout', 10));
-               $transport->setHost($this->config->getSystemValue('mail_smtphost', '127.0.0.1'));
-               $transport->setPort($this->config->getSystemValue('mail_smtpport', 25));
+       protected function getSmtpInstance(): EsmtpTransport {
+               // either null or true - if nothing is passed, let the symfony mailer figure out the configuration by itself
+               $mailSmtpsecure = ($this->config->getSystemValue('mail_smtpsecure', null) === 'ssl') ? true : null;
+               $transport = new EsmtpTransport(
+                       $this->config->getSystemValue('mail_smtphost', '127.0.0.1'),
+                       (int)$this->config->getSystemValue('mail_smtpport', 25),
+                       $mailSmtpsecure,
+                       null,
+                       $this->logger
+               );
+               /** @var SocketStream $stream */
+               $stream = $transport->getStream();
+               /** @psalm-suppress InternalMethod */
+               $stream->setTimeout($this->config->getSystemValue('mail_smtptimeout', 10));
+
                if ($this->config->getSystemValue('mail_smtpauth', false)) {
                        $transport->setUsername($this->config->getSystemValue('mail_smtpname', ''));
                        $transport->setPassword($this->config->getSystemValue('mail_smtppassword', ''));
-                       $transport->setAuthMode($this->config->getSystemValue('mail_smtpauthtype', 'LOGIN'));
-               }
-               $smtpSecurity = $this->config->getSystemValue('mail_smtpsecure', '');
-               if (!empty($smtpSecurity)) {
-                       $transport->setEncryption($smtpSecurity);
                }
+
                $streamingOptions = $this->config->getSystemValue('mail_smtpstreamoptions', []);
                if (is_array($streamingOptions) && !empty($streamingOptions)) {
-                       $transport->setStreamOptions($streamingOptions);
+                       /** @psalm-suppress InternalMethod */
+                       $currentStreamingOptions = $stream->getStreamOptions();
+
+                       $currentStreamingOptions = array_merge_recursive($currentStreamingOptions, $streamingOptions);
+
+                       /** @psalm-suppress InternalMethod */
+                       $stream->setStreamOptions($currentStreamingOptions);
                }
 
                $overwriteCliUrl = parse_url(
@@ -297,9 +335,9 @@ class Mailer implements IMailer {
        /**
         * Returns the sendmail transport
         *
-        * @return \Swift_SendmailTransport
+        * @return SendmailTransport
         */
-       protected function getSendMailInstance(): \Swift_SendmailTransport {
+       protected function getSendMailInstance(): SendmailTransport {
                switch ($this->config->getSystemValue('mail_smtpmode', 'smtp')) {
                        case 'qmail':
                                $binaryPath = '/var/qmail/bin/sendmail';
@@ -322,6 +360,6 @@ class Mailer implements IMailer {
                                break;
                }
 
-               return new \Swift_SendmailTransport($binaryPath . $binaryParam);
+               return new SendmailTransport($binaryPath . $binaryParam, null, $this->logger);
        }
 }
index 3313b39e2e2beebfc16471bcdfb8baf86ddedcf0..01beefcc6d6e234c425540ded7523fdb2c130c32 100644 (file)
@@ -35,64 +35,67 @@ use OCP\Mail\Headers\AutoSubmitted;
 use OCP\Mail\IAttachment;
 use OCP\Mail\IEMailTemplate;
 use OCP\Mail\IMessage;
-use Swift_Message;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Exception\InvalidArgumentException;
+use Symfony\Component\Mime\Exception\RfcComplianceException;
 
 /**
- * Class Message provides a wrapper around SwiftMail
+ * Class Message provides a wrapper around Symfony\Component\Mime\Email (Used to be around SwiftMail)
  *
  * @package OC\Mail
  */
 class Message implements IMessage {
-       /** @var Swift_Message */
-       private $swiftMessage;
-       /** @var bool */
-       private $plainTextOnly;
+       private Email $symfonyEmail;
+       private bool $plainTextOnly;
 
-       public function __construct(Swift_Message $swiftMessage, bool $plainTextOnly) {
-               $this->swiftMessage = $swiftMessage;
+       private array $to;
+       private array $from;
+       private array $replyTo;
+       private array $cc;
+       private array $bcc;
+
+       public function __construct(Email $symfonyEmail, bool $plainTextOnly) {
+               $this->symfonyEmail = $symfonyEmail;
                $this->plainTextOnly = $plainTextOnly;
+               $this->to = [];
+               $this->from = [];
+               $this->replyTo = [];
+               $this->cc = [];
+               $this->bcc = [];
        }
 
        /**
-        * @param IAttachment $attachment
         * @return $this
         * @since 13.0.0
         */
        public function attach(IAttachment $attachment): IMessage {
                /** @var Attachment $attachment */
-               $this->swiftMessage->attach($attachment->getSwiftAttachment());
+               $attachment->attach($this->symfonyEmail);
                return $this;
        }
 
        /**
-        * SwiftMailer does currently not work with IDN domains, this function therefore converts the domains
-        * FIXME: Remove this once SwiftMailer supports IDN
+        * Converts the [['displayName' => 'email'], ['displayName2' => 'email2']] arrays to valid Adresses
         *
-        * @param array $addresses Array of mail addresses, key will get converted
-        * @return array Converted addresses if `idn_to_ascii` exists
+        * @param array $addresses Array of mail addresses
+        * @return Address[]
+        * @throws RfcComplianceException|InvalidArgumentException
         */
        protected function convertAddresses(array $addresses): array {
-               if (!function_exists('idn_to_ascii') || !defined('INTL_IDNA_VARIANT_UTS46')) {
-                       return $addresses;
-               }
-
                $convertedAddresses = [];
 
-               foreach ($addresses as $email => $readableName) {
-                       $parsableEmail = is_numeric($email) ? $readableName : $email;
-                       if (strpos($parsableEmail, '@') === false) {
-                               $convertedAddresses[$parsableEmail] = $readableName;
-                               continue;
-                       }
+               if (empty($addresses)) {
+                       return [];
+               }
 
-                       [$name, $domain] = explode('@', $parsableEmail, 2);
-                       $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
+               array_walk($addresses, function ($readableName, $email) use (&$convertedAddresses) {
                        if (is_numeric($email)) {
-                               $convertedAddresses[] = $name . '@' . $domain;
+                               $convertedAddresses[] = new Address($readableName);
                        } else {
-                               $convertedAddresses[$name . '@' . $domain] = $readableName;
+                               $convertedAddresses[] = new Address($email, $readableName);
                        }
-               }
+               });
 
                return $convertedAddresses;
        }
@@ -106,41 +109,32 @@ class Message implements IMessage {
         * @return $this
         */
        public function setFrom(array $addresses): IMessage {
-               $addresses = $this->convertAddresses($addresses);
-
-               $this->swiftMessage->setFrom($addresses);
+               $this->from = $addresses;
                return $this;
        }
 
        /**
         * Get the from address of this message.
-        *
-        * @return array
         */
        public function getFrom(): array {
-               return $this->swiftMessage->getFrom() ?? [];
+               return $this->from;
        }
 
        /**
         * Set the Reply-To address of this message
         *
-        * @param array $addresses
         * @return $this
         */
        public function setReplyTo(array $addresses): IMessage {
-               $addresses = $this->convertAddresses($addresses);
-
-               $this->swiftMessage->setReplyTo($addresses);
+               $this->replyTo = $addresses;
                return $this;
        }
 
        /**
         * Returns the Reply-To address of this message
-        *
-        * @return string
         */
-       public function getReplyTo(): string {
-               return $this->swiftMessage->getReplyTo();
+       public function getReplyTo(): array {
+               return $this->replyTo;
        }
 
        /**
@@ -150,19 +144,15 @@ class Message implements IMessage {
         * @return $this
         */
        public function setTo(array $recipients): IMessage {
-               $recipients = $this->convertAddresses($recipients);
-
-               $this->swiftMessage->setTo($recipients);
+               $this->to = $recipients;
                return $this;
        }
 
        /**
         * Get the to address of this message.
-        *
-        * @return array
         */
        public function getTo(): array {
-               return $this->swiftMessage->getTo() ?? [];
+               return $this->to;
        }
 
        /**
@@ -172,19 +162,15 @@ class Message implements IMessage {
         * @return $this
         */
        public function setCc(array $recipients): IMessage {
-               $recipients = $this->convertAddresses($recipients);
-
-               $this->swiftMessage->setCc($recipients);
+               $this->cc = $recipients;
                return $this;
        }
 
        /**
         * Get the cc address of this message.
-        *
-        * @return array
         */
        public function getCc(): array {
-               return $this->swiftMessage->getCc() ?? [];
+               return $this->cc;
        }
 
        /**
@@ -194,104 +180,119 @@ class Message implements IMessage {
         * @return $this
         */
        public function setBcc(array $recipients): IMessage {
-               $recipients = $this->convertAddresses($recipients);
-
-               $this->swiftMessage->setBcc($recipients);
+               $this->bcc = $recipients;
                return $this;
        }
 
        /**
         * Get the Bcc address of this message.
-        *
-        * @return array
         */
        public function getBcc(): array {
-               return $this->swiftMessage->getBcc() ?? [];
+               return $this->bcc;
        }
 
        /**
         * Set the subject of this message.
         *
-        * @param string $subject
-        * @return IMessage
+        * @return $this
         */
        public function setSubject(string $subject): IMessage {
-               $this->swiftMessage->setSubject($subject);
+               $this->symfonyEmail->subject($subject);
                return $this;
        }
 
        /**
         * Get the from subject of this message.
-        *
-        * @return string
         */
        public function getSubject(): string {
-               return $this->swiftMessage->getSubject();
+               return $this->symfonyEmail->getSubject() ?? '';
        }
 
        /**
         * Set the plain-text body of this message.
-        *
-        * @param string $body
         * @return $this
         */
        public function setPlainBody(string $body): IMessage {
-               $this->swiftMessage->setBody($body);
+               $this->symfonyEmail->text($body);
                return $this;
        }
 
        /**
         * Get the plain body of this message.
-        *
-        * @return string
         */
        public function getPlainBody(): string {
-               return $this->swiftMessage->getBody();
+               /** @var string $body */
+               $body = $this->symfonyEmail->getTextBody() ?? '';
+               return $body;
        }
 
        /**
         * Set the HTML body of this message. Consider also sending a plain-text body instead of only an HTML one.
-        *
-        * @param string $body
         * @return $this
         */
-       public function setHtmlBody($body) {
+       public function setHtmlBody(string $body): IMessage {
                if (!$this->plainTextOnly) {
-                       $this->swiftMessage->addPart($body, 'text/html');
+                       $this->symfonyEmail->html($body);
                }
                return $this;
        }
 
        /**
-        * Get's the underlying SwiftMessage
-        * @param Swift_Message $swiftMessage
+        * Set the underlying Email intance
         */
-       public function setSwiftMessage(Swift_Message $swiftMessage): void {
-               $this->swiftMessage = $swiftMessage;
+       public function setSymfonyEmail(Email $symfonyEmail): void {
+               $this->symfonyEmail = $symfonyEmail;
        }
 
        /**
-        * Get's the underlying SwiftMessage
-        * @return Swift_Message
+        * Get the underlying Email instance
         */
-       public function getSwiftMessage(): Swift_Message {
-               return $this->swiftMessage;
+       public function getSymfonyEmail(): Email {
+               return $this->symfonyEmail;
        }
 
        /**
-        * @param string $body
-        * @param string $contentType
         * @return $this
         */
-       public function setBody($body, $contentType) {
+       public function setBody(string $body, string $contentType): IMessage {
                if (!$this->plainTextOnly || $contentType !== 'text/html') {
-                       $this->swiftMessage->setBody($body, $contentType);
+                       if ($contentType === 'text/html') {
+                               $this->symfonyEmail->html($body);
+                       } else {
+                               $this->symfonyEmail->text($body);
+                       }
                }
                return $this;
        }
 
        /**
-        * @param IEMailTemplate $emailTemplate
+        * Set the recipients on the symphony email
+        *
+        * Since
+        *
+        * setTo
+        * setFrom
+        * setReplyTo
+        * setCc
+        * setBcc
+        *
+        * could throw a \Symfony\Component\Mime\Exception\RfcComplianceException
+        * or a \Symfony\Component\Mime\Exception\InvalidArgumentException
+        * we wrap the calls here. We then have the validation errors all in one place and can
+        * throw shortly before \OC\Mail\Mailer::send
+        *
+        * @return void
+        * @throws InvalidArgumentException|RfcComplianceException
+        */
+       public function setRecipients() {
+               $this->symfonyEmail->to(...$this->convertAddresses($this->getTo()));
+               $this->symfonyEmail->from(...$this->convertAddresses($this->getFrom()));
+               $this->symfonyEmail->replyTo(...$this->convertAddresses($this->getReplyTo()));
+               $this->symfonyEmail->cc(...$this->convertAddresses($this->getCc()));
+               $this->symfonyEmail->bcc(...$this->convertAddresses($this->getBcc()));
+       }
+
+       /**
         * @return $this
         */
        public function useTemplate(IEMailTemplate $emailTemplate): IMessage {
@@ -311,7 +312,7 @@ class Message implements IMessage {
         * @return $this
         */
        public function setAutoSubmitted(string $value): IMessage {
-               $headers = $this->swiftMessage->getHeaders();
+               $headers = $this->symfonyEmail->getHeaders();
 
                if ($headers->has(AutoSubmitted::HEADER)) {
                        // if the header already exsists, remove it.
@@ -319,6 +320,7 @@ class Message implements IMessage {
                        // of the interface \Swift_Mime_Header, however the
                        // interface doesn't, and this makes the static-code
                        // analysis unhappy.
+                       // @todo check if symfony mailer can modify the autosubmitted header
                        $headers->remove(AutoSubmitted::HEADER);
                }
 
@@ -334,9 +336,9 @@ class Message implements IMessage {
         * @return string
         */
        public function getAutoSubmitted(): string {
-               $headers = $this->swiftMessage->getHeaders();
+               $headers = $this->symfonyEmail->getHeaders();
 
                return $headers->has(AutoSubmitted::HEADER) ?
-                       $headers->get(AutoSubmitted::HEADER)->toString() : AutoSubmitted::VALUE_NO;
+                       $headers->get(AutoSubmitted::HEADER)->getBodyAsString() : AutoSubmitted::VALUE_NO;
        }
 }
index 325abd9301e7b2794daccebc938a9469d24515ab..db46401a3d82bab7ff5e91b72bae347cd4152c30 100644 (file)
@@ -96,7 +96,7 @@ interface IMailer {
        public function send(IMessage $message): array;
 
        /**
-        * Checks if an e-mail address is valid
+        * @deprecated 26.0.0
         *
         * @param string $email Email address to be validated
         * @return bool True if the mail address is valid, false otherwise
index 74c4f2a62ab856423f74e5abcf3170bc7551e0d9..39e4d689a3709bf1fb53a17a5e5cbb9faa960198 100644 (file)
@@ -22,19 +22,23 @@ use OCP\IURLGenerator;
 use OCP\L10N\IFactory;
 use OCP\Mail\Events\BeforeMessageSent;
 use Psr\Log\LoggerInterface;
-use Swift_SwiftException;
 use Test\TestCase;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Mailer\Mailer as SymfonyMailer;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
+use Symfony\Component\Mailer\Transport\SendmailTransport;
 
 class MailerTest extends TestCase {
-       /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
+       /** @var IConfig|MockObject */
        private $config;
-       /** @var Defaults|\PHPUnit\Framework\MockObject\MockObject */
+       /** @var Defaults|MockObject */
        private $defaults;
-       /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+       /** @var LoggerInterface|MockObject */
        private $logger;
-       /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
+       /** @var IURLGenerator|MockObject */
        private $urlGenerator;
-       /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
+       /** @var IL10N|MockObject */
        private $l10n;
        /** @var Mailer */
        private $mailer;
@@ -91,7 +95,7 @@ class MailerTest extends TestCase {
                        $path = '/usr/sbin/sendmail';
                }
 
-               $expected = new \Swift_SendmailTransport($path . $binaryParam);
+               $expected = new SendmailTransport($path . $binaryParam, null, $this->logger);
                $this->assertEquals($expected, self::invokePrivate($this->mailer, 'getSendMailInstance'));
        }
 
@@ -109,13 +113,22 @@ class MailerTest extends TestCase {
                                ['mail_sendmailmode', 'smtp', $sendmailMode],
                        ]);
 
-               $this->assertEquals(new \Swift_SendmailTransport('/var/qmail/bin/sendmail' . $binaryParam), self::invokePrivate($this->mailer, 'getSendMailInstance'));
+               $sendmail = new SendmailTransport('/var/qmail/bin/sendmail' . $binaryParam, null, $this->logger);
+               $this->assertEquals($sendmail, self::invokePrivate($this->mailer, 'getSendMailInstance'));
        }
 
        public function testGetInstanceDefault() {
+               $this->config
+                       ->method('getSystemValue')
+                       ->willReturnMap([
+                               ['mail_smtphost', '127.0.0.1', '127.0.0.1'],
+                               ['mail_smtpport', 25, 25],
+                               ['mail_smtptimeout', 10, 10],
+                       ]);
                $mailer = self::invokePrivate($this->mailer, 'getInstance');
-               $this->assertInstanceOf(\Swift_Mailer::class, $mailer);
-               $this->assertInstanceOf(\Swift_SmtpTransport::class, $mailer->getTransport());
+               $this->assertInstanceOf(SymfonyMailer::class, $mailer);
+               $transport = self::invokePrivate($mailer, 'transport');
+               $this->assertInstanceOf(EsmtpTransport::class, $transport);
        }
 
        public function testGetInstanceSendmail() {
@@ -127,11 +140,33 @@ class MailerTest extends TestCase {
                        ]);
 
                $mailer = self::invokePrivate($this->mailer, 'getInstance');
-               $this->assertInstanceOf(\Swift_Mailer::class, $mailer);
-               $this->assertInstanceOf(\Swift_SendmailTransport::class, $mailer->getTransport());
+               $this->assertInstanceOf(SymfonyMailer::class, $mailer);
+               $transport = self::invokePrivate($mailer, 'transport');
+               $this->assertInstanceOf(SendmailTransport::class, $transport);
        }
 
        public function testEvents() {
+               $this->config
+                       ->method('getSystemValue')
+                       ->willReturnMap([
+                               ['mail_smtphost', '127.0.0.1', '127.0.0.1'],
+                               ['mail_smtpport', 25, 25],
+                       ]);
+               $this->mailer = $this->getMockBuilder(Mailer::class)
+                       ->setMethods(['getInstance'])
+                       ->setConstructorArgs(
+                               [
+                                       $this->config,
+                                       $this->logger,
+                                       $this->defaults,
+                                       $this->urlGenerator,
+                                       $this->l10n,
+                                       $this->dispatcher,
+                                       $this->createMock(IFactory::class)
+                               ]
+                       )
+                       ->getMock();
+
                $message = $this->createMock(Message::class);
 
                $event = new BeforeMessageSent($message);
@@ -139,11 +174,7 @@ class MailerTest extends TestCase {
                        ->method('dispatchTyped')
                        ->with($this->equalTo($event));
 
-               # We do not care at this point about errors in Swiftmailer
-               try {
-                       $this->mailer->send($message);
-               } catch (Swift_SwiftException $e) {
-               }
+               $this->mailer->send($message);
        }
 
        public function testCreateMessage() {
@@ -157,13 +188,20 @@ class MailerTest extends TestCase {
 
 
        public function testSendInvalidMailException() {
+               $this->config
+                       ->method('getSystemValue')
+                       ->willReturnMap([
+                               ['mail_smtphost', '127.0.0.1', '127.0.0.1'],
+                               ['mail_smtpport', 25, 25],
+                               ['mail_smtptimeout', 10, 10],
+                       ]);
                $this->expectException(\Exception::class);
 
                $message = $this->getMockBuilder('\OC\Mail\Message')
                        ->disableOriginalConstructor()->getMock();
                $message->expects($this->once())
-                       ->method('getSwiftMessage')
-                       ->willReturn(new \Swift_Message());
+                       ->method('getSymfonyEmail')
+                       ->willReturn(new Email());
 
                $this->mailer->send($message);
        }
@@ -202,58 +240,76 @@ class MailerTest extends TestCase {
                $this->config->method('getSystemValue')
                        ->willReturnMap([
                                ['mail_smtpmode', 'smtp', 'smtp'],
-                               ['mail_smtpstreamoptions', [], ['foo' => 1]]
+                               ['mail_smtpstreamoptions', [], ['foo' => 1]],
+                               ['mail_smtphost', '127.0.0.1', '127.0.0.1'],
+                               ['mail_smtpport', 25, 25],
+                               ['mail_smtptimeout', 10, 10],
                        ]);
                $mailer = self::invokePrivate($this->mailer, 'getInstance');
-               $this->assertEquals(1, count($mailer->getTransport()->getStreamOptions()));
-               $this->assertTrue(isset($mailer->getTransport()->getStreamOptions()['foo']));
+               /** @var EsmtpTransport $transport */
+               $transport = self::invokePrivate($mailer, 'transport');
+               $this->assertInstanceOf(EsmtpTransport::class, $transport);
+               $this->assertEquals(1, count($transport->getStream()->getStreamOptions()));
+               $this->assertTrue(isset($transport->getStream()->getStreamOptions()['foo']));
        }
 
        public function testStreamingOptionsWrongType() {
                $this->config->method('getSystemValue')
                        ->willReturnMap([
                                ['mail_smtpmode', 'smtp', 'smtp'],
-                               ['mail_smtpstreamoptions', [], 'bar']
+                               ['mail_smtpstreamoptions', [], 'bar'],
+                               ['mail_smtphost', '127.0.0.1', '127.0.0.1'],
+                               ['mail_smtpport', 25, 25],
+                               ['mail_smtptimeout', 10, 10],
                        ]);
                $mailer = self::invokePrivate($this->mailer, 'getInstance');
-               $this->assertEquals(0, count($mailer->getTransport()->getStreamOptions()));
+               /** @var EsmtpTransport $transport */
+               $transport = self::invokePrivate($mailer, 'transport');
+               $this->assertInstanceOf(EsmtpTransport::class, $transport);
+               $this->assertEquals(0, count($transport->getStream()->getStreamOptions()));
        }
 
        public function testLocalDomain(): void {
                $this->config->method('getSystemValue')
                        ->willReturnMap([
-                               ['mail_smtpmode', 'smtp', 'smtp']
+                               ['mail_smtpmode', 'smtp', 'smtp'],
+                               ['mail_smtphost', '127.0.0.1', '127.0.0.1'],
+                               ['mail_smtpport', 25, 25],
+                               ['mail_smtptimeout', 10, 10],
                        ]);
                $this->config->method('getSystemValueString')
                        ->with('overwrite.cli.url', '')
                        ->willReturn('https://some.valid.url.com:8080');
 
-               /** @var \Swift_Mailer $mailer */
+               /** @var SymfonyMailer $mailer */
                $mailer = self::invokePrivate($this->mailer, 'getInstance');
-               self::assertInstanceOf(\Swift_Mailer::class, $mailer);
+               self::assertInstanceOf(SymfonyMailer::class, $mailer);
 
-               /** @var \Swift_Transport_EsmtpTransport $transport */
-               $transport = $mailer->getTransport();
-               self::assertInstanceOf(\Swift_Transport_EsmtpTransport::class, $transport);
+               /** @var EsmtpTransport $transport */
+               $transport = self::invokePrivate($mailer, 'transport');
+               self::assertInstanceOf(EsmtpTransport::class, $transport);
                self::assertEquals('some.valid.url.com', $transport->getLocalDomain());
        }
 
        public function testLocalDomainInvalidUrl(): void {
                $this->config->method('getSystemValue')
                        ->willReturnMap([
-                               ['mail_smtpmode', 'smtp', 'smtp']
+                               ['mail_smtpmode', 'smtp', 'smtp'],
+                               ['mail_smtphost', '127.0.0.1', '127.0.0.1'],
+                               ['mail_smtpport', 25, 25],
+                               ['mail_smtptimeout', 10, 10],
                        ]);
                $this->config->method('getSystemValueString')
                        ->with('overwrite.cli.url', '')
                        ->willReturn('https:only.slash.does.not.work:8080');
 
-               /** @var \Swift_Mailer $mailer */
+               /** @var SymfonyMailer $mailer */
                $mailer = self::invokePrivate($this->mailer, 'getInstance');
-               self::assertInstanceOf(\Swift_Mailer::class, $mailer);
+               self::assertInstanceOf(SymfonyMailer::class, $mailer);
 
-               /** @var \Swift_Transport_EsmtpTransport $transport */
-               $transport = $mailer->getTransport();
-               self::assertInstanceOf(\Swift_Transport_EsmtpTransport::class, $transport);
+               /** @var EsmtpTransport $transport */
+               $transport = self::invokePrivate($mailer, 'transport');
+               self::assertInstanceOf(EsmtpTransport::class, $transport);
                self::assertEquals('[127.0.0.1]', $transport->getLocalDomain());
        }
 }
index b97240d13366db3e0bf1e9322c5b096377191dfa..2becc4d2081a706c742880f44f20383348ea010e 100644 (file)
@@ -11,12 +11,17 @@ namespace Test\Mail;
 use OC\Mail\Message;
 use OCP\Mail\Headers\AutoSubmitted;
 use OCP\Mail\IEMailTemplate;
-use Swift_Message;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Exception\RfcComplianceException;
+use Symfony\Component\Mime\Header\HeaderInterface;
+use Symfony\Component\Mime\Header\Headers;
 use Test\TestCase;
+use PHPUnit\Framework\MockObject\MockObject;
 
 class MessageTest extends TestCase {
-       /** @var Swift_Message */
-       private $swiftMessage;
+       /** @var Email */
+       private $symfonyEmail;
        /** @var Message */
        private $message;
 
@@ -25,10 +30,26 @@ class MessageTest extends TestCase {
         */
        public function mailAddressProvider() {
                return [
-                       [['lukas@owncloud.com' => 'Lukas Reschke'], ['lukas@owncloud.com' => 'Lukas Reschke']],
-                       [['lukas@owncloud.com' => 'Lukas Reschke', 'lukas@öwnclöüd.com', 'lukäs@owncloud.örg' => 'Lükäs Réschke'],
-                               ['lukas@owncloud.com' => 'Lukas Reschke', 'lukas@xn--wncld-iuae2c.com', 'lukäs@owncloud.xn--rg-eka' => 'Lükäs Réschke']],
-                       [['lukas@öwnclöüd.com'], ['lukas@xn--wncld-iuae2c.com']],
+                       [
+                               ['lukas@owncloud.com' => 'Lukas Reschke'],
+                               [new Address('lukas@owncloud.com', 'Lukas Reschke')]
+                       ],
+                       [
+                               [
+                                       'lukas@owncloud.com' => 'Lukas Reschke',
+                                       'lukas@öwnclöüd.com',
+                                       'lukäs@owncloud.örg' => 'Lükäs Réschke'
+                               ],
+                               [
+                                       new Address('lukas@owncloud.com', 'Lukas Reschke'),
+                                       new Address('lukas@öwnclöüd.com'),
+                                       new Address('lukäs@owncloud.örg', 'Lükäs Réschke')
+                               ]
+                       ],
+                       [
+                               ['lukas@öwnclöüd.com'],
+                               [new Address('lukas@öwnclöüd.com')]
+                       ],
                ];
        }
 
@@ -37,7 +58,7 @@ class MessageTest extends TestCase {
         */
        public function getMailAddressProvider() {
                return [
-                       [null, []],
+                       [[], []],
                        [['lukas@owncloud.com' => 'Lukas Reschke'], ['lukas@owncloud.com' => 'Lukas Reschke']],
                ];
        }
@@ -45,189 +66,158 @@ class MessageTest extends TestCase {
        protected function setUp(): void {
                parent::setUp();
 
-               $this->swiftMessage = $this->getMockBuilder('\Swift_Message')
+               $this->symfonyEmail = $this->getMockBuilder(Email::class)
                        ->disableOriginalConstructor()->getMock();
 
-               $this->message = new Message($this->swiftMessage, false);
+               $this->message = new Message($this->symfonyEmail, false);
        }
 
        /**
-        * @requires function idn_to_ascii
         * @dataProvider mailAddressProvider
         *
         * @param string $unconverted
         * @param string $expected
         */
        public function testConvertAddresses($unconverted, $expected) {
-               $this->assertSame($expected, self::invokePrivate($this->message, 'convertAddresses', [$unconverted]));
-       }
-
-       public function testSetFrom() {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('setFrom')
-                       ->with(['lukas@owncloud.com']);
-               $this->message->setFrom(['lukas@owncloud.com']);
+               $this->assertEquals($expected, self::invokePrivate($this->message, 'convertAddresses', [$unconverted]));
        }
 
+       public function testSetRecipients(): void {
+               $this->message = $this->message->setFrom(['pierres-general-store@stardewvalley.com' => 'Pierres General Store']);
+               $this->message = $this->message->setTo(['lewis-tent@stardewvalley.com' => "Lewis' Tent Life"]);
+               $this->message = $this->message->setReplyTo(['penny@stardewvalley-library.co.edu' => 'Penny']);
+               $this->message = $this->message->setCc(['gunther@stardewvalley-library.co.edu' => 'Gunther']);
+               $this->message = $this->message->setBcc(['pam@stardewvalley-bus.com' => 'Pam']);
 
-       /**
-        * @dataProvider getMailAddressProvider
-        *
-        * @param $swiftresult
-        * @param $return
-        */
-       public function testGetFrom($swiftresult, $return) {
-               $this->swiftMessage
+               $this->symfonyEmail
+                       ->expects($this->once())
+                       ->method('from')
+                       ->willReturn(new Address('pierres-general-store@stardewvalley.com', 'Pierres General Store'));
+               $this->symfonyEmail
+                       ->expects($this->once())
+                       ->method('to')
+                       ->willReturn(new Address('lewis-tent@stardewvalley.com', "Lewis' Tent Life"));
+               $this->symfonyEmail
+                       ->expects($this->once())
+                       ->method('replyTo')
+                       ->willReturn(new Address('penny@stardewvalley-library.co.edu', 'Penny'));
+               $this->symfonyEmail
+                       ->expects($this->once())
+                       ->method('cc')
+                       ->willReturn(new Address('gunther@stardewvalley-library.co.edu', 'Gunther'));
+               $this->symfonyEmail
                        ->expects($this->once())
-                       ->method('getFrom')
-                       ->willReturn($swiftresult);
+                       ->method('bcc')
+                       ->willReturn(new Address('pam@stardewvalley-bus.com', 'Pam'));
 
-               $this->assertSame($return, $this->message->getFrom());
+               $this->message->setRecipients();
        }
 
-       public function testSetReplyTo() {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('setReplyTo')
-                       ->with(['lukas@owncloud.com']);
-               $this->message->setReplyTo(['lukas@owncloud.com']);
-       }
+       public function testSetTo() {
+               $expected = ['pierres-general-store@stardewvalley.com' => 'Pierres General Store'];
 
-       public function testGetReplyTo() {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('getReplyTo')
-                       ->willReturn('lukas@owncloud.com');
+               $message = $this->message->setTo(['pierres-general-store@stardewvalley.com' => 'Pierres General Store']);
 
-               $this->assertSame('lukas@owncloud.com', $this->message->getReplyTo());
+               $this->assertEquals($expected, $message->getTo());
        }
+       public function testSetRecipientsException(): void {
+               $message = $this->message->setTo(['lewis-tent@~~~~.com' => "Lewis' Tent Life"]);
 
-       /** @dataProvider dataSetTo */
-       public function testSetTo(array $to, array $expected) {
-               $this->swiftMessage
+               $this->symfonyEmail
                        ->expects($this->once())
-                       ->method('setTo')
-                       ->with($expected);
-               $this->message->setTo($to);
-       }
+                       ->method('to')
+                       ->willThrowException(new RfcComplianceException());
 
-       public function dataSetTo(): array {
-               return [
-                       [['robot@example.com'], ['robot@example.com']],
-                       [['robot'], ['robot' => 'robot']],
-                       [['robot' => 'robot display name'], ['robot' => 'robot display name']],
-                       [['example@🤖.com'], ['example@xn--yp9h.com']],
-                       [['example@🤖.com' => 'A robot'], ['example@xn--yp9h.com' => 'A robot']],
-               ];
+               $this->expectException(RfcComplianceException::class);
+               $message->setRecipients();
        }
 
-       /**
-        * @dataProvider  getMailAddressProvider
-        */
-       public function testGetTo($swiftresult, $return) {
-               $this->swiftMessage
+       public function testSetRecipientsEmptyValues(): void {
+               $message = $this->message->setTo([]);
+
+               $this->symfonyEmail
                        ->expects($this->once())
-                       ->method('getTo')
-                       ->willReturn($swiftresult);
+                       ->method('to');
 
-               $this->assertSame($return, $this->message->getTo());
+               $message->setRecipients();
        }
 
-       public function testSetCc() {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('setCc')
-                       ->with(['lukas@owncloud.com']);
-               $this->message->setCc(['lukas@owncloud.com']);
-       }
+       public function testSetGetFrom() {
+               $expected = ['pierres-general-store@stardewvalley.com' => 'Pierres General Store'];
 
-       /**
-        * @dataProvider  getMailAddressProvider
-        */
-       public function testGetCc($swiftresult, $return) {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('getCc')
-                       ->willReturn($swiftresult);
+               $message = $this->message->setFrom(['pierres-general-store@stardewvalley.com' => 'Pierres General Store']);
 
-               $this->assertSame($return, $this->message->getCc());
+               $this->assertEquals($expected, $message->getFrom());
        }
 
-       public function testSetBcc() {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('setBcc')
-                       ->with(['lukas@owncloud.com']);
-               $this->message->setBcc(['lukas@owncloud.com']);
+       public function testSetGetTo() {
+               $expected = ['lewis-tent@stardewvalley.com' => "Lewis' Tent Life"];
+
+               $message = $this->message->setTo(['lewis-tent@stardewvalley.com' => "Lewis' Tent Life"]);
+
+               $this->assertEquals($expected, $message->getTo());
        }
 
-       /**
-        * @dataProvider  getMailAddressProvider
-        */
-       public function testGetBcc($swiftresult, $return) {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('getBcc')
-                       ->willReturn($swiftresult);
+       public function testSetGetReplyTo() {
+               $expected = ['penny@stardewvalley-library.co.edu' => 'Penny'];
 
-               $this->assertSame($return, $this->message->getBcc());
+               $message = $this->message->setReplyTo(['penny@stardewvalley-library.co.edu' => 'Penny']);
+
+               $this->assertEquals($expected, $message->getReplyTo());
        }
 
-       public function testSetSubject() {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('setSubject')
-                       ->with('Fancy Subject');
+       public function testSetGetCC() {
+               $expected = ['gunther@stardewvalley-library.co.edu' => 'Gunther'];
 
-               $this->message->setSubject('Fancy Subject');
+               $message = $this->message->setCc(['gunther@stardewvalley-library.co.edu' => 'Gunther']);
+
+               $this->assertEquals($expected, $message->getCc());
        }
 
-       public function testGetSubject() {
-               $this->swiftMessage
-                       ->expects($this->once())
-                       ->method('getSubject')
-                       ->willReturn('Fancy Subject');
+       public function testSetGetBCC() {
+               $expected = ['pam@stardewvalley-bus.com' => 'Pam'];
+
+               $message = $this->message->setBcc(['pam@stardewvalley-bus.com' => 'Pam']);
 
-               $this->assertSame('Fancy Subject', $this->message->getSubject());
+               $this->assertEquals($expected, $message->getBcc());
        }
 
        public function testSetPlainBody() {
-               $this->swiftMessage
+               $this->symfonyEmail
                        ->expects($this->once())
-                       ->method('setBody')
+                       ->method('text')
                        ->with('Fancy Body');
 
                $this->message->setPlainBody('Fancy Body');
        }
 
        public function testGetPlainBody() {
-               $this->swiftMessage
+               $this->symfonyEmail
                        ->expects($this->once())
-                       ->method('getBody')
+                       ->method('getTextBody')
                        ->willReturn('Fancy Body');
 
                $this->assertSame('Fancy Body', $this->message->getPlainBody());
        }
 
        public function testSetHtmlBody() {
-               $this->swiftMessage
+               $this->symfonyEmail
                        ->expects($this->once())
-                       ->method('addPart')
-                       ->with('<blink>Fancy Body</blink>', 'text/html');
+                       ->method('html')
+                       ->with('<blink>Fancy Body</blink>', 'utf-8');
 
                $this->message->setHtmlBody('<blink>Fancy Body</blink>');
        }
 
        public function testPlainTextRenderOption() {
-               /** @var \PHPUnit\Framework\MockObject\MockObject|Swift_Message $swiftMessage */
-               $swiftMessage = $this->getMockBuilder('\Swift_Message')
+               /** @var MockObject|Email $symfonyEmail */
+               $symfonyEmail = $this->getMockBuilder(Email::class)
                        ->disableOriginalConstructor()->getMock();
-               /** @var \PHPUnit\Framework\MockObject\MockObject|IEMailTemplate $template */
-               $template = $this->getMockBuilder('\OCP\Mail\IEMailTemplate')
+               /** @var MockObject|IEMailTemplate $template */
+               $template = $this->getMockBuilder(IEMailTemplate::class)
                        ->disableOriginalConstructor()->getMock();
 
-               $message = new Message($swiftMessage, true);
+               $message = new Message($symfonyEmail, true);
 
                $template
                        ->expects($this->never())
@@ -243,14 +233,14 @@ class MessageTest extends TestCase {
        }
 
        public function testBothRenderingOptions() {
-               /** @var \PHPUnit\Framework\MockObject\MockObject|Swift_Message $swiftMessage */
-               $swiftMessage = $this->getMockBuilder('\Swift_Message')
+               /** @var MockObject|Email $symfonyEmail */
+               $symfonyEmail = $this->getMockBuilder(Email::class)
                        ->disableOriginalConstructor()->getMock();
-               /** @var \PHPUnit\Framework\MockObject\MockObject|IEMailTemplate $template */
-               $template = $this->getMockBuilder('\OCP\Mail\IEMailTemplate')
+               /** @var MockObject|IEMailTemplate $template */
+               $template = $this->getMockBuilder(IEMailTemplate::class)
                        ->disableOriginalConstructor()->getMock();
 
-               $message = new Message($swiftMessage, false);
+               $message = new Message($symfonyEmail, false);
 
                $template
                        ->expects($this->once())
@@ -266,108 +256,40 @@ class MessageTest extends TestCase {
        }
 
        public function testSetAutoSubmitted1() {
-               $swiftMimeSimpleHeaderSet = $this->getMockBuilder('\Swift_Mime_SimpleHeaderSet')
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $swiftMessage = $this->getMockBuilder('\Swift_Message')
-                       ->disableOriginalConstructor()
-                       ->disableOriginalClone()
-                       ->disableArgumentCloning()
-                       ->disallowMockingUnknownTypes()
-                       ->getMock();
-
-               $swiftMessage->method('getHeaders')->willReturn($swiftMimeSimpleHeaderSet);
-
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('has')
-                       ->with('Auto-Submitted');
-               $swiftMimeSimpleHeaderSet->expects($this->never())
-                       ->method('remove');
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('addTextHeader')
-                       ->with('Auto-Submitted', AutoSubmitted::VALUE_AUTO_GENERATED);
-
-               $message = new Message($swiftMessage, false);
+               $headers = new Headers($this->createMock(HeaderInterface::class));
+               $headers->addTextHeader(AutoSubmitted::HEADER, "yes");
+               $symfonyEmail = $this->createMock(Email::class);
+
+               $symfonyEmail->method('getHeaders')
+                       ->willReturn($headers);
+
+               $message = new Message($symfonyEmail, false);
                $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
+               $this->assertNotSame('no', $message->getAutoSubmitted());
        }
 
        public function testSetAutoSubmitted2() {
-               $swiftMimeSimpleHeaderSet = $this->getMockBuilder('\Swift_Mime_SimpleHeaderSet')
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $swiftMessage = $this->getMockBuilder('\Swift_Message')
-                       ->disableOriginalConstructor()
-                       ->disableOriginalClone()
-                       ->disableArgumentCloning()
-                       ->disallowMockingUnknownTypes()
-                       ->getMock();
-
-               $swiftMessage->method('getHeaders')->willReturn($swiftMimeSimpleHeaderSet);
-
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('has')
-                       ->with('Auto-Submitted')
-                       ->willReturn(true);
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('remove')
-                       ->with('Auto-Submitted');
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('addTextHeader')
-                       ->with('Auto-Submitted', AutoSubmitted::VALUE_AUTO_GENERATED);
-
-               $message = new Message($swiftMessage, false);
+               $headers = new Headers($this->createMock(HeaderInterface::class));
+               $headers->addTextHeader(AutoSubmitted::HEADER, 'no');
+               $symfonyEmail = $this->createMock(Email::class);
+
+               $symfonyEmail->method('getHeaders')
+                       ->willReturn($headers);
+
+               $message = new Message($symfonyEmail, false);
                $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
+               $this->assertSame('auto-generated', $message->getAutoSubmitted());
        }
 
-       public function testGetAutoSubmitted1() {
-               $swiftMimeSimpleHeaderSet = $this->getMockBuilder('\Swift_Mime_SimpleHeaderSet')
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $swiftMessage = $this->getMockBuilder('\Swift_Message')
-                       ->disableOriginalConstructor()
-                       ->disableOriginalClone()
-                       ->disableArgumentCloning()
-                       ->disallowMockingUnknownTypes()
-                       ->getMock();
-
-               $swiftMessage->method('getHeaders')->willReturn($swiftMimeSimpleHeaderSet);
-
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('has')
-                       ->with('Auto-Submitted');
-               $swiftMimeSimpleHeaderSet->expects($this->never())
-                       ->method('get');
-
-               $message = new Message($swiftMessage, false);
+       public function testGetAutoSubmitted() {
+               $headers = new Headers($this->createMock(HeaderInterface::class));
+               $headers->addTextHeader(AutoSubmitted::HEADER, 'no');
+               $symfonyEmail = $this->createMock(Email::class);
+
+               $symfonyEmail->method('getHeaders')
+                       ->willReturn($headers);
+
+               $message = new Message($symfonyEmail, false);
                $this->assertSame("no", $message->getAutoSubmitted());
        }
-       public function testGetAutoSubmitted2() {
-               $swiftMimeHeader = $this->getMockBuilder('\Swift_Mime_Header')
-                       ->disableOriginalConstructor()
-                       ->getMockForAbstractClass();
-               $swiftMimeSimpleHeaderSet = $this->getMockBuilder('\Swift_Mime_SimpleHeaderSet')
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $swiftMessage = $this->getMockBuilder('\Swift_Message')
-                       ->disableOriginalConstructor()
-                       ->disableOriginalClone()
-                       ->disableArgumentCloning()
-                       ->disallowMockingUnknownTypes()
-                       ->getMock();
-
-
-               $swiftMessage->method('getHeaders')->willReturn($swiftMimeSimpleHeaderSet);
-               $swiftMimeHeader->method('toString')->willReturn(AutoSubmitted::VALUE_AUTO_GENERATED);
-
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('has')
-                       ->with('Auto-Submitted')
-                       ->willReturn(true);
-               $swiftMimeSimpleHeaderSet->expects($this->once())
-                       ->method('get')
-                       ->willReturn($swiftMimeHeader);
-
-               $message = new Message($swiftMessage, false);
-               $this->assertSame(AutoSubmitted::VALUE_AUTO_GENERATED, $message->getAutoSubmitted());
-       }
 }