From ee77fee3438e7907363b3ae7dd6589b1ddc11006 Mon Sep 17 00:00:00 2001 From: Thibaut Cheymol Date: Thu, 23 Apr 2020 10:42:20 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[Mailer]=20Add=20Mailjet=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Mailer/Bridge/Mailjet/CHANGELOG.md | 7 + .../Component/Mailer/Bridge/Mailjet/LICENSE | 19 +++ .../Component/Mailer/Bridge/Mailjet/README.md | 24 +++ .../Transport/MailjetTransportFactoryTest.php | 83 ++++++++++ .../Mailjet/Transport/MailjetApiTransport.php | 145 ++++++++++++++++++ .../Transport/MailjetSmtpTransport.php | 27 ++++ .../Transport/MailjetTransportFactory.php | 43 ++++++ .../Mailer/Bridge/Mailjet/composer.json | 39 +++++ .../Mailer/Bridge/Mailjet/phpunit.xml.dist | 31 ++++ .../Exception/UnsupportedSchemeException.php | 4 + src/Symfony/Component/Mailer/Transport.php | 2 + src/Symfony/Component/Mailer/composer.json | 1 + 12 files changed, 425 insertions(+) create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/LICENSE create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/README.md create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Transport/MailjetTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetApiTransport.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetSmtpTransport.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailjet/phpunit.xml.dist diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md new file mode 100644 index 0000000000000..0d994e934e55a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.2.0 +----- + + * Added the bridge diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/LICENSE b/src/Symfony/Component/Mailer/Bridge/Mailjet/LICENSE new file mode 100644 index 0000000000000..4bf0fef4ff3b0 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019-2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md b/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md new file mode 100644 index 0000000000000..0f7e78509b6a8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md @@ -0,0 +1,24 @@ +Mailjet Bridge +============== + +Provides Mailjet integration for Symfony Mailer. + + + +Configuration examples : +--------- + +```dotenv +# Api usage +MAILER_DSN=mailjet+api://$PUBLIC_KEY:$PRIVATE_KEY@default +# Smtp usage +MAILER_DSN=mailjet+smtp://$PUBLIC_KEY:$PRIVATE_KEY@default +``` + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Transport/MailjetTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Transport/MailjetTransportFactoryTest.php new file mode 100644 index 0000000000000..dcc30e525b87d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Transport/MailjetTransportFactoryTest.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\Mailjet\Tests\Transport; + +use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetSmtpTransport; +use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Test\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class MailjetTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new MailjetTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('mailjet', 'default'), + true, + ]; + + yield [ + new Dsn('mailjet+smtp', 'default'), + true, + ]; + + yield [ + new Dsn('mailjet+smtps', 'default'), + true, + ]; + + yield [ + new Dsn('mailjet+smtp', 'example.com'), + true, + ]; + } + + public function createProvider(): iterable + { + $dispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + yield [ + new Dsn('mailjet', 'default', self::USER, self::PASSWORD), + new MailjetSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger), + ]; + + yield [ + new Dsn('mailjet+smtp', 'default', self::USER, self::PASSWORD), + new MailjetSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger), + ]; + + yield [ + new Dsn('mailjet+smtps', 'default', self::USER, self::PASSWORD), + new MailjetSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [ + new Dsn('mailjet+foo', 'mailjet', self::USER, self::PASSWORD), + 'The "mailjet+foo" scheme is not supported; supported schemes for mailer "mailjet" are: "mailjet", "mailjet+smtp", "mailjet+smtps".', + ]; + } + + public function incompleteDsnProvider(): iterable + { + yield [new Dsn('mailjet+smtp', 'default')]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetApiTransport.php new file mode 100644 index 0000000000000..4be2e0d34bf95 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetApiTransport.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailjet\Transport; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\AbstractApiTransport; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class MailjetApiTransport extends AbstractApiTransport +{ + private const HOST = 'api.mailjet.com'; + private const API_VERSION = '3.1'; + + private $privateKey; + private $publicKey; + + public function __construct(string $publicKey, string $privateKey, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + $this->publicKey = $publicKey; + $this->privateKey = $privateKey; + + parent::__construct($client, $dispatcher, $logger); + } + + public function __toString(): string + { + return sprintf('mailjet+api://%s', $this->getEndpoint()); + } + + protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface + { + $response = $this->client->request('POST', sprintf('https://%s/v%s/send', $this->getEndpoint(), self::API_VERSION), [ + 'headers' => [ + 'Accept' => 'application/json', + ], + 'auth_basic' => $this->publicKey.':'.$this->privateKey, + 'json' => $this->getPayload($email, $envelope), + ]); + + $result = $response->toArray(false); + + if (200 !== $response->getStatusCode()) { + if ('application/json' === $response->getHeaders(false)['content-type'][0]) { + throw new HttpTransportException(sprintf('Unable to send an email: "%s" (code %d).', $result['Message'], $response->getStatusCode()), $response); + } + + throw new HttpTransportException(sprintf('Unable to send an email: "%s" (code %d).', $response->getContent(false), $response->getStatusCode()), $response); + } + + // The response needs to contains a 'Messages' key that is an array + if (!\array_key_exists('Messages', $result) || !\is_array($result['Messages']) || 0 === \count($result['Messages'])) { + throw new HttpTransportException(sprintf('Unable to send an email: "%s" malformed api response.', $response->getContent(false)), $response); + } + + $sentMessage->setMessageId($response->getHeaders(false)['x-mj-request-guid'][0]); + + return $response; + } + + private function getPayload(Email $email, Envelope $envelope): array + { + $html = $email->getHtmlBody(); + if (null !== $html && \is_resource($html)) { + if (stream_get_meta_data($html)['seekable'] ?? false) { + rewind($html); + } + $html = stream_get_contents($html); + } + [$attachments, $inlines, $html] = $this->prepareAttachments($email, $html); + + $message = [ + 'From' => [ + 'Email' => $envelope->getSender()->getAddress(), + 'Name' => $envelope->getSender()->getName(), + ], + 'To' => array_map(function (Address $recipient) { + return [ + 'Email' => $recipient->getAddress(), + 'Name' => $recipient->getName(), + ]; + }, $this->getRecipients($email, $envelope)), + 'Subject' => $email->getSubject(), + 'Attachments' => $attachments, + 'InlinedAttachments' => $inlines, + ]; + if ($emails = $email->getCc()) { + $message['Cc'] = implode(',', $this->stringifyAddresses($emails)); + } + if ($emails = $email->getBcc()) { + $message['Bcc'] = implode(',', $this->stringifyAddresses($emails)); + } + if ($email->getTextBody()) { + $message['TextPart'] = $email->getTextBody(); + } + if ($html) { + $message['HTMLPart'] = $html; + } + + return [ + 'Messages' => [$message], + ]; + } + + private function prepareAttachments(Email $email, ?string $html): array + { + $attachments = $inlines = []; + foreach ($email->getAttachments() as $attachment) { + $headers = $attachment->getPreparedHeaders(); + $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); + $formattedAttachment = [ + 'ContentType' => $attachment->getMediaType().'/'.$attachment->getMediaSubtype(), + 'Filename' => $filename, + 'Base64Content' => $attachment->bodyToString(), + ]; + if ('inline' === $headers->getHeaderBody('Content-Disposition')) { + $inlines[] = $formattedAttachment; + } else { + $attachments[] = $formattedAttachment; + } + } + + return [$attachments, $inlines, $html]; + } + + private function getEndpoint(): ?string + { + return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : ''); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetSmtpTransport.php new file mode 100644 index 0000000000000..a549e8b6a3691 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetSmtpTransport.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailjet\Transport; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +class MailjetSmtpTransport extends EsmtpTransport +{ + public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + parent::__construct('in-v3.mailjet.com', 465, true, $dispatcher, $logger); + + $this->setUsername($username); + $this->setPassword($password); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetTransportFactory.php new file mode 100644 index 0000000000000..200abae46ad0a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetTransportFactory.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailjet\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +class MailjetTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $user = $this->getUser($dsn); + $password = $this->getPassword($dsn); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + + if ('maijlet+api' === $scheme) { + return (new MailjetApiTransport($user, $password, $this->client, $this->dispatcher, $this->logger))->setHost($host); + } + + if (\in_array($scheme, ['mailjet+smtp', 'mailjet+smtps', 'mailjet'])) { + return new MailjetSmtpTransport($user, $password, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn, 'mailjet', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['mailjet', 'mailjet+api', 'mailjet+smtp', 'mailjet+smtps']; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json new file mode 100644 index 0000000000000..4ab5f6a8cbe4c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/mailjet-mailer", + "type": "symfony-bridge", + "description": "Symfony Mailjet Mailer Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.2.5", + "symfony/mailer": "^4.4|^5.0" + }, + "require-dev": { + "symfony/http-client": "^4.4|^5.0" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Mailjet\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/phpunit.xml.dist b/src/Symfony/Component/Mailer/Bridge/Mailjet/phpunit.xml.dist new file mode 100644 index 0000000000000..1dab9b3d2ce2a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php index dc367df85aa6d..bcf7f415ee5a6 100644 --- a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php @@ -44,6 +44,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class, 'package' => 'symfony/mailchimp-mailer', ], + 'mailjet' => [ + 'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class, + 'package' => 'symfony/mailjet-mailer', + ], ]; public function __construct(Dsn $dsn, string $name = null, array $supported = []) diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index 4d5e525f79b1d..0c52282729ef6 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -16,6 +16,7 @@ use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Exception\InvalidArgumentException; @@ -46,6 +47,7 @@ class Transport MailgunTransportFactory::class, PostmarkTransportFactory::class, SendgridTransportFactory::class, + MailjetTransportFactory::class, ]; private $factories; diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json index c50506a12a4f8..698661ed830e3 100644 --- a/src/Symfony/Component/Mailer/composer.json +++ b/src/Symfony/Component/Mailer/composer.json @@ -28,6 +28,7 @@ "symfony/amazon-mailer": "^4.4|^5.0", "symfony/google-mailer": "^4.4|^5.0", "symfony/http-client-contracts": "^1.1|^2", + "symfony/mailjet-mailer": "^4.4|^5.0", "symfony/mailgun-mailer": "^4.4|^5.0", "symfony/mailchimp-mailer": "^4.4|^5.0", "symfony/messenger": "^4.4|^5.0",