[!!!][FEATURE] Replace SwiftMailer with symfony/mailer and symfony/mime 52/61152/12
authorBenni Mack <benni@typo3.org>
Wed, 26 Jun 2019 05:24:29 +0000 (07:24 +0200)
committerGeorg Ringer <georg.ringer@gmail.com>
Thu, 4 Jul 2019 07:56:31 +0000 (09:56 +0200)
SwiftMailer is not in active development anymore
as the author created the successor symfony/mailer
and symfony/mime packages to create and send
emails.

This is a breaking change, as PHP mail() is not
supported anymore. This is now automatically
switched to "sendmail".

In addition \Symfony\Mime\Email has a different
signature than \Swift_Message, which will
cause some trouble, however we've managed
to overcome most of that functionality to
stay backwards-compatible.

Also, all extensions extending from SwiftMailer
will fail as the package is be removed
with this patch.

Spooling has been reimplemented as direct transport
methods (DelayedTransportInterface) instead of
Symfony Messaging for the time being.

Used composer commands:
 * composer require symfony/mailer symfony/mime
 * composer remove swiftmailer/swiftmailer

Resolves: #88643
Releases: master
Change-Id: Ic02db633392f1d0d7b7061e7b322435f892d2c04
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61152
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
34 files changed:
composer.json
composer.lock
typo3/sysext/backend/Classes/Security/EmailLoginNotification.php
typo3/sysext/core/Classes/Command/SendEmailCommand.php
typo3/sysext/core/Classes/Mail/DelayedTransportInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/Mail/FileSpool.php [new file with mode: 0644]
typo3/sysext/core/Classes/Mail/MailMessage.php
typo3/sysext/core/Classes/Mail/Mailer.php
typo3/sysext/core/Classes/Mail/MboxTransport.php
typo3/sysext/core/Classes/Mail/MemorySpool.php
typo3/sysext/core/Classes/Mail/TransportFactory.php
typo3/sysext/core/Configuration/Commands.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
typo3/sysext/core/Documentation/Changelog/master/Breaking-88643-RemovedSwiftmailerswiftmailerDependency.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-88643-NewMailAPIBasedOnSymfonymailerAndSymfonymime.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Acceptance/Support/Extension/BackendCoreEnvironment.php
typo3/sysext/core/Tests/Unit/Mail/Fixtures/FakeFileSpoolFixture.php
typo3/sysext/core/Tests/Unit/Mail/Fixtures/FakeInvalidSpoolFixture.php
typo3/sysext/core/Tests/Unit/Mail/Fixtures/FakeTransportFixture.php [deleted file]
typo3/sysext/core/Tests/Unit/Mail/Fixtures/FakeValidSpoolFixture.php
typo3/sysext/core/Tests/Unit/Mail/MailMessageTest.php [deleted file]
typo3/sysext/core/Tests/Unit/Mail/MailerTest.php
typo3/sysext/core/Tests/Unit/Mail/TransportFactoryTest.php
typo3/sysext/core/composer.json
typo3/sysext/form/Classes/Domain/Finishers/EmailFinisher.php
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/install/Classes/Authentication/AuthenticationService.php
typo3/sysext/install/Classes/Controller/EnvironmentController.php
typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php
typo3/sysext/install/Resources/Private/Templates/Environment/MailTest.html
typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php
typo3/sysext/reports/Classes/Task/SystemStatusUpdateTask.php
typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php

index 96d614c..d60e048 100644 (file)
                "psr/http-message": "~1.0",
                "psr/http-server-middleware": "^1.0",
                "psr/log": "~1.0.0",
-               "swiftmailer/swiftmailer": "~5.4.5",
                "symfony/console": "^4.1",
                "symfony/expression-language": "^4.1",
                "symfony/finder": "^4.1",
+               "symfony/mailer": "^4.3",
+               "symfony/mime": "^4.3",
                "symfony/polyfill-intl-icu": "^1.6",
                "symfony/polyfill-intl-idn": "^1.10",
                "symfony/polyfill-mbstring": "^1.2",
