From 5b9cded2760343dfe0687e1ce916c5a08d604f63 Mon Sep 17 00:00:00 2001 From: Konstantin Myakshin Date: Thu, 6 Jun 2019 02:11:36 +0300 Subject: [PATCH] Add transport factories (closes #31385, closes #32523) --- .../FrameworkExtension.php | 22 + .../Resources/config/mailer.xml | 9 +- .../Resources/config/mailer_transports.xml | 50 +++ .../Amazon/Factory/SesTransportFactory.php | 51 +++ .../Tests/Factory/SesTransportFactoryTest.php | 98 +++++ .../Google/Factory/GmailTransportFactory.php | 38 ++ .../Factory/GmailTransportFactoryTest.php | 50 +++ .../Factory/MandrillTransportFactory.php | 51 +++ .../Factory/MandrillTransportFactoryTest.php | 83 ++++ .../Factory/MailgunTransportFactory.php | 51 +++ .../Factory/MailgunTransportFactoryTest.php | 79 ++++ .../Factory/PostmarkTransportFactory.php | 45 ++ .../Factory/PostmarkTransportFactoryTest.php | 70 ++++ .../Factory/SendgridTransportFactory.php | 44 ++ .../Factory/SendgridTransportFactoryTest.php | 65 +++ src/Symfony/Component/Mailer/CHANGELOG.md | 2 + .../Exception/IncompleteDsnException.php | 19 + .../Exception/UnsupportedHostException.php | 61 +++ .../Exception/UnsupportedSchemeException.php | 25 ++ .../Mailer/Tests/Transport/DsnTest.php | 88 ++++ .../Transport/NullTransportFactoryTest.php | 52 +++ .../SendmailTransportFactoryTest.php | 52 +++ .../Smtp/EsmtpTransportFactoryTest.php | 52 +++ .../Mailer/Tests/TransportFactoryTestCase.php | 105 +++++ .../Component/Mailer/Tests/TransportTest.php | 384 +++--------------- src/Symfony/Component/Mailer/Transport.php | 218 ++++------ .../Transport/AbstractTransportFactory.php | 54 +++ .../Component/Mailer/Transport/Dsn.php | 89 ++++ .../Mailer/Transport/NullTransportFactory.php | 34 ++ .../Transport/SendmailTransportFactory.php | 34 ++ .../Transport/Smtp/EsmtpTransportFactory.php | 47 +++ .../Transport/TransportFactoryInterface.php | 29 ++ 32 files changed, 1672 insertions(+), 479 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml create mode 100644 src/Symfony/Component/Mailer/Bridge/Amazon/Factory/SesTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Factory/SesTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Google/Factory/GmailTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Google/Tests/Factory/GmailTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Factory/MandrillTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Factory/MandrillTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailgun/Factory/MailgunTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Factory/MailgunTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Postmark/Factory/PostmarkTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Factory/PostmarkTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Sendgrid/Factory/SendgridTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Factory/SendgridTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Exception/IncompleteDsnException.php create mode 100644 src/Symfony/Component/Mailer/Exception/UnsupportedHostException.php create mode 100644 src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php create mode 100644 src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php create mode 100644 src/Symfony/Component/Mailer/Tests/Transport/NullTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Tests/TransportFactoryTestCase.php create mode 100644 src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Transport/Dsn.php create mode 100644 src/Symfony/Component/Mailer/Transport/NullTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Transport/TransportFactoryInterface.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 2c7f8732f5..b79bc753f3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -77,6 +77,12 @@ use Symfony\Component\Lock\PersistStoreInterface; use Symfony\Component\Lock\Store\FlockStore; use Symfony\Component\Lock\Store\StoreFactory; use Symfony\Component\Lock\StoreInterface; +use Symfony\Component\Mailer\Bridge\Amazon\Factory\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Google\Factory\GmailTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailchimp\Factory\MandrillTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailgun\Factory\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Postmark\Factory\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendgrid\Factory\SendgridTransportFactory; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; @@ -1955,8 +1961,24 @@ class FrameworkExtension extends Extension } $loader->load('mailer.xml'); + $loader->load('mailer_transports.xml'); $container->getDefinition('mailer.default_transport')->setArgument(0, $config['dsn']); + $classToServices = [ + SesTransportFactory::class => 'mailer.transport_factory.amazon', + GmailTransportFactory::class => 'mailer.transport_factory.gmail', + MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', + MailgunTransportFactory::class => 'mailer.transport_factory.mailgun', + PostmarkTransportFactory::class => 'mailer.transport_factory.postmark', + SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', + ]; + + foreach ($classToServices as $class => $service) { + if (!class_exists($class)) { + $container->removeDefinition($service); + } + } + $recipients = $config['envelope']['recipients'] ?? null; $sender = $config['envelope']['sender'] ?? null; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml index cfe98f21dc..becf0d1b71 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml @@ -12,12 +12,13 @@ + + + + - + - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml new file mode 100644 index 0000000000..bddcc67f01 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Factory/SesTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Factory/SesTransportFactory.php new file mode 100644 index 0000000000..ca6fd49829 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Factory/SesTransportFactory.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Amazon\Factory; + +use Symfony\Component\Mailer\Bridge\Amazon; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class SesTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $user = $this->getUser($dsn); + $password = $this->getPassword($dsn); + $region = $dsn->getOption('region'); + + if ('api' === $scheme) { + return new Amazon\Http\Api\SesTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger); + } + + if ('http' === $scheme) { + return new Amazon\Http\SesTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger); + } + + if ('smtp' === $scheme) { + return new Amazon\Smtp\SesTransport($user, $password, $region, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'ses' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Factory/SesTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Factory/SesTransportFactoryTest.php new file mode 100644 index 0000000000..595f725828 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Factory/SesTransportFactoryTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Factory; + +use Symfony\Component\Mailer\Bridge\Amazon; +use Symfony\Component\Mailer\Bridge\Amazon\Factory\SesTransportFactory; +use Symfony\Component\Mailer\Tests\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class SesTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new SesTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('api', 'ses'), + true, + ]; + + yield [ + new Dsn('http', 'ses'), + true, + ]; + + yield [ + new Dsn('smtp', 'ses'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + $client = $this->getClient(); + $dispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + yield [ + new Dsn('api', 'ses', self::USER, self::PASSWORD), + new Amazon\Http\Api\SesTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('api', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']), + new Amazon\Http\Api\SesTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('http', 'ses', self::USER, self::PASSWORD), + new Amazon\Http\SesTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('http', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']), + new Amazon\Http\SesTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('smtp', 'ses', self::USER, self::PASSWORD), + new Amazon\Smtp\SesTransport(self::USER, self::PASSWORD, null, $dispatcher, $logger), + ]; + + yield [ + new Dsn('smtp', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']), + new Amazon\Smtp\SesTransport(self::USER, self::PASSWORD, 'eu-west-1', $dispatcher, $logger), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('foo', 'ses', self::USER, self::PASSWORD)]; + } + + public function incompleteDsnProvider(): iterable + { + yield [new Dsn('smtp', 'ses', self::USER)]; + + yield [new Dsn('smtp', 'ses', null, self::PASSWORD)]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Google/Factory/GmailTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Google/Factory/GmailTransportFactory.php new file mode 100644 index 0000000000..d96a471018 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Google/Factory/GmailTransportFactory.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Google\Factory; + +use Symfony\Component\Mailer\Bridge\Google\Smtp\GmailTransport; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class GmailTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('smtp' === $dsn->getScheme()) { + return new GmailTransport($this->getUser($dsn), $this->getPassword($dsn), $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'gmail' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Google/Tests/Factory/GmailTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Google/Tests/Factory/GmailTransportFactoryTest.php new file mode 100644 index 0000000000..a8a2f07396 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Google/Tests/Factory/GmailTransportFactoryTest.php @@ -0,0 +1,50 @@ +getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('smtp', 'gmail'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + yield [ + new Dsn('smtp', 'gmail', self::USER, self::PASSWORD), + new GmailTransport(self::USER, self::PASSWORD, $this->getDispatcher(), $this->getLogger()), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('http', 'gmail', self::USER, self::PASSWORD)]; + } + + public function incompleteDsnProvider(): iterable + { + yield [new Dsn('smtp', 'gmail', self::USER)]; + + yield [new Dsn('smtp', 'gmail', null, self::PASSWORD)]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Factory/MandrillTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Factory/MandrillTransportFactory.php new file mode 100644 index 0000000000..265302fa8b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Factory/MandrillTransportFactory.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailchimp\Factory; + +use Symfony\Component\Mailer\Bridge\Mailchimp; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class MandrillTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $user = $this->getUser($dsn); + + if ('api' === $scheme) { + return new Mailchimp\Http\Api\MandrillTransport($user, $this->client, $this->dispatcher, $this->logger); + } + + if ('http' === $scheme) { + return new Mailchimp\Http\MandrillTransport($user, $this->client, $this->dispatcher, $this->logger); + } + + if ('smtp' === $scheme) { + $password = $this->getPassword($dsn); + + return new Mailchimp\Smtp\MandrillTransport($user, $password, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'mandrill' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Factory/MandrillTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Factory/MandrillTransportFactoryTest.php new file mode 100644 index 0000000000..07dbdd4937 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Factory/MandrillTransportFactoryTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailchimp\Tests\Factory; + +use Symfony\Component\Mailer\Bridge\Mailchimp; +use Symfony\Component\Mailer\Bridge\Mailchimp\Factory\MandrillTransportFactory; +use Symfony\Component\Mailer\Tests\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class MandrillTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new MandrillTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('api', 'mandrill'), + true, + ]; + + yield [ + new Dsn('http', 'mandrill'), + true, + ]; + + yield [ + new Dsn('smtp', 'mandrill'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + $client = $this->getClient(); + $dispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + yield [ + new Dsn('api', 'mandrill', self::USER), + new Mailchimp\Http\Api\MandrillTransport(self::USER, $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('http', 'mandrill', self::USER), + new Mailchimp\Http\MandrillTransport(self::USER, $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('smtp', 'mandrill', self::USER, self::PASSWORD), + new Mailchimp\Smtp\MandrillTransport(self::USER, self::PASSWORD, $dispatcher, $logger), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('foo', 'mandrill', self::USER)]; + } + + public function incompleteDsnProvider(): iterable + { + yield [new Dsn('api', 'mandrill')]; + + yield [new Dsn('smtp', 'mandrill', self::USER)]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Factory/MailgunTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Factory/MailgunTransportFactory.php new file mode 100644 index 0000000000..3cb4369eb3 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Factory/MailgunTransportFactory.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailgun\Factory; + +use Symfony\Component\Mailer\Bridge\Mailgun; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class MailgunTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $user = $this->getUser($dsn); + $password = $this->getPassword($dsn); + $region = $dsn->getOption('region'); + + if ('api' === $scheme) { + return new Mailgun\Http\Api\MailgunTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger); + } + + if ('http' === $scheme) { + return new Mailgun\Http\MailgunTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger); + } + + if ('smtp' === $scheme) { + return new Mailgun\Smtp\MailgunTransport($user, $password, $region, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'mailgun' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Factory/MailgunTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Factory/MailgunTransportFactoryTest.php new file mode 100644 index 0000000000..c65b372d4c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Factory/MailgunTransportFactoryTest.php @@ -0,0 +1,79 @@ +getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('api', 'mailgun'), + true, + ]; + + yield [ + new Dsn('http', 'mailgun'), + true, + ]; + + yield [ + new Dsn('smtp', 'mailgun'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + $client = $this->getClient(); + $dispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + yield [ + new Dsn('api', 'mailgun', self::USER, self::PASSWORD), + new Mailgun\Http\Api\MailgunTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('api', 'mailgun', self::USER, self::PASSWORD, null, ['region' => 'eu']), + new Mailgun\Http\Api\MailgunTransport(self::USER, self::PASSWORD, 'eu', $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('http', 'mailgun', self::USER, self::PASSWORD), + new Mailgun\Http\MailgunTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger), + ]; + + yield [ + new Dsn('smtp', 'mailgun', self::USER, self::PASSWORD), + new Mailgun\Smtp\MailgunTransport(self::USER, self::PASSWORD, null, $dispatcher, $logger), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('foo', 'mailgun', self::USER, self::PASSWORD)]; + } + + public function incompleteDsnProvider(): iterable + { + yield [new Dsn('api', 'mailgun', self::USER)]; + + yield [new Dsn('api', 'mailgun', null, self::PASSWORD)]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Factory/PostmarkTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Factory/PostmarkTransportFactory.php new file mode 100644 index 0000000000..0a67102120 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Factory/PostmarkTransportFactory.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Postmark\Factory; + +use Symfony\Component\Mailer\Bridge\Postmark; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class PostmarkTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $user = $this->getUser($dsn); + + if ('api' === $scheme) { + return new Postmark\Http\Api\PostmarkTransport($user, $this->client, $this->dispatcher, $this->logger); + } + + if ('smtp' === $scheme) { + return new Postmark\Smtp\PostmarkTransport($user, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'postmark' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Factory/PostmarkTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Factory/PostmarkTransportFactoryTest.php new file mode 100644 index 0000000000..0de2e35aea --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Factory/PostmarkTransportFactoryTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Postmark\Tests\Factory; + +use Symfony\Component\Mailer\Bridge\Postmark; +use Symfony\Component\Mailer\Bridge\Postmark\Factory\PostmarkTransportFactory; +use Symfony\Component\Mailer\Tests\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class PostmarkTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new PostmarkTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('api', 'postmark'), + true, + ]; + + yield [ + new Dsn('smtp', 'postmark'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + $dispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + yield [ + new Dsn('api', 'postmark', self::USER), + new Postmark\Http\Api\PostmarkTransport(self::USER, $this->getClient(), $dispatcher, $logger), + ]; + + yield [ + new Dsn('smtp', 'postmark', self::USER), + new Postmark\Smtp\PostmarkTransport(self::USER, $dispatcher, $logger), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('foo', 'postmark', self::USER)]; + } + + public function incompleteDsnProvider(): iterable + { + yield [new Dsn('api', 'postmark')]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Factory/SendgridTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Factory/SendgridTransportFactory.php new file mode 100644 index 0000000000..ec7ed3cfdd --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Factory/SendgridTransportFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Sendgrid\Factory; + +use Symfony\Component\Mailer\Bridge\Sendgrid; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class SendgridTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $key = $this->getUser($dsn); + + if ('api' === $dsn->getScheme()) { + return new Sendgrid\Http\Api\SendgridTransport($key, $this->client, $this->dispatcher, $this->logger); + } + + if ('smtp' === $dsn->getScheme()) { + return new Sendgrid\Smtp\SendgridTransport($key, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'sendgrid' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Factory/SendgridTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Factory/SendgridTransportFactoryTest.php new file mode 100644 index 0000000000..82ac41e03b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Factory/SendgridTransportFactoryTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Factory; + +use Symfony\Component\Mailer\Bridge\Sendgrid; +use Symfony\Component\Mailer\Bridge\Sendgrid\Factory\SendgridTransportFactory; +use Symfony\Component\Mailer\Tests\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class SendgridTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new SendgridTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('api', 'sendgrid'), + true, + ]; + + yield [ + new Dsn('smtp', 'sendgrid'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + $dispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + yield [ + new Dsn('api', 'sendgrid', self::USER), + new Sendgrid\Http\Api\SendgridTransport(self::USER, $this->getClient(), $dispatcher, $logger), + ]; + + yield [ + new Dsn('smtp', 'sendgrid', self::USER), + new Sendgrid\Smtp\SendgridTransport(self::USER, $dispatcher, $logger), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('foo', 'sendgrid', self::USER)]; + } +} diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 5b2c5c528f..7e2c53504b 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG * [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface` instead of `Symfony\Component\EventDispatcher\EventDispatcherInterface`. + * Added possibility to register custom transport for dsn by implementing + `Symfony\Component\Mailer\Transport\TransportFactoryInterface` and tagging with `mailer.transport_factory` tag in DI. 4.3.0 ----- diff --git a/src/Symfony/Component/Mailer/Exception/IncompleteDsnException.php b/src/Symfony/Component/Mailer/Exception/IncompleteDsnException.php new file mode 100644 index 0000000000..f2618b65d9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Exception/IncompleteDsnException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Konstantin Myakshin + */ +class IncompleteDsnException extends InvalidArgumentException +{ +} diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedHostException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedHostException.php new file mode 100644 index 0000000000..92af7b2567 --- /dev/null +++ b/src/Symfony/Component/Mailer/Exception/UnsupportedHostException.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +use Symfony\Component\Mailer\Bridge; +use Symfony\Component\Mailer\Transport\Dsn; + +/** + * @author Konstantin Myakshin + */ +class UnsupportedHostException extends LogicException +{ + private const HOST_TO_PACKAGE_MAP = [ + 'gmail' => [ + 'class' => Bridge\Google\Factory\GmailTransportFactory::class, + 'package' => 'symfony/google-mailer', + ], + 'mailgun' => [ + 'class' => Bridge\Mailgun\Factory\MailgunTransportFactory::class, + 'package' => 'symfony/mailgun-mailer', + ], + 'postmark' => [ + 'class' => Bridge\Postmark\Factory\PostmarkTransportFactory::class, + 'package' => 'symfony/postmark-mailer', + ], + 'sendgrid' => [ + 'class' => Bridge\Sendgrid\Factory\SendgridTransportFactory::class, + 'package' => 'symfony/sendgrid-mailer', + ], + 'ses' => [ + 'class' => Bridge\Amazon\Factory\SesTransportFactory::class, + 'package' => 'symfony/amazon-mailer', + ], + 'mandrill' => [ + 'class' => Bridge\Mailchimp\Factory\MandrillTransportFactory::class, + 'package' => 'symfony/mailchimp-mailer', + ], + ]; + + public function __construct(Dsn $dsn) + { + $host = $dsn->getHost(); + $package = self::HOST_TO_PACKAGE_MAP[$host] ?? null; + if ($package && !class_exists($package['class'])) { + parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed. Try running "composer require %s".', $host, $package['package'])); + + return; + } + + parent::__construct(sprintf('The "%s" mailer is not supported.', $host)); + } +} diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php new file mode 100644 index 0000000000..8457378c46 --- /dev/null +++ b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +use Symfony\Component\Mailer\Transport\Dsn; + +/** + * @author Konstantin Myakshin + */ +class UnsupportedSchemeException extends LogicException +{ + public function __construct(Dsn $dsn) + { + parent::__construct(sprintf('The "%s" scheme is not supported for mailer "%s".', $dsn->getScheme(), $dsn->getHost())); + } +} diff --git a/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php b/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php new file mode 100644 index 0000000000..04f12030da --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Transport\Dsn; + +class DsnTest extends TestCase +{ + /** + * @dataProvider fromStringProvider + */ + public function testFromString(string $string, Dsn $dsn): void + { + $this->assertEquals($dsn, Dsn::fromString($string)); + } + + public function testGetOption(): void + { + $options = ['with_value' => 'some value', 'nullable' => null]; + $dsn = new Dsn('smtp', 'example.com', null, null, null, $options); + + $this->assertSame('some value', $dsn->getOption('with_value')); + $this->assertSame('default', $dsn->getOption('nullable', 'default')); + $this->assertSame('default', $dsn->getOption('not_existent_property', 'default')); + } + + /** + * @dataProvider invalidDsnProvider + */ + public function testInvalidDsn(string $dsn, string $exceptionMessage): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($exceptionMessage); + Dsn::fromString($dsn); + } + + public function fromStringProvider(): iterable + { + yield 'simple smtp without user and pass' => [ + 'smtp://example.com', + new Dsn('smtp', 'example.com'), + ]; + + yield 'simple smtp with custom port' => [ + 'smtp://user1:pass2@example.com:99', + new Dsn('smtp', 'example.com', 'user1', 'pass2', 99), + ]; + + yield 'gmail smtp with urlencoded user and pass' => [ + 'smtp://u%24er:pa%24s@gmail', + new Dsn('smtp', 'gmail', 'u$er', 'pa$s'), + ]; + + yield 'mailgun api with custom options' => [ + 'api://u%24er:pa%24s@mailgun?region=eu', + new Dsn('api', 'mailgun', 'u$er', 'pa$s', null, ['region' => 'eu']), + ]; + } + + public function invalidDsnProvider(): iterable + { + yield [ + 'some://', + 'The "some://" mailer DSN is invalid.', + ]; + + yield [ + '//sendmail', + 'The "//sendmail" mailer DSN must contain a transport scheme.', + ]; + + yield [ + 'file:///some/path', + 'The "file:///some/path" mailer DSN must contain a mailer name.', + ]; + } +} diff --git a/src/Symfony/Component/Mailer/Tests/Transport/NullTransportFactoryTest.php b/src/Symfony/Component/Mailer/Tests/Transport/NullTransportFactoryTest.php new file mode 100644 index 0000000000..8b8ab4a8cd --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Transport/NullTransportFactoryTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Tests\Transport; + +use Symfony\Component\Mailer\Tests\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\NullTransport; +use Symfony\Component\Mailer\Transport\NullTransportFactory; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class NullTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new NullTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('smtp', 'null'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + yield [ + new Dsn('smtp', 'null'), + new NullTransport($this->getDispatcher(), $this->getLogger()), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('foo', 'null')]; + } +} diff --git a/src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportFactoryTest.php b/src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportFactoryTest.php new file mode 100644 index 0000000000..d5ee4bec52 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportFactoryTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Tests\Transport; + +use Symfony\Component\Mailer\Tests\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\SendmailTransport; +use Symfony\Component\Mailer\Transport\SendmailTransportFactory; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class SendmailTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new SendmailTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('smtp', 'sendmail'), + true, + ]; + + yield [ + new Dsn('smtp', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + yield [ + new Dsn('smtp', 'sendmail'), + new SendmailTransport(null, $this->getDispatcher(), $this->getLogger()), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [new Dsn('http', 'sendmail')]; + } +} diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php new file mode 100644 index 0000000000..3a76d46fe1 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php @@ -0,0 +1,52 @@ +getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('smtp', 'example.com'), + true, + ]; + + yield [ + new Dsn('api', 'example.com'), + false, + ]; + } + + public function createProvider(): iterable + { + $eventDispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + $transport = new EsmtpTransport('example.com', 25, null, null, $eventDispatcher, $logger); + + yield [ + new Dsn('smtp', 'example.com'), + $transport, + ]; + + $transport = new EsmtpTransport('example.com', 99, 'ssl', 'login', $eventDispatcher, $logger); + $transport->setUsername(self::USER); + $transport->setPassword(self::PASSWORD); + + yield [ + new Dsn('smtp', 'example.com', self::USER, self::PASSWORD, 99, ['encryption' => 'ssl', 'auth_mode' => 'login']), + $transport, + ]; + } +} diff --git a/src/Symfony/Component/Mailer/Tests/TransportFactoryTestCase.php b/src/Symfony/Component/Mailer/Tests/TransportFactoryTestCase.php new file mode 100644 index 0000000000..b17f81c1e6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/TransportFactoryTestCase.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Tests; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +abstract class TransportFactoryTestCase extends TestCase +{ + protected const USER = 'u$er'; + protected const PASSWORD = 'pa$s'; + + protected $dispatcher; + protected $client; + protected $logger; + + abstract public function getFactory(): TransportFactoryInterface; + + abstract public function supportsProvider(): iterable; + + abstract public function createProvider(): iterable; + + public function unsupportedSchemeProvider(): iterable + { + return []; + } + + public function incompleteDsnProvider(): iterable + { + return []; + } + + /** + * @dataProvider supportsProvider + */ + public function testSupports(Dsn $dsn, bool $supports): void + { + $factory = $this->getFactory(); + + $this->assertSame($supports, $factory->supports($dsn)); + } + + /** + * @dataProvider createProvider + */ + public function testCreate(Dsn $dsn, TransportInterface $transport): void + { + $factory = $this->getFactory(); + + $this->assertEquals($transport, $factory->create($dsn)); + } + + /** + * @dataProvider unsupportedSchemeProvider + */ + public function testUnsupportedSchemeException(Dsn $dsn): void + { + $factory = $this->getFactory(); + + $this->expectException(UnsupportedSchemeException::class); + $factory->create($dsn); + } + + /** + * @dataProvider incompleteDsnProvider + */ + public function testIncompleteDsnException(Dsn $dsn): void + { + $factory = $this->getFactory(); + + $this->expectException(IncompleteDsnException::class); + $factory->create($dsn); + } + + protected function getDispatcher(): EventDispatcherInterface + { + return $this->dispatcher ?? $this->dispatcher = $this->createMock(EventDispatcherInterface::class); + } + + protected function getClient(): HttpClientInterface + { + return $this->client ?? $this->client = $this->createMock(HttpClientInterface::class); + } + + protected function getLogger(): LoggerInterface + { + return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class); + } +} diff --git a/src/Symfony/Component/Mailer/Tests/TransportTest.php b/src/Symfony/Component/Mailer/Tests/TransportTest.php index 8c7cd99d98..6fb3a1a08d 100644 --- a/src/Symfony/Component/Mailer/Tests/TransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/TransportTest.php @@ -12,345 +12,71 @@ namespace Symfony\Component\Mailer\Tests; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; -use Symfony\Component\Mailer\Bridge\Amazon; -use Symfony\Component\Mailer\Bridge\Google; -use Symfony\Component\Mailer\Bridge\Mailchimp; -use Symfony\Component\Mailer\Bridge\Mailgun; -use Symfony\Component\Mailer\Bridge\Postmark; -use Symfony\Component\Mailer\Bridge\Sendgrid; -use Symfony\Component\Mailer\Exception\InvalidArgumentException; -use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\SmtpEnvelope; use Symfony\Component\Mailer\Transport; -use Symfony\Component\Mime\Email; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\RawMessage; class TransportTest extends TestCase { - public function testFromDsnNull() + /** + * @dataProvider fromStringProvider + */ + public function testFromString(string $dsn, TransportInterface $transport): void { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://null', $dispatcher, null, $logger); - $this->assertInstanceOf(Transport\NullTransport::class, $transport); - $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher'); - $p->setAccessible(true); - $this->assertSame($dispatcher, $p->getValue($transport)); + $transportFactory = new Transport([new DummyTransportFactory()]); + + $this->assertEquals($transport, $transportFactory->fromString($dsn)); } - public function testFromDsnSendmail() + public function fromStringProvider(): iterable { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://sendmail', $dispatcher, null, $logger); - $this->assertInstanceOf(Transport\SendmailTransport::class, $transport); - $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher'); - $p->setAccessible(true); - $this->assertSame($dispatcher, $p->getValue($transport)); - } + $transportA = new DummyTransport('a'); + $transportB = new DummyTransport('b'); - public function testFromDsnSmtp() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://localhost:44?auth_mode=plain&encryption=tls', $dispatcher, null, $logger); - $this->assertInstanceOf(Transport\Smtp\SmtpTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger); - $this->assertEquals('localhost', $transport->getStream()->getHost()); - $this->assertEquals('plain', $transport->getAuthMode()); - $this->assertTrue($transport->getStream()->isTLS()); - $this->assertEquals(44, $transport->getStream()->getPort()); - } + yield 'simple transport' => [ + 'dummy://a', + $transportA, + ]; - public function testFromInvalidDsn() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The "some://" mailer DSN is invalid.'); - Transport::fromDsn('some://'); - } + yield 'failover transport' => [ + 'dummy://a || dummy://b', + new Transport\FailoverTransport([$transportA, $transportB]), + ]; - public function testNoScheme() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The "//sendmail" mailer DSN must contain a transport scheme.'); - Transport::fromDsn('//sendmail'); - } - - public function testFromInvalidDsnNoHost() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The "file:///some/path" mailer DSN must contain a mailer name.'); - Transport::fromDsn('file:///some/path'); - } - - public function testFromInvalidTransportName() - { - $this->expectException(LogicException::class); - Transport::fromDsn('api://foobar'); - } - - public function testFromDsnGmail() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@gmail', $dispatcher, null, $logger); - $this->assertInstanceOf(Google\Smtp\GmailTransport::class, $transport); - $this->assertEquals('u$er', $transport->getUsername()); - $this->assertEquals('pa$s', $transport->getPassword()); - $this->assertProperties($transport, $dispatcher, $logger); - - $this->expectException(LogicException::class); - Transport::fromDsn('http://gmail'); - } - - public function testFromDsnMailgun() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, null, $logger); - $this->assertInstanceOf(Mailgun\Smtp\MailgunTransport::class, $transport); - $this->assertEquals('u$er', $transport->getUsername()); - $this->assertEquals('pa$s', $transport->getPassword()); - $this->assertProperties($transport, $dispatcher, $logger); - - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, null, $logger); - $this->assertEquals('smtp.mailgun.org', $transport->getStream()->getHost()); - - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, null, $logger); - $this->assertEquals('smtp.eu.mailgun.org', $transport->getStream()->getHost()); - - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, null, $logger); - $this->assertEquals('smtp.mailgun.org', $transport->getStream()->getHost()); - - $client = $this->createMock(HttpClientInterface::class); - $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger); - $this->assertInstanceOf(Mailgun\Http\MailgunTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'key' => 'u$er', - 'domain' => 'pa$s', - 'client' => $client, - ]); - - $response = $this->createMock(ResponseInterface::class); - $response->expects($this->any())->method('getStatusCode')->willReturn(200); - $message = (new Email())->from('me@me.com')->to('you@you.com')->subject('hello')->text('Hello you'); - - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response); - $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger); - $transport->send($message); - - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.eu.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response); - $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, $client, $logger); - $transport->send($message); - - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response); - $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger); - $transport->send($message); - - $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger); - $this->assertInstanceOf(Mailgun\Http\Api\MailgunTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'key' => 'u$er', - 'domain' => 'pa$s', - 'client' => $client, - ]); - - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response); - $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger); - $transport->send($message); - - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.eu.mailgun.net/v3/pa%24s/messages')->willReturn($response); - $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, $client, $logger); - $transport->send($message); - - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response); - $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger); - $transport->send($message); - - $message = (new Email())->from('me@me.com')->to('you@you.com')->subject('hello')->html('test'); - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response); - $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger); - $transport->send($message); - - $stream = fopen('data://text/plain,'.$message->getTextBody(), 'r'); - $message = (new Email())->from('me@me.com')->to('you@you.com')->subject('hello')->html($stream); - $client = $this->createMock(HttpClientInterface::class); - $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response); - $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger); - $transport->send($message); - - $this->expectException(LogicException::class); - Transport::fromDsn('foo://mailgun'); - } - - public function testFromDsnPostmark() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').'@postmark', $dispatcher, null, $logger); - $this->assertInstanceOf(Postmark\Smtp\PostmarkTransport::class, $transport); - $this->assertEquals('u$er', $transport->getUsername()); - $this->assertEquals('u$er', $transport->getPassword()); - $this->assertProperties($transport, $dispatcher, $logger); - - $client = $this->createMock(HttpClientInterface::class); - $transport = Transport::fromDsn('api://'.urlencode('u$er').'@postmark', $dispatcher, $client, $logger); - $this->assertInstanceOf(Postmark\Http\Api\PostmarkTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'key' => 'u$er', - 'client' => $client, - ]); - - $this->expectException(LogicException::class); - Transport::fromDsn('http://postmark'); - } - - public function testFromDsnSendgrid() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').'@sendgrid', $dispatcher, null, $logger); - $this->assertInstanceOf(Sendgrid\Smtp\SendgridTransport::class, $transport); - $this->assertEquals('apikey', $transport->getUsername()); - $this->assertEquals('u$er', $transport->getPassword()); - $this->assertProperties($transport, $dispatcher, $logger); - - $client = $this->createMock(HttpClientInterface::class); - $transport = Transport::fromDsn('api://'.urlencode('u$er').'@sendgrid', $dispatcher, $client, $logger); - $this->assertInstanceOf(Sendgrid\Http\Api\SendgridTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'key' => 'u$er', - 'client' => $client, - ]); - - $this->expectException(LogicException::class); - Transport::fromDsn('http://sendgrid'); - } - - public function testFromDsnAmazonSes() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, null, $logger); - $this->assertInstanceOf(Amazon\Smtp\SesTransport::class, $transport); - $this->assertEquals('u$er', $transport->getUsername()); - $this->assertEquals('pa$s', $transport->getPassword()); - $this->assertContains('.sun.', $transport->getStream()->getHost()); - $this->assertProperties($transport, $dispatcher, $logger); - - $client = $this->createMock(HttpClientInterface::class); - $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, $client, $logger); - $this->assertInstanceOf(Amazon\Http\SesTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'accessKey' => 'u$er', - 'secretKey' => 'pa$s', - 'region' => 'sun', - 'client' => $client, - ]); - - $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, $client, $logger); - $this->assertInstanceOf(Amazon\Http\Api\SesTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'accessKey' => 'u$er', - 'secretKey' => 'pa$s', - 'region' => 'sun', - 'client' => $client, - ]); - - $this->expectException(LogicException::class); - Transport::fromDsn('foo://ses'); - } - - public function testFromDsnMailchimp() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mandrill', $dispatcher, null, $logger); - $this->assertInstanceOf(Mailchimp\Smtp\MandrillTransport::class, $transport); - $this->assertEquals('u$er', $transport->getUsername()); - $this->assertEquals('pa$s', $transport->getPassword()); - $this->assertProperties($transport, $dispatcher, $logger); - - $client = $this->createMock(HttpClientInterface::class); - $transport = Transport::fromDsn('http://'.urlencode('u$er').'@mandrill', $dispatcher, $client, $logger); - $this->assertInstanceOf(Mailchimp\Http\MandrillTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'key' => 'u$er', - 'client' => $client, - ]); - - $transport = Transport::fromDsn('api://'.urlencode('u$er').'@mandrill', $dispatcher, $client, $logger); - $this->assertInstanceOf(Mailchimp\Http\Api\MandrillTransport::class, $transport); - $this->assertProperties($transport, $dispatcher, $logger, [ - 'key' => 'u$er', - 'client' => $client, - ]); - - $this->expectException(LogicException::class); - Transport::fromDsn('foo://mandrill'); - } - - public function testFromDsnFailover() - { - $user = 'user'; - $pass = 'pass'; - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://example.com || smtp://'.urlencode($user).'@example.com || smtp://'.urlencode($user).':'.urlencode($pass).'@example.com', $dispatcher, null, $logger); - $this->assertInstanceOf(Transport\FailoverTransport::class, $transport); - $p = new \ReflectionProperty(Transport\RoundRobinTransport::class, 'transports'); - $p->setAccessible(true); - $transports = $p->getValue($transport); - $this->assertCount(3, $transports); - foreach ($transports as $transport) { - $this->assertProperties($transport, $dispatcher, $logger); - } - $this->assertSame('', $transports[0]->getUsername()); - $this->assertSame('', $transports[0]->getPassword()); - $this->assertSame($user, $transports[1]->getUsername()); - $this->assertSame('', $transports[1]->getPassword()); - $this->assertSame($user, $transports[2]->getUsername()); - $this->assertSame($pass, $transports[2]->getPassword()); - } - - public function testFromDsnRoundRobin() - { - $dispatcher = $this->createMock(EventDispatcherInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transport = Transport::fromDsn('smtp://null && smtp://null && smtp://null', $dispatcher, null, $logger); - $this->assertInstanceOf(Transport\RoundRobinTransport::class, $transport); - $p = new \ReflectionProperty(Transport\RoundRobinTransport::class, 'transports'); - $p->setAccessible(true); - $transports = $p->getValue($transport); - $this->assertCount(3, $transports); - foreach ($transports as $transport) { - $this->assertProperties($transport, $dispatcher, $logger); - } - } - - private function assertProperties(Transport\TransportInterface $transport, EventDispatcherInterface $dispatcher, LoggerInterface $logger, array $props = []) - { - $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher'); - $p->setAccessible(true); - $this->assertSame($dispatcher, $p->getValue($transport)); - - $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'logger'); - $p->setAccessible(true); - $this->assertSame($logger, $p->getValue($transport)); - - foreach ($props as $prop => $value) { - $p = new \ReflectionProperty($transport, $prop); - $p->setAccessible(true); - $this->assertEquals($value, $p->getValue($transport)); - } + yield 'round robin transport' => [ + 'dummy://a && dummy://b', + new Transport\RoundRobinTransport([$transportA, $transportB]), + ]; + } +} + +class DummyTransport implements Transport\TransportInterface +{ + private $host; + + public function __construct(string $host) + { + $this->host = $host; + } + + public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage + { + throw new \BadMethodCallException('This method newer should be called.'); + } +} + +class DummyTransportFactory implements Transport\TransportFactoryInterface +{ + public function create(Dsn $dsn): TransportInterface + { + return new DummyTransport($dsn->getHost()); + } + + public function supports(Dsn $dsn): bool + { + return 'dummy' === $dsn->getScheme(); } } diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index 42f545f9fd..b167b17d8c 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -12,181 +12,107 @@ namespace Symfony\Component\Mailer; use Psr\Log\LoggerInterface; -use Symfony\Component\Mailer\Bridge\Amazon; -use Symfony\Component\Mailer\Bridge\Google; -use Symfony\Component\Mailer\Bridge\Mailchimp; -use Symfony\Component\Mailer\Bridge\Mailgun; -use Symfony\Component\Mailer\Bridge\Postmark; -use Symfony\Component\Mailer\Bridge\Sendgrid; -use Symfony\Component\Mailer\Exception\InvalidArgumentException; -use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mailer\Bridge\Amazon\Factory\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Google\Factory\GmailTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailchimp\Factory\MandrillTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailgun\Factory\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Postmark\Factory\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendgrid\Factory\SendgridTransportFactory; +use Symfony\Component\Mailer\Exception\UnsupportedHostException; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\NullTransportFactory; +use Symfony\Component\Mailer\Transport\SendmailTransportFactory; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Fabien Potencier + * @author Konstantin Myakshin */ class Transport { + private const FACTORY_CLASSES = [ + SesTransportFactory::class, + GmailTransportFactory::class, + MandrillTransportFactory::class, + MailgunTransportFactory::class, + PostmarkTransportFactory::class, + SendgridTransportFactory::class, + ]; + + private $factories; + public static function fromDsn(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface { - // failover? + $factory = new self(self::getDefaultFactories($dispatcher, $client, $logger)); + + return $factory->fromString($dsn); + } + + /** + * @param TransportFactoryInterface[] $factories + */ + public function __construct(iterable $factories) + { + $this->factories = $factories; + } + + public function fromString(string $dsn): TransportInterface + { $dsns = preg_split('/\s++\|\|\s++/', $dsn); if (\count($dsns) > 1) { - $transports = []; - foreach ($dsns as $dsn) { - $transports[] = self::createTransport($dsn, $dispatcher, $client, $logger); - } - - return new Transport\FailoverTransport($transports); + return new Transport\FailoverTransport($this->createFromDsns($dsns)); } - // round robin? $dsns = preg_split('/\s++&&\s++/', $dsn); if (\count($dsns) > 1) { - $transports = []; - foreach ($dsns as $dsn) { - $transports[] = self::createTransport($dsn, $dispatcher, $client, $logger); - } - - return new Transport\RoundRobinTransport($transports); + return new Transport\RoundRobinTransport($this->createFromDsns($dsns)); } - return self::createTransport($dsn, $dispatcher, $client, $logger); + return $this->fromDsnObject(Dsn::fromString($dsn)); } - private static function createTransport(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface + public function fromDsnObject(Dsn $dsn): TransportInterface { - if (false === $parsedDsn = parse_url($dsn)) { - throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn)); + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return $factory->create($dsn); + } } - if (!isset($parsedDsn['scheme'])) { - throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a transport scheme.', $dsn)); + throw new UnsupportedHostException($dsn); + } + + /** + * @param string[] $dsns + * + * @return TransportInterface[] + */ + private function createFromDsns(array $dsns): array + { + $transports = []; + foreach ($dsns as $dsn) { + $transports[] = $this->fromDsnObject(Dsn::fromString($dsn)); } - if (!isset($parsedDsn['host'])) { - throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a mailer name.', $dsn)); + return $transports; + } + + private static function getDefaultFactories(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): iterable + { + foreach (self::FACTORY_CLASSES as $factoryClass) { + if (class_exists($factoryClass)) { + yield new $factoryClass($dispatcher, $client, $logger); + } } - $user = urldecode($parsedDsn['user'] ?? ''); - $pass = urldecode($parsedDsn['pass'] ?? ''); - parse_str($parsedDsn['query'] ?? '', $query); + yield new NullTransportFactory($dispatcher, $client, $logger); - switch ($parsedDsn['host']) { - case 'null': - if ('smtp' === $parsedDsn['scheme']) { - return new Transport\NullTransport($dispatcher, $logger); - } + yield new SendmailTransportFactory($dispatcher, $client, $logger); - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - case 'sendmail': - if ('smtp' === $parsedDsn['scheme']) { - return new Transport\SendmailTransport(null, $dispatcher, $logger); - } - - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - case 'gmail': - if (!class_exists(Google\Smtp\GmailTransport::class)) { - throw new \LogicException('Unable to send emails via Gmail as the Google bridge is not installed. Try running "composer require symfony/google-mailer".'); - } - - if ('smtp' === $parsedDsn['scheme']) { - return new Google\Smtp\GmailTransport($user, $pass, $dispatcher, $logger); - } - - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - case 'mailgun': - if (!class_exists(Mailgun\Smtp\MailgunTransport::class)) { - throw new \LogicException('Unable to send emails via Mailgun as the bridge is not installed. Try running "composer require symfony/mailgun-mailer".'); - } - - if ('smtp' === $parsedDsn['scheme']) { - return new Mailgun\Smtp\MailgunTransport($user, $pass, $query['region'] ?? null, $dispatcher, $logger); - } - if ('http' === $parsedDsn['scheme']) { - return new Mailgun\Http\MailgunTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger); - } - if ('api' === $parsedDsn['scheme']) { - return new Mailgun\Http\Api\MailgunTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger); - } - - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - case 'postmark': - if (!class_exists(Postmark\Smtp\PostmarkTransport::class)) { - throw new \LogicException('Unable to send emails via Postmark as the bridge is not installed. Try running "composer require symfony/postmark-mailer".'); - } - - if ('smtp' === $parsedDsn['scheme']) { - return new Postmark\Smtp\PostmarkTransport($user, $dispatcher, $logger); - } - if ('api' === $parsedDsn['scheme']) { - return new Postmark\Http\Api\PostmarkTransport($user, $client, $dispatcher, $logger); - } - - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - case 'sendgrid': - if (!class_exists(Sendgrid\Smtp\SendgridTransport::class)) { - throw new \LogicException('Unable to send emails via Sendgrid as the bridge is not installed. Try running "composer require symfony/sendgrid-mailer".'); - } - - if ('smtp' === $parsedDsn['scheme']) { - return new Sendgrid\Smtp\SendgridTransport($user, $dispatcher, $logger); - } - if ('api' === $parsedDsn['scheme']) { - return new Sendgrid\Http\Api\SendgridTransport($user, $client, $dispatcher, $logger); - } - - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - case 'ses': - if (!class_exists(Amazon\Smtp\SesTransport::class)) { - throw new \LogicException('Unable to send emails via Amazon SES as the bridge is not installed. Try running "composer require symfony/amazon-mailer".'); - } - - if ('smtp' === $parsedDsn['scheme']) { - return new Amazon\Smtp\SesTransport($user, $pass, $query['region'] ?? null, $dispatcher, $logger); - } - if ('api' === $parsedDsn['scheme']) { - return new Amazon\Http\Api\SesTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger); - } - if ('http' === $parsedDsn['scheme']) { - return new Amazon\Http\SesTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger); - } - - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - case 'mandrill': - if (!class_exists(Mailchimp\Smtp\MandrillTransport::class)) { - throw new \LogicException('Unable to send emails via Mandrill as the bridge is not installed. Try running "composer require symfony/mailchimp-mailer".'); - } - - if ('smtp' === $parsedDsn['scheme']) { - return new Mailchimp\Smtp\MandrillTransport($user, $pass, $dispatcher, $logger); - } - if ('api' === $parsedDsn['scheme']) { - return new Mailchimp\Http\Api\MandrillTransport($user, $client, $dispatcher, $logger); - } - if ('http' === $parsedDsn['scheme']) { - return new Mailchimp\Http\MandrillTransport($user, $client, $dispatcher, $logger); - } - - throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host'])); - default: - if ('smtp' === $parsedDsn['scheme']) { - $transport = new Transport\Smtp\EsmtpTransport($parsedDsn['host'], $parsedDsn['port'] ?? 25, $query['encryption'] ?? null, $query['auth_mode'] ?? null, $dispatcher, $logger); - - if ($user) { - $transport->setUsername($user); - } - - if ($pass) { - $transport->setPassword($pass); - } - - return $transport; - } - - throw new LogicException(sprintf('The "%s" mailer is not supported.', $parsedDsn['host'])); - } + yield new EsmtpTransportFactory($dispatcher, $client, $logger); } } diff --git a/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php b/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php new file mode 100644 index 0000000000..959fca5746 --- /dev/null +++ b/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Konstantin Myakshin + */ +abstract class AbstractTransportFactory implements TransportFactoryInterface +{ + protected $dispatcher; + protected $client; + protected $logger; + + public function __construct(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null) + { + $this->dispatcher = $dispatcher; + $this->client = $client; + $this->logger = $logger; + } + + protected function getUser(Dsn $dsn): string + { + $user = $dsn->getUser(); + if (null === $user) { + throw new IncompleteDsnException('User is not set.'); + } + + return $user; + } + + protected function getPassword(Dsn $dsn): string + { + $password = $dsn->getPassword(); + if (null === $password) { + throw new IncompleteDsnException('Password is not set.'); + } + + return $password; + } +} diff --git a/src/Symfony/Component/Mailer/Transport/Dsn.php b/src/Symfony/Component/Mailer/Transport/Dsn.php new file mode 100644 index 0000000000..b5e2843ab4 --- /dev/null +++ b/src/Symfony/Component/Mailer/Transport/Dsn.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\InvalidArgumentException; + +/** + * @author Konstantin Myakshin + */ +final class Dsn +{ + private $scheme; + private $host; + private $user; + private $password; + private $port; + private $options; + + public function __construct(string $scheme, string $host, ?string $user = null, ?string $password = null, ?int $port = null, array $options = []) + { + $this->scheme = $scheme; + $this->host = $host; + $this->user = $user; + $this->password = $password; + $this->port = $port; + $this->options = $options; + } + + public static function fromString(string $dsn): self + { + if (false === $parsedDsn = parse_url($dsn)) { + throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn)); + } + + if (!isset($parsedDsn['scheme'])) { + throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a transport scheme.', $dsn)); + } + + if (!isset($parsedDsn['host'])) { + throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a mailer name.', $dsn)); + } + + $user = urldecode($parsedDsn['user'] ?? null); + $password = urldecode($parsedDsn['pass'] ?? null); + $port = $parsedDsn['port'] ?? null; + parse_str($parsedDsn['query'] ?? '', $query); + + return new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getPort(int $default = null): ?int + { + return $this->port ?? $default; + } + + public function getOption(string $key, $default = null) + { + return $this->options[$key] ?? $default; + } +} diff --git a/src/Symfony/Component/Mailer/Transport/NullTransportFactory.php b/src/Symfony/Component/Mailer/Transport/NullTransportFactory.php new file mode 100644 index 0000000000..34600f7ec3 --- /dev/null +++ b/src/Symfony/Component/Mailer/Transport/NullTransportFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +final class NullTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('smtp' === $dsn->getScheme()) { + return new NullTransport($this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'null' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php b/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php new file mode 100644 index 0000000000..99e7bbf097 --- /dev/null +++ b/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +final class SendmailTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('smtp' === $dsn->getScheme()) { + return new SendmailTransport(null, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn); + } + + public function supports(Dsn $dsn): bool + { + return 'sendmail' === $dsn->getHost(); + } +} diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php new file mode 100644 index 0000000000..d1a5c60c5f --- /dev/null +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp; + +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class EsmtpTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $encryption = $dsn->getOption('encryption'); + $authMode = $dsn->getOption('auth_mode'); + $port = $dsn->getPort(25); + $host = $dsn->getHost(); + + $transport = new EsmtpTransport($host, $port, $encryption, $authMode, $this->dispatcher, $this->logger); + + if ($user = $dsn->getUser()) { + $transport->setUsername($user); + } + + if ($password = $dsn->getPassword()) { + $transport->setPassword($password); + } + + return $transport; + } + + public function supports(Dsn $dsn): bool + { + return 'smtp' === $dsn->getScheme(); + } +} diff --git a/src/Symfony/Component/Mailer/Transport/TransportFactoryInterface.php b/src/Symfony/Component/Mailer/Transport/TransportFactoryInterface.php new file mode 100644 index 0000000000..9785ae81a9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Transport/TransportFactoryInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +interface TransportFactoryInterface +{ + /** + * @throws UnsupportedSchemeException + * @throws IncompleteDsnException + */ + public function create(Dsn $dsn): TransportInterface; + + public function supports(Dsn $dsn): bool; +}