index dcf4965..92026b1 100644 (file)
@@ -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": "ab63943f41a231201a3e0b76fa2de7ef",
+    "content-hash": "7b9b7abce9b3a820cfda31f71cb538c2",
     "packages": [
         {
             "name": "cogpowered/finediff",
             "time": "2014-09-09T13:34:57+00:00"
         },
         {
+            "name": "egulias/email-validator",
+            "version": "2.1.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/egulias/EmailValidator.git",
+                "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/128cc721d771ec2c46ce59698f4ca42b73f71b25",
+                "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/lexer": "^1.0.1",
+                "php": ">= 5.5"
+            },
+            "require-dev": {
+                "dominicsayers/isemail": "dev-master",
+                "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
+                "satooshi/php-coveralls": "^1.0.1"
+            },
+            "suggest": {
+                "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Egulias\\EmailValidator\\": "EmailValidator"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Eduardo Gulias Davis"
+                }
+            ],
+            "description": "A library for validating emails against several RFCs",
+            "homepage": "https://github.com/egulias/EmailValidator",
+            "keywords": [
+                "email",
+                "emailvalidation",
+                "emailvalidator",
+                "validation",
+                "validator"
+            ],
+            "time": "2019-06-23T10:14:27+00:00"
+        },
+        {
             "name": "guzzlehttp/guzzle",
             "version": "6.3.3",
             "source": {
             "time": "2016-02-11T07:05:27+00:00"
         },
         {
-            "name": "swiftmailer/swiftmailer",
-            "version": "v5.4.10",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/swiftmailer/swiftmailer.git",
-                "reference": "dd71cc1638ed7aebbb33f2e2b0edd2cf6ea73d97"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/dd71cc1638ed7aebbb33f2e2b0edd2cf6ea73d97",
-                "reference": "dd71cc1638ed7aebbb33f2e2b0edd2cf6ea73d97",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "mockery/mockery": "~0.9.1",
-                "symfony/phpunit-bridge": "~3.2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.4-dev"
-                }
-            },
-            "autoload": {
-                "files": [
-                    "lib/swift_required.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Chris Corbyn"
-                },
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                }
-            ],
-            "description": "Swiftmailer, free feature-rich PHP mailer",
-            "homepage": "https://swiftmailer.symfony.com",
-            "keywords": [
-                "email",
-                "mail",
-                "mailer"
-            ],
-            "time": "2018-07-27T08:58:59+00:00"
-        },
-        {
             "name": "symfony/cache",
             "version": "v4.3.1",
             "source": {
             "time": "2019-06-05T13:25:51+00:00"
         },
         {
+            "name": "symfony/event-dispatcher",
+            "version": "v4.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher.git",
+                "reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4e6c670af81c4fb0b6c08b035530a9915d0b691f",
+                "reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "symfony/event-dispatcher-contracts": "^1.1"
+            },
+            "conflict": {
+                "symfony/dependency-injection": "<3.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "1.1"
+            },
+            "require-dev": {
+                "psr/log": "~1.0",
+                "symfony/config": "~3.4|~4.0",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/http-foundation": "^3.4|^4.0",
+                "symfony/service-contracts": "^1.1",
+                "symfony/stopwatch": "~3.4|~4.0"
+            },
+            "suggest": {
+                "symfony/dependency-injection": "",
+                "symfony/http-kernel": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\EventDispatcher\\": ""
+                },
+                "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": "Symfony EventDispatcher Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-05-30T16:10:05+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "8fa2cf2177083dd59cf8e44ea4b6541764fbda69"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8fa2cf2177083dd59cf8e44ea4b6541764fbda69",
+                "reference": "8fa2cf2177083dd59cf8e44ea4b6541764fbda69",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3"
+            },
+            "suggest": {
+                "psr/event-dispatcher": "",
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-05-22T12:23:29+00:00"
+        },
+        {
             "name": "symfony/expression-language",
             "version": "v4.3.1",
             "source": {
             "time": "2019-05-30T16:10:05+00:00"
         },
         {
+            "name": "symfony/mailer",
+            "version": "v4.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/mailer.git",
+                "reference": "623c5e5a8303a936a1a265dc08b488ac43977dce"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/mailer/zipball/623c5e5a8303a936a1a265dc08b488ac43977dce",
+                "reference": "623c5e5a8303a936a1a265dc08b488ac43977dce",
+                "shasum": ""
+            },
+            "require": {
+                "egulias/email-validator": "^2.0",
+                "php": "^7.1.3",
+                "psr/log": "~1.0",
+                "symfony/event-dispatcher": "^4.3",
+                "symfony/mime": "^4.3"
+            },
+            "require-dev": {
+                "symfony/amazon-mailer": "^4.3",
+                "symfony/google-mailer": "^4.3",
+                "symfony/http-client-contracts": "^1.1",
+                "symfony/mailchimp-mailer": "^4.3",
+                "symfony/mailgun-mailer": "^4.3.2",
+                "symfony/postmark-mailer": "^4.3",
+                "symfony/sendgrid-mailer": "^4.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "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": "Symfony Mailer Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-06-26T08:48:20+00:00"
+        },
+        {
+            "name": "symfony/mime",
+            "version": "v4.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/mime.git",
+                "reference": "ec2c5565de60e03f33d4296a655e3273f0ad1f8b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/ec2c5565de60e03f33d4296a655e3273f0ad1f8b",
+                "reference": "ec2c5565de60e03f33d4296a655e3273f0ad1f8b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "symfony/polyfill-intl-idn": "^1.10",
+                "symfony/polyfill-mbstring": "^1.0"
+            },
+            "require-dev": {
+                "egulias/email-validator": "^2.0",
+                "symfony/dependency-injection": "~3.4|^4.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Mime\\": ""
+                },
+                "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": "A library to manipulate MIME messages",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "mime",
+                "mime-type"
+            ],
+            "time": "2019-06-04T09:22:54+00:00"
+        },
+        {
             "name": "symfony/polyfill-ctype",
             "version": "v1.11.0",
             "source": {
             "time": "2019-05-31T18:55:30+00:00"
         },
         {
-            "name": "symfony/event-dispatcher",
-            "version": "v4.3.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4e6c670af81c4fb0b6c08b035530a9915d0b691f",
-                "reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1.3",
-                "symfony/event-dispatcher-contracts": "^1.1"
-            },
-            "conflict": {
-                "symfony/dependency-injection": "<3.4"
-            },
-            "provide": {
-                "psr/event-dispatcher-implementation": "1.0",
-                "symfony/event-dispatcher-implementation": "1.1"
-            },
-            "require-dev": {
-                "psr/log": "~1.0",
-                "symfony/config": "~3.4|~4.0",
-                "symfony/dependency-injection": "~3.4|~4.0",
-                "symfony/expression-language": "~3.4|~4.0",
-                "symfony/http-foundation": "^3.4|^4.0",
-                "symfony/service-contracts": "^1.1",
-                "symfony/stopwatch": "~3.4|~4.0"
-            },
-            "suggest": {
-                "symfony/dependency-injection": "",
-                "symfony/http-kernel": ""
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.3-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\EventDispatcher\\": ""
-                },
-                "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": "Symfony EventDispatcher Component",
-            "homepage": "https://symfony.com",
-            "time": "2019-05-30T16:10:05+00:00"
-        },
-        {
-            "name": "symfony/event-dispatcher-contracts",
-            "version": "v1.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
-                "reference": "8fa2cf2177083dd59cf8e44ea4b6541764fbda69"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8fa2cf2177083dd59cf8e44ea4b6541764fbda69",
-                "reference": "8fa2cf2177083dd59cf8e44ea4b6541764fbda69",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1.3"
-            },
-            "suggest": {
-                "psr/event-dispatcher": "",
-                "symfony/event-dispatcher-implementation": ""
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.1-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Contracts\\EventDispatcher\\": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Generic abstractions related to dispatching event",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "abstractions",
-                "contracts",
-                "decoupling",
-                "interfaces",
-                "interoperability",
-                "standards"
-            ],
-            "time": "2019-05-22T12:23:29+00:00"
-        },
-        {
             "name": "symfony/filesystem",
             "version": "v4.3.1",
             "source": {
index 0f9b6c1..6f376d9 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Security;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mime\Address;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Mail\MailMessage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -83,19 +84,19 @@ class EmailLoginNotification
     }
 
     /**
-     * Sends an email and returns the number of recipients accepting the email.
+     * Sends an email.
      *
      * @param string $recipient
      * @param string $subject
      * @param string $body
-     * @return int number of recipients that successfully accepted the email
      */
-    protected function sendEmail(string $recipient, string $subject, string $body): int
+    protected function sendEmail(string $recipient, string $subject, string $body): void
     {
-        return GeneralUtility::makeInstance(MailMessage::class)
-            ->setTo($recipient)
-            ->setSubject($subject)
-            ->setBody($body)
+        $recipients = explode(',', $recipient);
+        GeneralUtility::makeInstance(MailMessage::class)
+            ->to(...$recipients)
+            ->subject($subject)
+            ->text($body)
             ->send();
     }
 
index 53af870..33a18f0 100644 (file)
@@ -20,13 +20,15 @@ use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
+use TYPO3\CMS\Core\Mail\DelayedTransportInterface;
+use TYPO3\CMS\Core\Mail\FileSpool;
 use TYPO3\CMS\Core\Mail\Mailer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Command for sending spooled messages.
  *
- * Inspired and partially taken from symfony's swiftmailer package.
+ * Inspired and partially taken from symfony's swiftmailer package, adapted for Symfony/Mailer.
  *
  * @link https://github.com/symfony/swiftmailer-bundle/blob/master/Command/SendEmailCommand.php
  */
@@ -41,7 +43,8 @@ class SendEmailCommand extends Command
             ->setDescription('Sends emails from the spool')
             ->addOption('message-limit', null, InputOption::VALUE_REQUIRED, 'The maximum number of messages to send.')
             ->addOption('time-limit', null, InputOption::VALUE_REQUIRED, 'The time limit for sending messages (in seconds).')
-            ->addOption('recover-timeout', null, InputOption::VALUE_REQUIRED, 'The timeout for recovering messages that have taken too long to send (in seconds).');
+            ->addOption('recover-timeout', null, InputOption::VALUE_REQUIRED, 'The timeout for recovering messages that have taken too long to send (in seconds).')
+            ->setAliases(['swiftmailer:spool:send']);
     }
 
     /**
@@ -58,21 +61,18 @@ class SendEmailCommand extends Command
         $mailer = $this->getMailer();
 
         $transport = $mailer->getTransport();
-        if ($transport instanceof \Swift_Transport_SpoolTransport) {
-            $spool = $transport->getSpool();
-            if ($spool instanceof \Swift_ConfigurableSpool) {
-                $spool->setMessageLimit((int)$input->getOption('message-limit'));
-                $spool->setTimeLimit((int)$input->getOption('time-limit'));
-            }
-            if ($spool instanceof \Swift_FileSpool) {
+        if ($transport instanceof DelayedTransportInterface) {
+            if ($transport instanceof FileSpool) {
+                $transport->setMessageLimit((int)$input->getOption('message-limit'));
+                $transport->setTimeLimit((int)$input->getOption('time-limit'));
                 $recoverTimeout = (int)$input->getOption('recover-timeout');
                 if ($recoverTimeout) {
-                    $spool->recover($recoverTimeout);
+                    $transport->recover($recoverTimeout);
                 } else {
-                    $spool->recover();
+                    $transport->recover();
                 }
             }
-            $sent = $spool->flushQueue($mailer->getRealTransport());
+            $sent = $transport->flushQueue($mailer->getRealTransport());
             $io->comment($sent . ' emails sent');
         } else {
             $io->error('The Mailer Transport is not set to "spool".');
diff --git a/typo3/sysext/core/Classes/Mail/DelayedTransportInterface.php b/typo3/sysext/core/Classes/Mail/DelayedTransportInterface.php
new file mode 100644 (file)
index 0000000..fb18845
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Mail;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * Used to implement backwards-compatible spooling
+ */
+interface DelayedTransportInterface extends TransportInterface
+{
+    /**
+     * Sends messages using the given transport instance
+     *
+     * @param TransportInterface $transport
+     * @return int the number of messages sent
+     */
+    public function flushQueue(TransportInterface $transport): int;
+}
diff --git a/typo3/sysext/core/Classes/Mail/FileSpool.php b/typo3/sysext/core/Classes/Mail/FileSpool.php
new file mode 100644 (file)
index 0000000..21083f7
--- /dev/null
@@ -0,0 +1,243 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Mail;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use DirectoryIterator;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\Mailer\DelayedSmtpEnvelope;
+use Symfony\Component\Mailer\Exception\TransportException;
+use Symfony\Component\Mailer\SentMessage;
+use Symfony\Component\Mailer\SmtpEnvelope;
+use Symfony\Component\Mailer\Transport\AbstractTransport;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Message;
+use Symfony\Component\Mime\RawMessage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Inspired by SwiftMailer, adapted for TYPO3 and Symfony/Mailer
+ *
+ * @internal This class is experimental and subject to change!
+ */
+class FileSpool extends AbstractTransport implements DelayedTransportInterface, LoggerAwareInterface
+{
+    use LoggerAwareTrait;
+
+    /**
+     * The spool directory
+     * @var string
+     */
+    protected $path;
+
+    /**
+     * File WriteRetry Limit.
+     *
+     * @var int
+     */
+    protected $_retryLimit = 10;
+
+    /**
+     * The maximum number of messages to send per flush
+     * @var int
+     */
+    protected $messageLimit;
+
+    /**
+     * The time limit per flush
+     * @var int
+     */
+    protected $timeLimit;
+
+    /**
+     * Create a new FileSpool.
+     *
+     * @param string $path
+     */
+    public function __construct(string $path)
+    {
+        parent::__construct();
+        $this->path = $path;
+
+        if (!file_exists($this->path)) {
+            GeneralUtility::mkdir_deep($this->path);
+        }
+    }
+
+    /**
+     * Stores a message in the queue.
+     * @param SentMessage $message
+     */
+    protected function doSend(SentMessage $message): void
+    {
+        $ser = serialize($message);
+        $fileName = $this->path . '/' . $this->getRandomString(10);
+        for ($i = 0; $i < $this->_retryLimit; ++$i) {
+            // We try an exclusive creation of the file. This is an atomic operation, it avoid locking mechanism
+            $fp = @fopen($fileName . '.message', 'x');
+            if (false !== $fp) {
+                if (false === fwrite($fp, $ser)) {
+                    throw new TransportException('Could not create file for spooling', 1561618885);
+                }
+                fclose($fp);
+            } else {
+                // The file already exists, we try a longer fileName
+                $fileName .= $this->getRandomString(1);
+            }
+        }
+    }
+
+    /**
+     * Allow to manage the enqueuing retry limit.
+     *
+     * Default, is ten and allows over 64^20 different fileNames
+     *
+     * @param int $limit
+     */
+    public function setRetryLimit(int $limit): void
+    {
+        $this->_retryLimit = $limit;
+    }
+
+    /**
+     * Execute a recovery if for any reason a process is sending for too long.
+     *
+     * @param int $timeout in second Defaults is for very slow smtp responses
+     */
+    public function recover(int $timeout = 900): void
+    {
+        foreach (new DirectoryIterator($this->path) as $file) {
+            $file = $file->getRealPath();
+
+            if (substr($file, -16) == '.message.sending') {
+                $lockedtime = filectime($file);
+                if ((time() - $lockedtime) > $timeout) {
+                    rename($file, substr($file, 0, -8));
+                }
+            }
+        }
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function flushQueue(TransportInterface $transport): int
+    {
+        $directoryIterator = new DirectoryIterator($this->path);
+
+        $count = 0;
+        $time = time();
+        foreach ($directoryIterator as $file) {
+            $file = $file->getRealPath();
+
+            if (substr($file, -8) != '.message') {
+                continue;
+            }
+
+            /* We try a rename, it's an atomic operation, and avoid locking the file */
+            if (rename($file, $file . '.sending')) {
+                $message = unserialize(file_get_contents($file . '.sending'), [
+                    'allowedClasses' => [
+                        RawMessage::class,
+                        Message::class,
+                        Email::class,
+                        DelayedSmtpEnvelope::class,
+                        SmtpEnvelope::class,
+                    ],
+                ]);
+
+                $transport->send($message);
+                $count++;
+
+                unlink($file . '.sending');
+            } else {
+                /* This message has just been catched by another process */
+                continue;
+            }
+
+            if ($this->getMessageLimit() && $count >= $this->getMessageLimit()) {
+                break;
+            }
+
+            if ($this->getTimeLimit() && ($GLOBALS['EXEC_TIME'] - $time) >= $this->getTimeLimit()) {
+                break;
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Returns a random string needed to generate a fileName for the queue.
+     *
+     * @param int $count
+     *
+     * @return string
+     */
+    protected function getRandomString(int $count): string
+    {
+        // This string MUST stay FS safe, avoid special chars
+        $base = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
+        $ret = '';
+        $strlen = strlen($base);
+        for ($i = 0; $i < $count; ++$i) {
+            $ret .= $base[((int)rand(0, $strlen - 1))];
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Sets the maximum number of messages to send per flush.
+     *
+     * @param int $limit
+     */
+    public function setMessageLimit(int $limit): void
+    {
+        $this->messageLimit = (int)$limit;
+    }
+
+    /**
+     * Gets the maximum number of messages to send per flush.
+     *
+     * @return int The limit
+     */
+    public function getMessageLimit(): int
+    {
+        return $this->messageLimit;
+    }
+
+    /**
+     * Sets the time limit (in seconds) per flush.
+     *
+     * @param int $limit The limit
+     */
+    public function setTimeLimit(int $limit): void
+    {
+        $this->timeLimit = (int)$limit;
+    }
+
+    /**
+     * Gets the time limit (in seconds) per flush.
+     *
+     * @return int The limit
+     */
+    public function getTimeLimit(): int
+    {
+        return $this->timeLimit;
+    }
+}
index 39be762..8697296 100644 (file)
@@ -14,16 +14,20 @@ namespace TYPO3\CMS\Core\Mail;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Utility\HttpUtility;
-use TYPO3\CMS\Core\Utility\MailUtility;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\NamedAddress;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
- * Adapter for Swift_Mailer to be used by TYPO3 extensions
+ * Adapter for Symfony Mime to be used by TYPO3 extensions, also provides
+ * some backwards-compatibility for previous TYPO3 installations where
+ * send() was baked into the MailMessage object.
  */
-class MailMessage extends \Swift_Message
+class MailMessage extends Email
 {
     /**
-     * @var \TYPO3\CMS\Core\Mail\Mailer
+     * @var Mailer
      */
     protected $mailer;
 
@@ -40,40 +44,30 @@ class MailMessage extends \Swift_Message
     protected $sent = false;
 
     /**
-     * Holds the failed recipients after the message has been sent
-     *
-     * @var array
-     */
-    protected $failedRecipients = [];
-
-    /**
      */
     private function initializeMailer()
     {
-        $this->mailer = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\Mailer::class);
+        $this->mailer = GeneralUtility::makeInstance(Mailer::class);
     }
 
     /**
      * Sends the message.
      *
-     * @return int the number of recipients who were accepted for delivery
+     * This is a short-hand method. It is however more useful to create
+     * a Mailer instance which can be used via Mailer->send($message);
+     *
+     * @return bool whether the message was accepted or not
      */
     public function send()
     {
-        // Ensure to always have a From: header set
-        if (empty($this->getFrom())) {
-            $this->setFrom(MailUtility::getSystemFrom());
-        }
-        if (empty($this->getReplyTo())) {
-            $replyTo = MailUtility::getSystemReplyTo();
-            if (!empty($replyTo)) {
-                $this->setReplyTo($replyTo);
-            }
-        }
         $this->initializeMailer();
-        $this->sent = true;
-        $this->getHeaders()->addTextHeader('X-Mailer', $this->mailerHeader);
-        return $this->mailer->send($this, $this->failedRecipients);
+        $this->sent = false;
+        $this->mailer->send($this);
+        $sentMessage = $this->mailer->getSentMessage();
+        if ($sentMessage) {
+            $this->sent = true;
+        }
+        return $this->sent;
     }
 
     /**
@@ -87,25 +81,42 @@ class MailMessage extends \Swift_Message
     }
 
     /**
-     * Returns the recipients for which the mail was not accepted for delivery.
+     * compatibility methods to allow for associative arrays as [name => email address]
+     * as it was possible in TYPO3 v9 / SwiftMailer.
+     *
+     * Also, ensure to switch to NamedAddress objects and the ->subject()/->from() methods directly
+     * to directly use the new API.
+     */
+
+    /**
+     * Set the subject of the message.
      *
-     * @return array the recipients who were not accepted for delivery
+     * @param string $subject
      */
-    public function getFailedRecipients()
+    public function setSubject($subject)
     {
-        return $this->failedRecipients;
+        $this->subject($subject);
+    }
+
+    /**
+     * Set the origination date of the message as a UNIX timestamp.
+     *
+     * @param int $date
+     */
+    public function setDate($date)
+    {
+        $this->date(new \DateTime('@' . $date));
     }
 
     /**
      * Set the return-path (the bounce address) of this message.
      *
      * @param string $address
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setReturnPath($address)
     {
-        $address = $this->idnaEncodeAddresses($address);
-        return parent::setReturnPath($address);
+        return $this->returnPath($address);
     }
 
     /**
@@ -115,12 +126,12 @@ class MailMessage extends \Swift_Message
      *
      * @param string $address
      * @param string $name optional
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setSender($address, $name = null)
     {
-        $address = $this->idnaEncodeAddresses($address);
-        return parent::setSender($address, $name);
+        $address = $this->convertNamedAddress($address, $name);
+        return $this->sender($address);
     }
 
     /**
@@ -133,12 +144,12 @@ class MailMessage extends \Swift_Message
      *
      * @param string|array $addresses
      * @param string $name optional
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setFrom($addresses, $name = null)
     {
-        $addresses = $this->idnaEncodeAddresses($addresses);
-        return parent::setFrom($addresses, $name);
+        $addresses = $this->convertNamedAddress($addresses, $name);
+        return $this->from($addresses, $name);
     }
 
     /**
@@ -151,12 +162,12 @@ class MailMessage extends \Swift_Message
      *
      * @param string|array $addresses
      * @param string $name optional
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setReplyTo($addresses, $name = null)
     {
-        $addresses = $this->idnaEncodeAddresses($addresses);
-        return parent::setReplyTo($addresses, $name);
+        $addresses = $this->convertNamedAddress($addresses, $name);
+        return $this->replyTo($addresses);
     }
 
     /**
@@ -170,12 +181,12 @@ class MailMessage extends \Swift_Message
      *
      * @param string|array $addresses
      * @param string $name optional
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setTo($addresses, $name = null)
     {
-        $addresses = $this->idnaEncodeAddresses($addresses);
-        return parent::setTo($addresses, $name);
+        $addresses = $this->convertNamedAddress($addresses, $name);
+        return $this->to($addresses);
     }
 
     /**
@@ -186,12 +197,12 @@ class MailMessage extends \Swift_Message
      *
      * @param string|array $addresses
      * @param string $name optional
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setCc($addresses, $name = null)
     {
-        $addresses = $this->idnaEncodeAddresses($addresses);
-        return parent::setCc($addresses, $name);
+        $addresses = $this->convertNamedAddress($addresses, $name);
+        return $this->cc($addresses);
     }
 
     /**
@@ -202,43 +213,60 @@ class MailMessage extends \Swift_Message
      *
      * @param string|array $addresses
      * @param string $name optional
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setBcc($addresses, $name = null)
     {
-        $addresses = $this->idnaEncodeAddresses($addresses);
-        return parent::setBcc($addresses, $name);
+        $addresses = $this->convertNamedAddress($addresses, $name);
+        return $this->bcc($addresses);
     }
 
     /**
      * Ask for a delivery receipt from the recipient to be sent to $addresses.
      *
      * @param array $addresses
-     * @return \TYPO3\CMS\Core\Mail\MailMessage
+     * @return MailMessage
      */
     public function setReadReceiptTo($addresses)
     {
-        $addresses = $this->idnaEncodeAddresses($addresses);
-        return parent::setReadReceiptTo($addresses);
+        $addresses = $this->convertNamedAddress($addresses);
+        return $this->setReadReceiptTo($addresses);
+    }
+
+    /**
+     * Converts Adresses into Address/NamedAddress objects.
+     *
+     * @param string|array $args
+     * @return string|array
+     */
+    protected function convertNamedAddress(...$args)
+    {
+        if (isset($args[1])) {
+            return new NamedAddress($args[0], $args[1]);
+        }
+        if (is_string($args[0]) || is_array($args[0])) {
+            return $this->convertAddresses($args[0]);
+        }
+        return $this->convertAddresses($args);
     }
 
     /**
-     * IDNA encode email addresses. Accepts addresses in all formats that SwiftMailer supports
+     * Converts Adresses into Address/NamedAddress objects.
      *
      * @param string|array $addresses
      * @return string|array
      */
-    protected function idnaEncodeAddresses($addresses)
+    protected function convertAddresses($addresses)
     {
         if (!is_array($addresses)) {
-            return $this->idnaEncodeAddress($addresses);
+            return Address::create($addresses);
         }
         $newAddresses = [];
         foreach ($addresses as $email => $name) {
-            if (ctype_digit($email)) {
-                $newAddresses[] = $this->idnaEncodeAddress($name);
+            if (is_numeric($email) || ctype_digit($email)) {
+                $newAddresses[] = Address::create($name);
             } else {
-                $newAddresses[$this->idnaEncodeAddress($email)] = $name;
+                $newAddresses[] = new NamedAddress($email, $name);
             }
         }
 
@@ -246,28 +274,52 @@ class MailMessage extends \Swift_Message
     }
 
     /**
-     * IDNA encode the domain part of an email address if it contains non ASCII characters
-     *
-     * @param mixed $email
-     * @return mixed
-     * @see \TYPO3\CMS\Core\Utility\GeneralUtility::validEmail
+     * compatibility methods to allow for associative arrays as [name => email address]
+     * as it was possible in TYPO3 v9 / SwiftMailer.
      */
-    protected function idnaEncodeAddress($email)
+
+    /**
+     * @inheritdoc
+     */
+    public function addFrom(...$addresses)
     {
-        // Early return in case input is not a string
-        if (!is_string($email)) {
-            return $email;
-        }
-        // Split on the last "@" since addresses like "foo@bar"@example.org are valid
-        $atPosition = strrpos($email, '@');
-        if (!$atPosition || $atPosition + 1 === strlen($email)) {
-            // Return if no @ found or it is placed at the very beginning or end of the email
-            return $email;
-        }
-        $domain = substr($email, $atPosition + 1);
-        $local = substr($email, 0, $atPosition);
-        $domain = (string)HttpUtility::idn_to_ascii($domain);
+        $addresses = $this->convertNamedAddress(...$addresses);
+        return parent::addFrom(...$addresses);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function addReplyTo(...$addresses)
+    {
+        $addresses = $this->convertNamedAddress(...$addresses);
+        return parent::addReplyTo(...$addresses);
+    }
 
-        return $local . '@' . $domain;
+    /**
+     * @inheritdoc
+     */
+    public function addTo(...$addresses)
+    {
+        $addresses = $this->convertNamedAddress(...$addresses);
+        return parent::addTo(...$addresses);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function addCc(...$addresses)
+    {
+        $addresses = $this->convertNamedAddress(...$addresses);
+        return parent::addCc(...$addresses);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function addBcc(...$addresses)
+    {
+        $addresses = $this->convertNamedAddress(...$addresses);
+        return parent::addBcc(...$addresses);
     }
 }
index 98d09c2..cc45131 100644 (file)
@@ -14,21 +14,30 @@ namespace TYPO3\CMS\Core\Mail;
  * The TYPO3 project - inspiring people to share!
  */
 
-use Swift_Transport;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mailer\SentMessage;
+use Symfony\Component\Mailer\SmtpEnvelope;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\NamedAddress;
+use Symfony\Component\Mime\RawMessage;
+use TYPO3\CMS\Core\Exception as CoreException;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MailUtility;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
 
 /**
- * Adapter for Swift_Mailer to be used by TYPO3 extensions.
+ * Adapter for Symfony/Mailer to be used by TYPO3 extensions.
  *
  * This will use the setting in TYPO3_CONF_VARS to choose the correct transport
  * for it to work out-of-the-box.
  */
-class Mailer extends \Swift_Mailer
+class Mailer implements MailerInterface
 {
     /**
-     * @var \Swift_Transport
+     * @var TransportInterface
      */
     protected $transport;
 
@@ -38,12 +47,22 @@ class Mailer extends \Swift_Mailer
     protected $mailSettings = [];
 
     /**
-     * When constructing, also initializes the \Swift_Transport like configured
+     * @var SentMessage|null
+     */
+    protected $sentMessage;
+
+    /**
+     * @var string This will be added as X-Mailer to all outgoing mails
+     */
+    protected $mailerHeader = 'TYPO3';
+
+    /**
+     * When constructing, also initializes the Symfony Transport like configured
      *
-     * @param \Swift_Transport|null $transport optionally pass a transport to the constructor.
-     * @throws \TYPO3\CMS\Core\Exception
+     * @param TransportInterface|null $transport optionally pass a transport to the constructor.
+     * @throws CoreException
      */
-    public function __construct(\Swift_Transport $transport = null)
+    public function __construct(TransportInterface $transport = null)
     {
         if ($transport !== null) {
             $this->transport = $transport;
@@ -54,19 +73,64 @@ class Mailer extends \Swift_Mailer
             try {
                 $this->initializeTransport();
             } catch (\Exception $e) {
-                throw new \TYPO3\CMS\Core\Exception($e->getMessage(), 1291068569);
+                throw new CoreException($e->getMessage(), 1291068569);
             }
         }
-        parent::__construct($this->transport);
-
         $this->emitPostInitializeMailerSignal();
     }
 
     /**
+     * @inheritdoc
+     */
+    public function send(RawMessage $message, SmtpEnvelope $envelope = null): void
+    {
+        if ($message instanceof Email) {
+            // Ensure to always have a From: header set
+            if (empty($message->getFrom())) {
+                $address = MailUtility::getSystemFromAddress();
+                if ($address) {
+                    $name = MailUtility::getSystemFromName();
+                    if ($name) {
+                        $from = new NamedAddress($address, $name);
+                    } else {
+                        $from = new Address($address);
+                    }
+                    $message->from($from);
+                }
+            }
+            if (empty($message->getReplyTo())) {
+                $replyTo = MailUtility::getSystemReplyTo();
+                if (!empty($replyTo)) {
+                    $address = key($replyTo);
+                    if ($address === 0) {
+                        $replyTo = new Address($replyTo[$address]);
+                    } else {
+                        $replyTo = new NamedAddress(reset($replyTo), $address);
+                    }
+                    $message->replyTo($replyTo);
+                }
+            }
+            $message->getHeaders()->addTextHeader('X-Mailer', $this->mailerHeader);
+        }
+
+        $this->sentMessage = $this->transport->send($message, $envelope);
+    }
+
+    public function getSentMessage(): ?SentMessage
+    {
+        return $this->sentMessage;
+    }
+
+    public function getTransport(): TransportInterface
+    {
+        return $this->transport;
+    }
+
+    /**
      * Prepares a transport using the TYPO3_CONF_VARS configuration
      *
      * Used options:
-     * $TYPO3_CONF_VARS['MAIL']['transport'] = 'smtp' | 'sendmail' | 'mail' | 'mbox'
+     * $TYPO3_CONF_VARS['MAIL']['transport'] = 'smtp' | 'sendmail' | 'null' | 'mbox'
      *
      * $TYPO3_CONF_VARS['MAIL']['transport_smtp_server'] = 'smtp.example.org';
      * $TYPO3_CONF_VARS['MAIL']['transport_smtp_port'] = '25';
@@ -76,7 +140,7 @@ class Mailer extends \Swift_Mailer
      *
      * $TYPO3_CONF_VARS['MAIL']['transport_sendmail_command'] = '/usr/sbin/sendmail -bs'
      *
-     * @throws \TYPO3\CMS\Core\Exception
+     * @throws CoreException
      * @throws \RuntimeException
      */
     private function initializeTransport()
@@ -102,9 +166,9 @@ class Mailer extends \Swift_Mailer
     /**
      * Returns the real transport (not a spool).
      *
-     * @return \Swift_Transport
+     * @return TransportInterface
      */
-    public function getRealTransport(): Swift_Transport
+    public function getRealTransport(): TransportInterface
     {
         $mailSettings = !empty($this->mailSettings) ? $this->mailSettings : (array)$GLOBALS['TYPO3_CONF_VARS']['MAIL'];
         unset($mailSettings['transport_spool_type']);
@@ -122,7 +186,7 @@ class Mailer extends \Swift_Mailer
     /**
      * Get the object manager
      *
-     * @return \TYPO3\CMS\Extbase\Object\ObjectManager
+     * @return ObjectManager
      */
     protected function getObjectManager()
     {
@@ -132,7 +196,7 @@ class Mailer extends \Swift_Mailer
     /**
      * Get the SignalSlot dispatcher
      *
-     * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
+     * @return Dispatcher
      */
     protected function getSignalSlotDispatcher()
     {
@@ -144,6 +208,6 @@ class Mailer extends \Swift_Mailer
      */
     protected function emitPostInitializeMailerSignal()
     {
-        $this->getSignalSlotDispatcher()->dispatch('TYPO3\\CMS\\Core\\Mail\\Mailer', 'postInitializeMailer', [$this]);
+        $this->getSignalSlotDispatcher()->dispatch(self::class, 'postInitializeMailer', [$this]);
     }
 }
index adf832f..edc53f9 100644 (file)
@@ -14,122 +14,56 @@ namespace TYPO3\CMS\Core\Mail;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mailer\SentMessage;
+use Symfony\Component\Mailer\Transport\AbstractTransport;
 use TYPO3\CMS\Core\Locking\LockFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
- * Adapter for Swift_Mailer to be used by TYPO3 extensions.
- *
- * This will use the setting in TYPO3_CONF_VARS to choose the correct transport
- * for it to work out-of-the-box.
+ * Additional Mbox Transport option
  */
-class MboxTransport implements \Swift_Transport
+class MboxTransport extends AbstractTransport
 {
     /**
      * @var string The file to write our mails into
      */
-    private $debugFile;
+    private $mboxFile;
 
     /**
      * Create a new MailTransport
      *
-     * @param string $debugFile
-     */
-    public function __construct($debugFile)
-    {
-        $this->debugFile = $debugFile;
-    }
-
-    /**
-     * Not used.
+     * @param string $mboxFile
      */
-    public function isStarted()
-    {
-        return false;
-    }
-
-    /**
-     * Not used.
-     */
-    public function start()
-    {
-    }
-
-    /**
-     * Not used.
-     */
-    public function stop()
+    public function __construct($mboxFile)
     {
+        parent::__construct();
+        $this->mboxFile = $mboxFile;
+        $this->setMaxPerSecond(0);
     }
 
     /**
      * Outputs the mail to a text file according to RFC 4155.
      *
-     * @param \Swift_Mime_Message $message The message to send
-     * @param string[] &$failedRecipients To collect failures by-reference, nothing will fail in our debugging case
-     * @return int
-     * @throws \RuntimeException
+     * @param SentMessage $message
+     * @throws \TYPO3\CMS\Core\Locking\Exception\LockAcquireException
+     * @throws \TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException
+     * @throws \TYPO3\CMS\Core\Locking\Exception\LockCreateException
      */
-    public function send(\Swift_Mime_Message $message, &$failedRecipients = null)
+    protected function doSend(SentMessage $message): void
     {
-        $message->generateId();
-        // Create a mbox-like header
-        $mboxFrom = $this->getReversePath($message);
-        $mboxDate = strftime('%c', $message->getDate());
-        $messageStr = sprintf('From %s  %s', $mboxFrom, $mboxDate) . LF;
         // Add the complete mail inclusive headers
-        $messageStr .= $message->toString();
-        $messageStr .= LF . LF;
         $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
         $lockObject = $lockFactory->createLocker('mbox');
         $lockObject->acquire();
         // Write the mbox file
-        $file = @fopen($this->debugFile, 'a');
+        $file = @fopen($this->mboxFile, 'a');
         if (!$file) {
             $lockObject->release();
-            throw new \RuntimeException(sprintf('Could not write to file "%s" when sending an email to debug transport', $this->debugFile), 1291064151);
+            throw new \RuntimeException(sprintf('Could not write to file "%s" when sending an email to debug transport', $this->mboxFile), 1291064151);
         }
-        @fwrite($file, $messageStr);
+        @fwrite($file, $message->toString());
         @fclose($file);
-        GeneralUtility::fixPermissions($this->debugFile);
+        GeneralUtility::fixPermissions($this->mboxFile);
         $lockObject->release();
-        // Return every recipient as "delivered"
-        $count = count((array)$message->getTo()) + count((array)$message->getCc()) + count((array)$message->getBcc());
-        return $count;
-    }
-
-    /**
-     * Determine the best-use reverse path for this message
-     *
-     * @param \Swift_Mime_Message $message
-     * @return mixed|null
-     */
-    private function getReversePath(\Swift_Mime_Message $message)
-    {
-        $return = $message->getReturnPath();
-        $sender = $message->getSender();
-        $from = $message->getFrom();
-        $path = null;
-        if (!empty($return)) {
-            $path = $return;
-        } elseif (!empty($sender)) {
-            $keys = array_keys($sender);
-            $path = array_shift($keys);
-        } elseif (!empty($from)) {
-            $keys = array_keys($from);
-            $path = array_shift($keys);
-        }
-        return $path;
-    }
-
-    /**
-     * Register a plugin in the Transport.
-     *
-     * @param \Swift_Events_EventListener $plugin
-     * @return bool
-     */
-    public function registerPlugin(\Swift_Events_EventListener $plugin)
-    {
-        return true;
     }
 }
index 6273a8c..443cef9 100644 (file)
@@ -17,12 +17,14 @@ namespace TYPO3\CMS\Core\Mail;
 
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
+use Symfony\Component\Mailer\SentMessage;
+use Symfony\Component\Mailer\Transport\AbstractTransport;
+use Symfony\Component\Mailer\Transport\TransportInterface;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
- * Small wrapper for \Swift_MemorySpool
- *
  * Because TYPO3 doesn't offer a terminate signal or hook,
  * and taking in account the risk that extensions do some redirects or even exit,
  * we simply use the destructor of a singleton class which should be pretty much
@@ -32,25 +34,76 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  *
  * @internal This class is experimental and subject to change!
  */
-class MemorySpool extends \Swift_MemorySpool implements SingletonInterface, LoggerAwareInterface
+class MemorySpool extends AbstractTransport implements SingletonInterface, LoggerAwareInterface, DelayedTransportInterface
 {
     use LoggerAwareTrait;
 
     /**
+     * @var SentMessage[]
+     */
+    protected $queuedMessages;
+
+    /**
+     * Maximum number of retries when the real transport has failed.
+     *
+     * @var int
+     */
+    protected $retries = 3;
+
+    /**
      * Sends out the messages in the memory
      */
-    public function sendMessages()
+    public function __destruct()
     {
         $mailer = GeneralUtility::makeInstance(Mailer::class);
         try {
             $this->flushQueue($mailer->getRealTransport());
-        } catch (\Swift_TransportException $exception) {
+        } catch (TransportExceptionInterface $exception) {
             $this->logger->error('An Exception occurred while flushing email queue: ' . $exception->getMessage());
         }
     }
 
-    public function __destruct()
+    /**
+     * @inheritdoc
+     */
+    public function flushQueue(TransportInterface $transport): int
+    {
+        if (!$this->queuedMessages) {
+            return 0;
+        }
+
+        $retries = $this->retries;
+        $message = null;
+        $count = 0;
+        while ($retries--) {
+            try {
+                while ($message = array_pop($this->queuedMessages)) {
+                    $transport->send($message->getMessage(), $message->getEnvelope());
+                    $count++;
+                }
+            } catch (TransportExceptionInterface $exception) {
+                if ($retries && $message) {
+                    // re-queue the message at the end of the queue to give a chance
+                    // to the other messages to be sent, in case the failure was due to
+                    // this message and not just the transport failing
+                    array_unshift($this->queuedMessages, $message);
+
+                    // wait half a second before we try again
+                    usleep(500000);
+                } else {
+                    throw $exception;
+                }
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Stores a message in the queue.
+     * @param SentMessage $message
+     */
+    protected function doSend(SentMessage $message): void
     {
-        $this->sendMessages();
+        $this->queuedMessages[] = $message;
     }
 }
index dc6c646..7caa2dc 100644 (file)
@@ -15,6 +15,13 @@ namespace TYPO3\CMS\Core\Mail;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\Mailer\Transport;
+use Symfony\Component\Mailer\Transport\NullTransport;
+use Symfony\Component\Mailer\Transport\SendmailTransport;
+use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
+use Symfony\Component\Mailer\Transport\TransportInterface;
 use TYPO3\CMS\Core\Exception;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -22,8 +29,10 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 /**
  * TransportFactory
  */
-class TransportFactory implements SingletonInterface
+class TransportFactory implements SingletonInterface, LoggerAwareInterface
 {
+    use LoggerAwareTrait;
+
     const SPOOL_MEMORY = 'memory';
     const SPOOL_FILE = 'file';
 
@@ -31,11 +40,11 @@ class TransportFactory implements SingletonInterface
      * Gets a transport from settings.
      *
      * @param array $mailSettings from $GLOBALS['TYPO3_CONF_VARS']['MAIL']
-     * @return \Swift_Transport
+     * @return TransportInterface
      * @throws Exception
      * @throws \RuntimeException
      */
-    public function get(array $mailSettings): \Swift_Transport
+    public function get(array $mailSettings): TransportInterface
     {
         if (!isset($mailSettings['transport'])) {
             throw new \InvalidArgumentException('Key "transport" must be set in the mail settings', 1469363365);
@@ -49,7 +58,7 @@ class TransportFactory implements SingletonInterface
 
         switch ($transportType) {
             case 'spool':
-                $transport = \Swift_SpoolTransport::newInstance($this->createSpool($mailSettings));
+                $transport = $this->createSpool($mailSettings);
                 break;
             case 'smtp':
                 // Get settings to be used when constructing the transport object
@@ -67,10 +76,12 @@ class TransportFactory implements SingletonInterface
                 }
                 if ($port === null || $port === '') {
                     $port = 25;
+                } else {
+                    $port = (int)$port;
                 }
-                $useEncryption = $mailSettings['transport_smtp_encrypt'] ?? null;
-                // Create our transport
-                $transport = \Swift_SmtpTransport::newInstance($host, $port, $useEncryption);
+                $useEncryption = ($mailSettings['transport_smtp_encrypt'] ?? '') ?: null;
+                // Create transport
+                $transport = new EsmtpTransport($host, $port, $useEncryption);
                 // Need authentication?
                 $username = (string)($mailSettings['transport_smtp_username'] ?? '');
                 if ($username !== '') {
@@ -82,12 +93,13 @@ class TransportFactory implements SingletonInterface
                 }
                 break;
             case 'sendmail':
-                $sendmailCommand = $mailSettings['transport_sendmail_command'];
+                $sendmailCommand = $mailSettings['transport_sendmail_command'] ?? @ini_get('sendmail_path');
                 if (empty($sendmailCommand)) {
-                    throw new Exception('$GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'transport_sendmail_command\'] needs to be set when transport is set to "sendmail".', 1291068620);
+                    $sendmailCommand = '/usr/sbin/sendmail -bs';
+                    $this->logger->warning('Mailer transport "sendmail" was chosen without a specific command, using "' . $sendmailCommand . '"');
                 }
-                // Create our transport
-                $transport = \Swift_SendmailTransport::newInstance($sendmailCommand);
+                // Create transport
+                $transport = new SendmailTransport($sendmailCommand);
                 break;
             case 'mbox':
                 $mboxFile = $mailSettings['transport_mbox_file'];
@@ -97,17 +109,20 @@ class TransportFactory implements SingletonInterface
                 // Create our transport
                 $transport = GeneralUtility::makeInstance(MboxTransport::class, $mboxFile);
                 break;
-            case 'mail':
-                // Create the transport, no configuration required
-                $transport = \Swift_MailTransport::newInstance();
+                // Used for testing purposes
+            case 'null':
+            case NullTransport::class:
+                $transport = new NullTransport();
+                break;
+                // Used by Symfony's Transport Factory
+            case !empty($mailSettings['dsn']):
+                $transport = Transport::fromDsn($mailSettings['dsn']);
                 break;
             default:
                 // Custom mail transport
-                $customTransport = GeneralUtility::makeInstance($mailSettings['transport'], $mailSettings);
-                if ($customTransport instanceof \Swift_Transport) {
-                    $transport = $customTransport;
-                } else {
-                    throw new \RuntimeException($mailSettings['transport'] . ' is not an implementation of \\Swift_Transport,
+                $transport = GeneralUtility::makeInstance($mailSettings['transport'], $mailSettings);
+                if (!$transport instanceof TransportInterface) {
+                    throw new \RuntimeException($mailSettings['transport'] . ' is not an implementation of Symfony\Mailer\TransportInterface,
                             but must implement that interface to be used as a mail transport.', 1323006478);
                 }
         }
@@ -118,10 +133,10 @@ class TransportFactory implements SingletonInterface
      * Creates a spool from mail settings.
      *
      * @param array $mailSettings
-     * @return \Swift_Spool
+     * @return DelayedTransportInterface
      * @throws \RuntimeException
      */
-    protected function createSpool(array $mailSettings): \Swift_Spool
+    protected function createSpool(array $mailSettings): DelayedTransportInterface
     {
         $spool = null;
         switch ($mailSettings['transport_spool_type']) {
@@ -130,17 +145,16 @@ class TransportFactory implements SingletonInterface
                 if (empty($path) || !file_exists($path) || !is_writable($path)) {
                     throw new \RuntimeException('The Spool Type filepath must be available and writeable for TYPO3 in order to be used. Be sure that it\'s not accessible via the web.', 1518558797);
                 }
-                $spool = GeneralUtility::makeInstance(\Swift_FileSpool::class, $path);
+                $spool = GeneralUtility::makeInstance(FileSpool::class, $path);
                 break;
             case self::SPOOL_MEMORY:
                 $spool = GeneralUtility::makeInstance(MemorySpool::class);
                 break;
             default:
                 $spool = GeneralUtility::makeInstance($mailSettings['transport_spool_type'], $mailSettings);
-                if (!$spool instanceof \Swift_Spool) {
+                if (!($spool instanceof DelayedTransportInterface)) {
                     throw new \RuntimeException(
-                        $mailSettings['transport_spool_type'] . ' is not an implementation of \\Swift_Spool,
-                            but must implement that interface to be used as a mail spool.',
+                        $mailSettings['transport_spool_type'] . ' is not an implementation of DelayedTransportInterface, but must implement that interface to be used as a mail spool.',
                         1466799482
                     );
                 }
index c13b715..15028c8 100644 (file)
@@ -5,7 +5,7 @@ return [
         'class' => \TYPO3\CMS\Core\Command\DumpAutoloadCommand::class,
         'schedulable' => false,
     ],
-    'swiftmailer:spool:send' => [
+    'mailer:spool:send' => [
         'class' => \TYPO3\CMS\Core\Command\SendEmailCommand::class,
     ],
     'extension:list' => [
index 346f6e0..a5ddee1 100644 (file)
@@ -1311,7 +1311,7 @@ return [
         ],
     ],
     'MAIL' => [ // Mail configurations to tune how \TYPO3\CMS\Core\Mail\ classes will send their mails.
-        'transport' => 'mail',
+        'transport' => 'sendmail',
         'transport_smtp_server' => 'localhost:25',
         'transport_smtp_encrypt' => '',
         'transport_smtp_username' => '',
index f65dfac..189bf46 100644 (file)
@@ -495,7 +495,7 @@ MAIL:
     items:
         transport:
             type: text
-            description: '<dl><dt>mail</dt><dd>Sends messages by delegating to PHP''s internal mail() function. No further settings required. This is the most unreliable option. If you are serious about sending mails, consider using "smtp" or "sendmail".</dd><dt>smtp</dt><dd>Sends messages over the (standardized) Simple Message Transfer Protocol. It can deal with encryption and authentication. Most flexible option, requires a mail server and configurations in transport_smtp_* settings below. Works the same on Windows, Unix and MacOS.</dd><dt>sendmail</dt><dd>Sends messages by communicating with a locally installed MTA - such as sendmail. See setting transport_sendmail_command bellow.<dd><dt>mbox</dt><dd>This doesn''t send any mail out, but instead will write every outgoing mail to a file adhering to the RFC 4155 mbox format, which is a simple text file where the mails are concatenated. Useful for debugging the mail sending process and on development machines which cannot send mails to the outside. Configure the file to write to in the ''transport_mbox_file'' setting below</dd><dt>&lt;classname&gt;</dt><dd>Custom class which implements Swift_Transport. The constructor receives all settings from the MAIL section to make it possible to add custom settings.</dd></dl>'
+            description: '<dl><dt>smtp</dt><dd>Sends messages over the (standardized) Simple Message Transfer Protocol. It can deal with encryption and authentication. Most flexible option, requires a mail server and configurations in transport_smtp_* settings below. Works the same on Windows, Unix and MacOS.</dd><dt>sendmail</dt><dd>Sends messages by communicating with a locally installed MTA - such as sendmail. See setting transport_sendmail_command bellow.<dd><dt>mbox</dt><dd>This doesn''t send any mail out, but instead will write every outgoing mail to a file adhering to the RFC 4155 mbox format, which is a simple text file where the mails are concatenated. Useful for debugging the mail sending process and on development machines which cannot send mails to the outside. Configure the file to write to in the ''transport_mbox_file'' setting below</dd><dt>&lt;classname&gt;</dt><dd>Custom class which implements Swift_Transport. The constructor receives all settings from the MAIL section to make it possible to add custom settings.</dd></dl>'
         transport_smtp_server:
             type: text
             description: '<em>only with transport=smtp</em>: &lt;server:port> of mailserver to connect to. &lt;port> defaults to "25".'
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-88643-RemovedSwiftmailerswiftmailerDependency.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-88643-RemovedSwiftmailerswiftmailerDependency.rst
new file mode 100644 (file)
index 0000000..bc83eb9
--- /dev/null
@@ -0,0 +1,45 @@
+.. include:: ../../Includes.txt
+
+=============================================================
+Breaking: #88643 - Removed swiftmailer/swiftmailer dependency
+=============================================================
+
+See :issue:`88643`
+
+Description
+===========
+
+TYPO3's dependency swiftmailer has been removed in favor of new symfony-based
+components "mime" and "mailer".
+
+This means that all SwiftMailer-related PHP code has been removed.
+
+
+Impact
+======
+
+Custom SwiftMailer plugins or transports cannot be used without further
+migration anymore and will result in a fatal error.
+
+Using SwiftMailer-specific API by using TYPO3's MailMessage class might result
+in fatal errors as well when sending out emails.
+
+
+Affected Installations
+======================
+
+Any TYPO3 installation with third-party extension sending out emails or extending
+TYPO3's email sending capabilities.
+
+
+Migration
+=========
+
+Search the third-party extensions' code for occurrences of MailMessage or
+parts starting with `\Swift_` and migrate to symfony/mime or symfony/mailer
+APIs, which are included in TYPO3 v10.0.
+
+If required, SwiftMailer code can be installed via composer (when running TYPO3 via composer)
+via `composer require swiftmailer/swiftmailer`.
+
+.. index:: PHP-API, NotScanned, ext:core
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-88643-NewMailAPIBasedOnSymfonymailerAndSymfonymime.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-88643-NewMailAPIBasedOnSymfonymailerAndSymfonymime.rst
new file mode 100644 (file)
index 0000000..29f9b47
--- /dev/null
@@ -0,0 +1,82 @@
+.. include:: ../../Includes.txt
+
+=======================================================================
+Feature: #88643 - New Mail API based on symfony/mailer and symfony/mime
+=======================================================================
+
+See :issue:`88643`
+
+Description
+===========
+
+TYPO3 has relied on the third-party dependency "SwiftMailer" for a long time.
+
+However, the library has been superseded by the author in favor of new, more modern
+libraries "symfony/mailer" for sending emails and "symfony/mime" for creating email
+messages.
+
+TYPO3 has replaced swiftmailer with the symfony components.
+
+The new component does not handle the regular PHP function :php:`mail()` which
+has been declared unsafe in various scenarios anymore. Instead, it is recommended
+to switch to `sendmail` or `smtp`, which can be configured within the TYPO3
+Install Tool or the Settings module for System Maintainers under "Presets" => "Mail".
+
+All existing installations which still have "mail" are migrated to "sendmail"
+by automatically detecting the sendmail path by checking PHP.ini settings, but
+should be reviewed on update.
+
+In addition, the MailMessage API to create Email messages now inherits from
+:php:`Symfony\Mail\Email` instead of `Swift_Message`, and adds certain shortcuts
+and more flexibility, but is also stricter in validation.
+
+Especially custom extensions using the MailMessage API need to be evaluated,
+as it is not possible anymore to add multiple email addresses as a simple associative
+array but rather a NamedAddress object or a simple Address object from symfony/mime.
+
+All existing Swiftmailer-based transports which TYPO3 supports natively have been
+replaced by Symfony-based transport APIs.
+
+Spool-based transports are still experimental, as it might be replaced by a native
+Symfony component as well.
+
+
+Impact
+======
+
+The MailMessage API now has more possibilities to add multi-part files and attachments,
+for use in third-party extensions, but some APIs might be adapted.
+
+See the documentation of the Symfony components (https://symfony.com/doc/current/mailer.html)
+for further details on how to use the new Email class where TYPO3's MailMessage
+class extends from.
+
+An example implementation within a third-party extension:
+
+:php:
+
+    $email = GeneralUtility::makeInstance(MailMessage::class)
+         ->to(new Address('benni@typo3.org'), new NamedAddress('benni@typo3.org', 'Benni Mack'))
+         ->subject('This is an example email')
+         ->text('This is the plain-text variant')
+         ->html('<h4>Hello Benni.</h4><p>Enjoy a HTML-readable email. <marquee>We love TYPO3</marquee>.</p>');
+
+    $email->send();
+
+It is however also possible to re-use a Mailer instance, also adding custom Mailer
+settings via a custom Transport for special cases.
+
+:php:
+
+    $mailer = GeneralUtility::makeInstance(Mailer::class)
+
+    $email = GeneralUtility::makeInstance(MailMessage::class)
+         ->to(new Address('benni@typo3.org'), new NamedAddress('benni@typo3.org', 'Benni Mack'))
+         ->subject('This is an example email')
+         ->text('This is the plain-text variant')
+         ->html('<h4>Hello Benni.</h4><p>Enjoy a HTML-readable email. <marquee>We love TYPO3</marquee>.</p>');
+
+    // Send the email via the Mailer instance
+    $mailer->send($email);
+
+.. index:: PHP-API, ext:core
index cbd7255..e2af8f0 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Support\Extension;
  */
 
 use Codeception\Event\SuiteEvent;
+use Symfony\Component\Mailer\Transport\NullTransport;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Styleguide\TcaDataGenerator\Generator;
 use TYPO3\TestingFramework\Core\Acceptance\Extension\BackendEnvironment;
@@ -62,6 +63,11 @@ class BackendCoreEnvironment extends BackendEnvironment
             'PACKAGE:typo3/testing-framework/Resources/Core/Acceptance/Fixtures/tx_extensionmanager_domain_model_extension.xml',
             'PACKAGE:typo3/testing-framework/Resources/Core/Acceptance/Fixtures/tx_extensionmanager_domain_model_repository.xml',
         ],
+        'configurationToUseInTestInstance' => [
+            'MAIL' => [
+                'transport' => NullTransport::class
+            ]
+        ]
     ];
 
     /**
index 99a5a21..72ae1e2 100644 (file)
@@ -14,18 +14,20 @@ namespace TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Mail\FileSpool;
+
 /**
  * Fixture fake valid spool
  */
-class FakeFileSpoolFixture extends \Swift_FileSpool
+class FakeFileSpoolFixture extends FileSpool
 {
     public function __construct($path)
     {
-        $this->_path = $path;
+        $this->path = $path;
     }
 
     public function getPath(): string
     {
-        return $this->_path;
+        return $this->path;
     }
 }
index 18dc455..ee08f62 100644 (file)
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mime\RawMessage;
+
 /**
  * Fixture fake invalid spool
  */
@@ -31,23 +33,7 @@ class FakeInvalidSpoolFixture
         return $this->settings;
     }
 
-    public function start()
-    {
-    }
-
-    public function stop()
-    {
-    }
-
-    public function isStarted()
-    {
-    }
-
-    public function queueMessage(\Swift_Mime_Message $message)
-    {
-    }
-
-    public function flushQueue(\Swift_Transport $transport, &$failedRecipients = null)
+    public function queueMessage(RawMessage $message)
     {
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Mail/Fixtures/FakeTransportFixture.php b/typo3/sysext/core/Tests/Unit/Mail/Fixtures/FakeTransportFixture.php
deleted file mode 100644 (file)
index 6d3d186..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-namespace TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\Mail\MboxTransport;
-
-/**
- * Fixture fake transport
- */
-class FakeTransportFixture extends MboxTransport
-{
-    /**
-     * Constructor
-     *
-     * @param string $settings
-     */
-    public function __construct($settings)
-    {
-    }
-}
index 6a82287..c74640e 100644 (file)
@@ -14,10 +14,16 @@ namespace TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mailer\SentMessage;
+use Symfony\Component\Mailer\SmtpEnvelope;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Component\Mime\RawMessage;
+use TYPO3\CMS\Core\Mail\DelayedTransportInterface;
+
 /**
  * Fixture fake valid spool
  */
-class FakeValidSpoolFixture implements \Swift_Spool
+class FakeValidSpoolFixture implements DelayedTransportInterface
 {
     private $settings;
 
@@ -31,23 +37,13 @@ class FakeValidSpoolFixture implements \Swift_Spool
         return $this->settings;
     }
 
-    public function start()
-    {
-    }
-
-    public function stop()
-    {
-    }
-
-    public function isStarted()
-    {
-    }
-
-    public function queueMessage(\Swift_Mime_Message $message)
+    public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage
     {
+        // dont do anything
     }
 
-    public function flushQueue(\Swift_Transport $transport, &$failedRecipients = null)
+    public function flushQueue(TransportInterface $transport): int
     {
+        return 1;
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Mail/MailMessageTest.php b/typo3/sysext/core/Tests/Unit/Mail/MailMessageTest.php
deleted file mode 100644 (file)
index bf328d2..0000000
+++ /dev/null
@@ -1,227 +0,0 @@
-<?php
-namespace TYPO3\CMS\Core\Tests\Unit\Mail;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
-
-/**
- * Testcase for the TYPO3\CMS\Core\Mail\MailMessage class.
- */
-class MailMessageTest extends UnitTestCase
-{
-    /**
-     * @var \TYPO3\CMS\Core\Mail\MailMessage
-     */
-    protected $subject;
-
-    protected function setUp(): void
-    {
-        parent::setUp();
-        $this->subject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
-    }
-
-    /**
-     * @returns array
-     */
-    public function returnPathEmailAddressDataProvider()
-    {
-        return [
-            'string with ascii email address' => [
-                'john.doe@example.com',
-                'john.doe@example.com'
-            ],
-            'string with utf8 email address' => [
-                'john.doe@☺example.com',
-                'john.doe@xn--example-tc7d.com'
-            ]
-        ];
-    }
-
-    /**
-     * @test
-     * @param string $address
-     * @param string $expected
-     * @dataProvider returnPathEmailAddressDataProvider
-     */
-    public function setReturnPathIdnaEncodesAddresses($address, $expected)
-    {
-        $this->subject->setReturnPath($address);
-
-        $this->assertSame($expected, $this->subject->getReturnPath());
-    }
-
-    /**
-     * @returns array
-     */
-    public function senderEmailAddressDataProvider()
-    {
-        return [
-            'string with ascii email address' => [
-                'john.doe@example.com',
-                [
-                    'john.doe@example.com' => null,
-                ]
-            ],
-            'string with utf8 email address' => [
-                'john.doe@☺example.com',
-                [
-                    'john.doe@xn--example-tc7d.com' => null,
-                ]
-            ]
-        ];
-    }
-
-    /**
-     * @test
-     * @param string $address
-     * @param array $expected
-     * @dataProvider senderEmailAddressDataProvider
-     */
-    public function setSenderIdnaEncodesAddresses($address, $expected)
-    {
-        $this->subject->setSender($address);
-
-        $this->assertSame($expected, $this->subject->getSender());
-    }
-
-    /**
-     * @returns array
-     */
-    public function emailAddressesDataProvider()
-    {
-        return [
-            'string with ascii email address' => [
-                'john.doe@example.com',
-                [
-                    'john.doe@example.com' => null
-                ]
-            ],
-            'string with utf8 email address' => [
-                'john.doe@☺example.com',
-                [
-                    'john.doe@xn--example-tc7d.com' => null
-                ]
-            ],
-            'array with ascii email addresses' => [
-                [
-                    'john.doe@example.com' => 'John Doe',
-                    'jane.doe@example.com'
-                ],
-                [
-                    'john.doe@example.com' => 'John Doe',
-                    'jane.doe@example.com' => null,
-                ],
-            ],
-            'array with utf8 email addresses' => [
-                [
-                    'john.doe@☺example.com' => 'John Doe',
-                    'jane.doe@äöu.com' => 'Jane Doe',
-                ],
-                [
-                    'john.doe@xn--example-tc7d.com' => 'John Doe',
-                    'jane.doe@xn--u-zfa8c.com' => 'Jane Doe',
-                ],
-            ],
-            'array with mixed email addresses' => [
-                [
-                    'john.doe@☺example.com' => 'John Doe',
-                    'jane.doe@example.com' => 'Jane Doe',
-                ],
-                [
-                    'john.doe@xn--example-tc7d.com' => 'John Doe',
-                    'jane.doe@example.com' => 'Jane Doe',
-                ],
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     * @param string|array $addresses
-     * @param string|array $expected
-     * @dataProvider emailAddressesDataProvider
-     */
-    public function setFromIdnaEncodesAddresses($addresses, $expected)
-    {
-        $this->subject->setFrom($addresses);
-
-        $this->assertSame($expected, $this->subject->getFrom());
-    }
-
-    /**
-     * @test
-     * @param string|array $addresses
-     * @param string|array $expected
-     * @dataProvider emailAddressesDataProvider
-     */
-    public function setReplyToIdnaEncodesAddresses($addresses, $expected)
-    {
-        $this->subject->setReplyTo($addresses);
-
-        $this->assertSame($expected, $this->subject->getReplyTo());
-    }
-
-    /**
-     * @test
-     * @param string|array $addresses
-     * @param string|array $expected
-     * @dataProvider emailAddressesDataProvider
-     */
-    public function setToIdnaEncodesAddresses($addresses, $expected)
-    {
-        $this->subject->setTo($addresses);
-
-        $this->assertSame($expected, $this->subject->getTo());
-    }
-
-    /**
-     * @test
-     * @param string|array $addresses
-     * @param string|array $expected
-     * @dataProvider emailAddressesDataProvider
-     */
-    public function setCcIdnaEncodesAddresses($addresses, $expected)
-    {
-        $this->subject->setCc($addresses);
-
-        $this->assertSame($expected, $this->subject->getCc());
-    }
-
-    /**
-     * @test
-     * @param string|array $addresses
-     * @param string|array $expected
-     * @dataProvider emailAddressesDataProvider
-     */
-    public function setBccIdnaEncodesAddresses($addresses, $expected)
-    {
-        $this->subject->setBcc($addresses);
-
-        $this->assertSame($expected, $this->subject->getBcc());
-    }
-
-    /**
-     * @test
-     * @param string|array $addresses
-     * @param string|array $expected
-     * @dataProvider emailAddressesDataProvider
-     */
-    public function setReadReceiptToIdnaEncodesAddresses($addresses, $expected)
-    {
-        $this->subject->setReadReceiptTo($addresses);
-
-        $this->assertSame($expected, $this->subject->getReadReceiptTo());
-    }
-}
index 944acea..de0ca0b 100644 (file)
@@ -16,11 +16,14 @@ namespace TYPO3\CMS\Core\Tests\Unit\Mail;
  */
 
 use Prophecy\Argument;
+use Symfony\Component\Mailer\Transport\NullTransport;
+use Symfony\Component\Mailer\Transport\SendmailTransport;
+use Symfony\Component\Mailer\Transport\TransportInterface;
 use TYPO3\CMS\Core\Controller\ErrorPageController;
 use TYPO3\CMS\Core\Exception;
+use TYPO3\CMS\Core\Mail\DelayedTransportInterface;
 use TYPO3\CMS\Core\Mail\Mailer;
 use TYPO3\CMS\Core\Mail\TransportFactory;
-use TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures\FakeTransportFixture;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
@@ -57,10 +60,10 @@ class MailerTest extends UnitTestCase
     public function injectedSettingsAreNotReplacedByGlobalSettings()
     {
         $settings = ['transport' => 'mbox', 'transport_mbox_file' => '/path/to/file'];
-        $GLOBALS['TYPO3_CONF_VARS']['MAIL'] = ['transport' => 'sendmail', 'transport_sendmail_command' => 'sendmail'];
+        $GLOBALS['TYPO3_CONF_VARS']['MAIL'] = ['transport' => 'sendmail', 'transport_sendmail_command' => 'sendmail -bs'];
 
         $transportFactory = $this->prophesize(TransportFactory::class);
-        $transportFactory->get(Argument::any())->willReturn($this->prophesize(\Swift_Transport::class));
+        $transportFactory->get(Argument::any())->willReturn($this->prophesize(SendmailTransport::class));
         GeneralUtility::setSingletonInstance(TransportFactory::class, $transportFactory->reveal());
         $this->subject->injectMailSettings($settings);
         $this->subject->__construct();
@@ -73,10 +76,10 @@ class MailerTest extends UnitTestCase
      */
     public function globalSettingsAreUsedIfNoSettingsAreInjected()
     {
-        $settings = ($GLOBALS['TYPO3_CONF_VARS']['MAIL'] = ['transport' => 'sendmail', 'transport_sendmail_command' => 'sendmail']);
+        $settings = ($GLOBALS['TYPO3_CONF_VARS']['MAIL'] = ['transport' => 'sendmail', 'transport_sendmail_command' => 'sendmail -bs']);
         $this->subject->__construct();
         $transportFactory = $this->prophesize(TransportFactory::class);
-        $transportFactory->get(Argument::any())->willReturn($this->prophesize(\Swift_Transport::class));
+        $transportFactory->get(Argument::any())->willReturn($this->prophesize(SendmailTransport::class));
         GeneralUtility::setSingletonInstance(TransportFactory::class, $transportFactory->reveal());
         $this->subject->injectMailSettings($settings);
         $this->subject->__construct();
@@ -93,9 +96,8 @@ class MailerTest extends UnitTestCase
     {
         return [
             'smtp but no host' => [['transport' => 'smtp']],
-            'sendmail but no command' => [['transport' => 'sendmail']],
             'mbox but no file' => [['transport' => 'mbox']],
-            'no instance of Swift_Transport' => [['transport' => ErrorPageController::class]]
+            'no instance of TransportInterface' => [['transport' => ErrorPageController::class]]
         ];
     }
 
@@ -118,7 +120,7 @@ class MailerTest extends UnitTestCase
      */
     public function providingCorrectClassnameDoesNotThrowException()
     {
-        $this->subject->injectMailSettings(['transport' => FakeTransportFixture::class]);
+        $this->subject->injectMailSettings(['transport' => NullTransport::class]);
         $this->subject->__construct();
     }
 
@@ -129,7 +131,7 @@ class MailerTest extends UnitTestCase
     {
         $this->subject->injectMailSettings(['transport' => 'smtp', 'transport_smtp_server' => 'localhost']);
         $this->subject->__construct();
-        $port = $this->subject->getTransport()->getPort();
+        $port = $this->subject->getTransport()->getStream()->getPort();
         $this->assertEquals(25, $port);
     }
 
@@ -140,7 +142,7 @@ class MailerTest extends UnitTestCase
     {
         $this->subject->injectMailSettings(['transport' => 'smtp', 'transport_smtp_server' => 'localhost:']);
         $this->subject->__construct();
-        $port = $this->subject->getTransport()->getPort();
+        $port = $this->subject->getTransport()->getStream()->getPort();
         $this->assertEquals(25, $port);
     }
 
@@ -151,7 +153,7 @@ class MailerTest extends UnitTestCase
     {
         $this->subject->injectMailSettings(['transport' => 'smtp', 'transport_smtp_server' => 'localhost:12345']);
         $this->subject->__construct();
-        $port = $this->subject->getTransport()->getPort();
+        $port = $this->subject->getTransport()->getStream()->getPort();
         $this->assertEquals(12345, $port);
     }
 
@@ -164,8 +166,8 @@ class MailerTest extends UnitTestCase
         $this->subject->injectMailSettings($settings);
         $transport = $this->subject->getRealTransport();
 
-        $this->assertInstanceOf(\Swift_Transport::class, $transport);
-        $this->assertNotInstanceOf(\Swift_SpoolTransport::class, $transport);
+        $this->assertInstanceOf(TransportInterface::class, $transport);
+        $this->assertNotInstanceOf(DelayedTransportInterface::class, $transport);
     }
 
     /**
@@ -177,11 +179,11 @@ class MailerTest extends UnitTestCase
     {
         return [
             'without spool' => [[
-                'transport' => 'mail',
+                'transport' => 'sendmail',
                 'spool' => '',
             ]],
             'with spool' => [[
-                'transport' => 'mail',
+                'transport' => 'sendmail',
                 'spool' => 'memory',
             ]],
         ];
index 82dd654..91af15b 100644 (file)
@@ -15,9 +15,13 @@ namespace TYPO3\CMS\Core\Tests\Unit\Mail;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mailer\Transport\TransportInterface;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Mail\DelayedTransportInterface;
+use TYPO3\CMS\Core\Mail\FileSpool;
 use TYPO3\CMS\Core\Mail\MemorySpool;
 use TYPO3\CMS\Core\Mail\TransportFactory;
+use TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures\FakeFileSpoolFixture;
 use TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures\FakeInvalidSpoolFixture;
 use TYPO3\CMS\Core\Tests\Unit\Mail\Fixtures\FakeValidSpoolFixture;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
@@ -35,10 +39,10 @@ class TransportFactoryTest extends UnitTestCase
     /**
      * @test
      */
-    public function getReturnsSwiftSpoolTransportUsingSwiftFileSpool(): void
+    public function getReturnsSpoolTransportUsingFileSpool(): void
     {
         $mailSettings = [
-            'transport' => 'mail',
+            'transport' => 'sendmail',
             'transport_smtp_server' => 'localhost:25',
             'transport_smtp_encrypt' => '',
             'transport_smtp_username' => '',
@@ -52,23 +56,20 @@ class TransportFactoryTest extends UnitTestCase
         ];
 
         // Register fixture class
-        $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\Swift_FileSpool::class]['className'] = Fixtures\FakeFileSpoolFixture::class;
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][FileSpool::class]['className'] = Fixtures\FakeFileSpoolFixture::class;
 
         $transport = (new TransportFactory())->get($mailSettings);
-        $this->assertInstanceOf(\Swift_SpoolTransport::class, $transport);
-
-        /** @var Fixtures\FakeFileSpoolFixture $spool */
-        $spool = $transport->getSpool();
-        $this->assertInstanceOf(\Swift_FileSpool::class, $spool);
+        $this->assertInstanceOf(DelayedTransportInterface::class, $transport);
+        $this->assertInstanceOf(FakeFileSpoolFixture::class, $transport);
 
-        $path = $spool->getPath();
+        $path = $transport->getPath();
         $this->assertStringContainsString($mailSettings['transport_spool_filepath'], $path);
     }
 
     /**
      * @test
      */
-    public function getReturnsSwiftSpoolTransportUsingSwiftMemorySpool(): void
+    public function getReturnsSpoolTransportUsingMemorySpool(): void
     {
         $mailSettings = [
             'transport' => 'mail',
@@ -88,20 +89,17 @@ class TransportFactoryTest extends UnitTestCase
         $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][MemorySpool::class]['className'] = Fixtures\FakeMemorySpoolFixture::class;
 
         $transport = (new TransportFactory())->get($mailSettings);
-        $this->assertInstanceOf(\Swift_SpoolTransport::class, $transport);
-
-        /** @var \Swift_MemorySpool $spool */
-        $spool = $transport->getSpool();
-        $this->assertInstanceOf(\Swift_MemorySpool::class, $spool);
+        $this->assertInstanceOf(DelayedTransportInterface::class, $transport);
+        $this->assertInstanceOf(MemorySpool::class, $transport);
     }
 
     /**
      * @test
      */
-    public function getReturnsSwiftSpoolTransportUsingCustomSpool(): void
+    public function getReturnsSpoolTransportUsingCustomSpool(): void
     {
         $mailSettings = [
-            'transport' => 'mail',
+            'transport' => 'sendmail',
             'transport_smtp_server' => 'localhost:25',
             'transport_smtp_encrypt' => '',
             'transport_smtp_username' => '',
@@ -115,13 +113,10 @@ class TransportFactoryTest extends UnitTestCase
         ];
 
         $transport = (new TransportFactory())->get($mailSettings);
-        $this->assertInstanceOf(\Swift_SpoolTransport::class, $transport);
-
-        /** @var Fixtures\FakeValidSpoolFixture $spool */
-        $spool = $transport->getSpool();
-        $this->assertInstanceOf(Fixtures\FakeValidSpoolFixture::class, $spool);
+        $this->assertInstanceOf(DelayedTransportInterface::class, $transport);
+        $this->assertInstanceOf(Fixtures\FakeValidSpoolFixture::class, $transport);
 
-        $this->assertSame($mailSettings, $spool->getSettings());
+        $this->assertSame($mailSettings, $transport->getSettings());
     }
 
     /**
@@ -129,7 +124,6 @@ class TransportFactoryTest extends UnitTestCase
      */
     public function getThrowsRuntimeExceptionForInvalidCustomSpool(): void
     {
-        $this->expectException(\RuntimeException::class);
         $this->expectExceptionCode(1466799482);
 
         $mailSettings = [
@@ -152,10 +146,10 @@ class TransportFactoryTest extends UnitTestCase
     /**
      * @test
      */
-    public function getReturnsSwiftMailTransport(): void
+    public function getReturnsMailerTransportInterface(): void
     {
         $mailSettings = [
-            'transport' => 'mail',
+            'transport' => 'smtp',
             'transport_smtp_server' => 'localhost:25',
             'transport_smtp_encrypt' => '',
             'transport_smtp_username' => '',
@@ -169,6 +163,6 @@ class TransportFactoryTest extends UnitTestCase
         ];
 
         $transport = (new TransportFactory())->get($mailSettings);
-        $this->assertInstanceOf(\Swift_MailTransport::class, $transport);
+        $this->assertInstanceOf(TransportInterface::class, $transport);
     }
 }
index 0aee9dd..d9795c1 100644 (file)
                "psr/http-server-handler": "^1.0",
                "psr/http-server-middleware": "^1.0",
                "psr/log": "~1.0.0",
-               "swiftmailer/swiftmailer": "~5.4.5",
                "symfony/console": "^4.1",
                "symfony/expression-language": "^4.1",
                "symfony/finder": "^4.1",
+               "symfony/mailer": "^4.3",
+               "symfony/mime": "^4.3",
                "symfony/polyfill-intl-icu": "^1.6",
                "symfony/polyfill-intl-idn": "^1.10",
                "symfony/polyfill-mbstring": "^1.2",
index e0bfb74..8e3162a 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Form\Domain\Finishers;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mime\NamedAddress;
 use TYPO3\CMS\Core\Mail\MailMessage;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Extbase\Domain\Model\FileReference;
@@ -104,20 +105,20 @@ class EmailFinisher extends AbstractFinisher
 
         $mail = $this->objectManager->get(MailMessage::class);
 
-        $mail->setFrom([$senderAddress => $senderName])
-            ->setTo($recipients)
-            ->setSubject($subject);
+        $mail->from(new NamedAddress($senderAddress, $senderName))
+            ->to(...$recipients)
+            ->subject($subject);
 
         if (!empty($replyToRecipients)) {
-            $mail->setReplyTo($replyToRecipients);
+            $mail->replyTo(...$replyToRecipients);
         }
 
         if (!empty($carbonCopyRecipients)) {
-            $mail->setCc($carbonCopyRecipients);
+            $mail->cc(...$carbonCopyRecipients);
         }
 
         if (!empty($blindCarbonCopyRecipients)) {
-            $mail->setBcc($blindCarbonCopyRecipients);
+            $mail->bcc(...$blindCarbonCopyRecipients);
         }
 
         $formRuntime = $this->finisherContext->getFormRuntime();
@@ -146,10 +147,10 @@ class EmailFinisher extends AbstractFinisher
             $standaloneView = $this->initializeStandaloneView($formRuntime, $part['format']);
             $message = $standaloneView->render();
 
-            if ($i > 0) {
-                $mail->addPart($message, $part['contentType']);
+            if ($part['contentType'] === 'text/plain') {
+                $mail->text($message);
             } else {
-                $mail->setBody($message, $part['contentType']);
+                $mail->html($message);
             }
         }
 
@@ -170,7 +171,7 @@ class EmailFinisher extends AbstractFinisher
                         $file = $file->getOriginalResource();
                     }
 
-                    $mail->attach(\Swift_Attachment::newInstance($file->getContents(), $file->getName(), $file->getMimeType()));
+                    $mail->attach($file->getContents(), $file->getName(), $file->getMimeType());
                 }
             }
         }
index 32eea01..c0439a1 100644 (file)
@@ -18,6 +18,7 @@ use Doctrine\DBAL\DBALException;
 use Doctrine\DBAL\Driver\Statement;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\Mime\NamedAddress;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\LanguageAspect;
@@ -5690,13 +5691,13 @@ class ContentObjectRenderer implements LoggerAwareInterface
         $senderName = trim($senderName);
         $senderAddress = trim($senderAddress);
         if ($senderName !== '' && $senderAddress !== '') {
-            $mail->setFrom([$senderAddress => $senderName]);
+            $mail->from(new NamedAddress($senderAddress, $senderName));
         } elseif ($senderAddress !== '') {
-            $mail->setFrom([$senderAddress]);
+            $mail->from($senderAddress);
         }
         $parsedReplyTo = MailUtility::parseAddresses($replyTo);
         if (!empty($parsedReplyTo)) {
-            $mail->setReplyTo($parsedReplyTo);
+            $mail->replyTo($parsedReplyTo);
         }
         $message = trim($message);
         if ($message !== '') {
@@ -5706,9 +5707,9 @@ class ContentObjectRenderer implements LoggerAwareInterface
             $plainMessage = trim($messageParts[1]);
             $parsedRecipients = MailUtility::parseAddresses($recipients);
             if (!empty($parsedRecipients)) {
-                $mail->setTo($parsedRecipients)
-                    ->setSubject($subject)
-                    ->setBody($plainMessage);
+                $mail->to(...$parsedRecipients)
+                    ->subject($subject)
+                    ->text($plainMessage);
                 $mail->send();
             }
             $parsedCc = MailUtility::parseAddresses($cc);
@@ -5717,12 +5718,12 @@ class ContentObjectRenderer implements LoggerAwareInterface
                 /** @var MailMessage $mail */
                 $mail = GeneralUtility::makeInstance(MailMessage::class);
                 if (!empty($parsedReplyTo)) {
-                    $mail->setReplyTo($parsedReplyTo);
+                    $mail->replyTo($parsedReplyTo);
                 }
-                $mail->setFrom($from)
-                    ->setTo($parsedCc)
-                    ->setSubject($subject)
-                    ->setBody($plainMessage);
+                $mail->from($from)
+                    ->to(...$parsedCc)
+                    ->subject($subject)
+                    ->text($plainMessage);
                 $mail->send();
             }
             return true;
index 8460ed2..2f0da9f 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Install\Authentication;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mime\NamedAddress;
 use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Mail\MailMessage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -74,10 +75,10 @@ class AuthenticationService
         if ($warningEmailAddress) {
             $mailMessage = GeneralUtility::makeInstance(MailMessage::class);
             $mailMessage
-                ->addTo($warningEmailAddress)
-                ->setSubject('Install Tool Login at \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\'')
-                ->addFrom($this->getSenderEmailAddress(), $this->getSenderEmailName())
-                ->setBody('There has been an Install Tool login at TYPO3 site'
+                ->to($warningEmailAddress)
+                ->subject('Install Tool Login at \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\'')
+                ->from(new NamedAddress($this->getSenderEmailAddress(), $this->getSenderEmailName()))
+                ->text('There has been an Install Tool login at TYPO3 site'
                     . ' \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\''
                     . ' (' . GeneralUtility::getIndpEnv('HTTP_HOST') . ')'
                     . ' from remote address \'' . GeneralUtility::getIndpEnv('REMOTE_ADDR') . '\'')
@@ -95,10 +96,10 @@ class AuthenticationService
         if ($warningEmailAddress) {
             $mailMessage = GeneralUtility::makeInstance(MailMessage::class);
             $mailMessage
-                ->addTo($warningEmailAddress)
-                ->setSubject('Install Tool Login ATTEMPT at \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\'')
-                ->addFrom($this->getSenderEmailAddress(), $this->getSenderEmailName())
-                ->setBody('There has been an Install Tool login attempt at TYPO3 site'
+                ->to($warningEmailAddress)
+                ->subject('Install Tool Login ATTEMPT at \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\'')
+                ->from(new NamedAddress($this->getSenderEmailAddress(), $this->getSenderEmailName()))
+                ->text('There has been an Install Tool login attempt at TYPO3 site'
                     . ' \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\''
                     . ' (' . GeneralUtility::getIndpEnv('HTTP_HOST') . ')'
                     . ' The last 5 characters of the MD5 hash of the password tried was \'' . substr(md5($formValues['password']), -5) . '\''
index a48bdbf..27481f5 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Install\Controller;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Component\Mime\NamedAddress;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
@@ -243,11 +244,11 @@ class EnvironmentController extends AbstractController
         } else {
             $mailMessage = GeneralUtility::makeInstance(MailMessage::class);
             $mailMessage
-                ->addTo($recipient)
-                ->addFrom($this->getSenderEmailAddress(), $this->getSenderEmailName())
-                ->setSubject($this->getEmailSubject())
-                ->setBody('<html><body>html test content</body></html>', 'text/html')
-                ->addPart('plain test content', 'text/plain')
+                ->to($recipient)
+                ->from(new NamedAddress($this->getSenderEmailAddress(), $this->getSenderEmailName()))
+                ->subject($this->getEmailSubject())
+                ->html('<html><body>html test content</body></html>')
+                ->text('plain test content')
                 ->send();
             $messages->enqueue(new FlashMessage(
                 'Recipient: ' . $recipient,
index 5c711b2..c24a555 100644 (file)
@@ -183,6 +183,7 @@ class SilentConfigurationUpgradeService
         $this->migrateDisplayErrorsSetting();
         $this->migrateSaltedPasswordsSettings();
         $this->migrateCachingFrameworkCaches();
+        $this->migrateMailSettingsToSendmail();
 
         // Should run at the end to prevent obsolete settings are removed before migration
         $this->removeObsoleteLocalConfigurationSettings();
@@ -1078,4 +1079,24 @@ class SilentConfigurationUpgradeService
             // no change inside the LocalConfiguration.php found, so nothing needs to be modified
         }
     }
+
+    /**
+     * Migrates "mail" to "sendmail" as "mail" (PHP's built-in mail() method) is not supported anymore
+     * with Symfony components.
+     * See #88643
+     */
+    protected function migrateMailSettingsToSendmail()
+    {
+        $confManager = $this->configurationManager;
+        try {
+            $transport = (array)$confManager->getLocalConfigurationValueByPath('MAIL/transport');
+            if ($transport === 'mail') {
+                $confManager->setLocalConfigurationValueByPath('MAIL/transport', 'sendmail');
+                $confManager->setLocalConfigurationValueByPath('MAIL/transport_sendmail_command', (string)@ini_get('sendmail_path'));
+                $this->throwConfigurationChangedException();
+            }
+        } catch (MissingArrayPathException $e) {
+            // no change inside the LocalConfiguration.php found, so nothing needs to be modified
+        }
+    }
 }
index 69f80b3..1f0a8c7 100644 (file)
@@ -10,7 +10,7 @@
 <ul>
     <li>
         If <code>$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport']</code> is set to mail or sendmail, TYPO3 tries to use
-        PHPs internal mail() function. There might be no such program installed.
+        the operating systems' sendmail application. There might be no such program installed.
     </li>
     <li>
         If <code>$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport']</code> is set to smtp, check if the data given in
index 9b94641..37c3007 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Linkvalidator\Task;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mime\NamedAddress;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Mail\MailMessage;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
@@ -451,7 +452,6 @@ class ValidatorTask extends AbstractTask
             unset($params);
         }
         $content = $this->templateService->substituteMarkerArray($content, $markerArray, '###|###', true, true);
-        /** @var MailMessage $mail */
         $mail = GeneralUtility::makeInstance(MailMessage::class);
         if (empty($modTsConfig['mail.']['fromemail'])) {
             $modTsConfig['mail.']['fromemail'] = MailUtility::getSystemFromAddress();
@@ -460,7 +460,7 @@ class ValidatorTask extends AbstractTask
             $modTsConfig['mail.']['fromname'] = MailUtility::getSystemFromName();
         }
         if (GeneralUtility::validEmail($modTsConfig['mail.']['fromemail'])) {
-            $mail->setFrom([$modTsConfig['mail.']['fromemail'] => $modTsConfig['mail.']['fromname']]);
+            $mail->from(new NamedAddress($modTsConfig['mail.']['fromemail'], $modTsConfig['mail.']['fromname']));
         } else {
             throw new \Exception(
                 $lang->sL($this->languageFile . ':tasks.error.invalidFromEmail'),
@@ -468,10 +468,10 @@ class ValidatorTask extends AbstractTask
             );
         }
         if (GeneralUtility::validEmail($modTsConfig['mail.']['replytoemail'])) {
-            $mail->setReplyTo([$modTsConfig['mail.']['replytoemail'] => $modTsConfig['mail.']['replytoname']]);
+            $mail->replyTo(new NamedAddress($modTsConfig['mail.']['replytoemail'], $modTsConfig['mail.']['replytoname']));
         }
         if (!empty($modTsConfig['mail.']['subject'])) {
-            $mail->setSubject($modTsConfig['mail.']['subject']);
+            $mail->subject($modTsConfig['mail.']['subject']);
         } else {
             throw new \Exception(
                 $lang->sL($this->languageFile . ':tasks.error.noSubject'),
@@ -499,15 +499,13 @@ class ValidatorTask extends AbstractTask
             }
         }
         if (is_array($validEmailList) && !empty($validEmailList)) {
-            $mail->setTo($validEmailList);
-        } else {
-            $sendEmail = false;
-        }
-        if ($sendEmail) {
-            $mail->setBody($content, 'text/html');
-            $mail->send();
+            $mail
+                ->to(...$validEmailList)
+                ->html($content)
+                ->send();
+            return true;
         }
-        return $sendEmail;
+        return false;
     }
 
     /**
index af9ee49..b400015 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Reports\Task;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Mime\Address;
 use TYPO3\CMS\Core\Mail\MailMessage;
 use TYPO3\CMS\Core\Registry;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -102,7 +103,7 @@ class SystemStatusUpdateTask extends AbstractTask
         $notificationEmails = GeneralUtility::trimExplode(LF, $this->notificationEmail, true);
         $sendEmailsTo = [];
         foreach ($notificationEmails as $notificationEmail) {
-            $sendEmailsTo[] = $notificationEmail;
+            $sendEmailsTo[] = new Address($notificationEmail);
         }
         $subject = sprintf($this->getLanguageService()->getLL('status_updateTask_email_subject'), $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
         $message = $this->getNotificationAll() ? $this->getLanguageService()->getLL('status_allNotification') : $this->getLanguageService()->getLL('status_problemNotification');
@@ -112,12 +113,12 @@ class SystemStatusUpdateTask extends AbstractTask
         $message .= $this->getLanguageService()->getLL('status_updateTask_email_issues') . ': ' . CRLF;
         $message .= implode(CRLF, $systemIssues);
         $message .= CRLF . CRLF;
-        /** @var MailMessage $mail */
-        $mail = GeneralUtility::makeInstance(MailMessage::class);
-        $mail->setTo($sendEmailsTo);
-        $mail->setSubject($subject);
-        $mail->setBody($message);
-        $mail->send();
+
+        GeneralUtility::makeInstance(MailMessage::class)
+            ->to(...$sendEmailsTo)
+            ->subject($subject)
+            ->text($message)
+            ->send();
     }
 
     /**
index 09400af..6e9fdaa 100644 (file)
@@ -16,6 +16,8 @@ namespace TYPO3\CMS\Workspaces\Hook;
 
 use Doctrine\DBAL\DBALException;
 use Doctrine\DBAL\Platforms\SQLServerPlatform;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\NamedAddress;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Core\Environment;
@@ -27,6 +29,7 @@ use TYPO3\CMS\Core\Database\ReferenceIndex;
 use TYPO3\CMS\Core\Database\RelationHandler;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Mail\MailMessage;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
@@ -661,16 +664,15 @@ class DataHandlerHook
                 $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true);
                 $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true);
                 // Send an email to the recipient
-                /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */
-                $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
+                $mail = GeneralUtility::makeInstance(MailMessage::class);
                 if (!empty($recipientData['realName'])) {
-                    $recipient = [$recipientData['email'] => $recipientData['realName']];
+                    $recipient = new NamedAddress($recipientData['email'], $recipientData['realName']);
                 } else {
-                    $recipient = $recipientData['email'];
+                    $recipient = new Address($recipientData['email']);
                 }
-                $mail->setTo($recipient)
-                    ->setSubject($emailSubject)
-                    ->setBody($emailMessage);
+                $mail->to($recipient)
+                    ->subject($emailSubject)
+                    ->html($emailMessage);
                 $mail->send();
             }
             $emailRecipients = implode(',', $emailRecipients);