Skip to content

Commit ee77fee

Browse files
tcheymolfabpot
authored andcommitted
✨ [Mailer] Add Mailjet bridge
1 parent c4ff074 commit ee77fee

File tree

12 files changed

+425
-0
lines changed

12 files changed

+425
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.2.0
5+
-----
6+
7+
* Added the bridge
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2019-2020 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Mailjet Bridge
2+
==============
3+
4+
Provides Mailjet integration for Symfony Mailer.
5+
6+
7+
8+
Configuration examples :
9+
---------
10+
11+
```dotenv
12+
# Api usage
13+
MAILER_DSN=mailjet+api://$PUBLIC_KEY:$PRIVATE_KEY@default
14+
# Smtp usage
15+
MAILER_DSN=mailjet+smtp://$PUBLIC_KEY:$PRIVATE_KEY@default
16+
```
17+
18+
Resources
19+
---------
20+
21+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
22+
* [Report issues](https://github.com/symfony/symfony/issues) and
23+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
24+
in the [main Symfony repository](https://github.com/symfony/symfony)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Mailjet\Tests\Transport;
13+
14+
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetSmtpTransport;
15+
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
16+
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
17+
use Symfony\Component\Mailer\Transport\Dsn;
18+
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
19+
20+
class MailjetTransportFactoryTest extends TransportFactoryTestCase
21+
{
22+
public function getFactory(): TransportFactoryInterface
23+
{
24+
return new MailjetTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
25+
}
26+
27+
public function supportsProvider(): iterable
28+
{
29+
yield [
30+
new Dsn('mailjet', 'default'),
31+
true,
32+
];
33+
34+
yield [
35+
new Dsn('mailjet+smtp', 'default'),
36+
true,
37+
];
38+
39+
yield [
40+
new Dsn('mailjet+smtps', 'default'),
41+
true,
42+
];
43+
44+
yield [
45+
new Dsn('mailjet+smtp', 'example.com'),
46+
true,
47+
];
48+
}
49+
50+
public function createProvider(): iterable
51+
{
52+
$dispatcher = $this->getDispatcher();
53+
$logger = $this->getLogger();
54+
55+
yield [
56+
new Dsn('mailjet', 'default', self::USER, self::PASSWORD),
57+
new MailjetSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger),
58+
];
59+
60+
yield [
61+
new Dsn('mailjet+smtp', 'default', self::USER, self::PASSWORD),
62+
new MailjetSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger),
63+
];
64+
65+
yield [
66+
new Dsn('mailjet+smtps', 'default', self::USER, self::PASSWORD),
67+
new MailjetSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger),
68+
];
69+
}
70+
71+
public function unsupportedSchemeProvider(): iterable
72+
{
73+
yield [
74+
new Dsn('mailjet+foo', 'mailjet', self::USER, self::PASSWORD),
75+
'The "mailjet+foo" scheme is not supported; supported schemes for mailer "mailjet" are: "mailjet", "mailjet+smtp", "mailjet+smtps".',
76+
];
77+
}
78+
79+
public function incompleteDsnProvider(): iterable
80+
{
81+
yield [new Dsn('mailjet+smtp', 'default')];
82+
}
83+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Mailjet\Transport;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Mailer\Envelope;
16+
use Symfony\Component\Mailer\Exception\HttpTransportException;
17+
use Symfony\Component\Mailer\SentMessage;
18+
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
19+
use Symfony\Component\Mime\Address;
20+
use Symfony\Component\Mime\Email;
21+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
use Symfony\Contracts\HttpClient\ResponseInterface;
24+
25+
class MailjetApiTransport extends AbstractApiTransport
26+
{
27+
private const HOST = 'api.mailjet.com';
28+
private const API_VERSION = '3.1';
29+
30+
private $privateKey;
31+
private $publicKey;
32+
33+
public function __construct(string $publicKey, string $privateKey, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
34+
{
35+
$this->publicKey = $publicKey;
36+
$this->privateKey = $privateKey;
37+
38+
parent::__construct($client, $dispatcher, $logger);
39+
}
40+
41+
public function __toString(): string
42+
{
43+
return sprintf('mailjet+api://%s', $this->getEndpoint());
44+
}
45+
46+
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
47+
{
48+
$response = $this->client->request('POST', sprintf('https://%s/v%s/send', $this->getEndpoint(), self::API_VERSION), [
49+
'headers' => [
50+
'Accept' => 'application/json',
51+
],
52+
'auth_basic' => $this->publicKey.':'.$this->privateKey,
53+
'json' => $this->getPayload($email, $envelope),
54+
]);
55+
56+
$result = $response->toArray(false);
57+
58+
if (200 !== $response->getStatusCode()) {
59+
if ('application/json' === $response->getHeaders(false)['content-type'][0]) {
60+
throw new HttpTransportException(sprintf('Unable to send an email: "%s" (code %d).', $result['Message'], $response->getStatusCode()), $response);
61+
}
62+
63+
throw new HttpTransportException(sprintf('Unable to send an email: "%s" (code %d).', $response->getContent(false), $response->getStatusCode()), $response);
64+
}
65+
66+
// The response needs to contains a 'Messages' key that is an array
67+
if (!\array_key_exists('Messages', $result) || !\is_array($result['Messages']) || 0 === \count($result['Messages'])) {
68+
throw new HttpTransportException(sprintf('Unable to send an email: "%s" malformed api response.', $response->getContent(false)), $response);
69+
}
70+
71+
$sentMessage->setMessageId($response->getHeaders(false)['x-mj-request-guid'][0]);
72+
73+
return $response;
74+
}
75+
76+
private function getPayload(Email $email, Envelope $envelope): array
77+
{
78+
$html = $email->getHtmlBody();
79+
if (null !== $html && \is_resource($html)) {
80+
if (stream_get_meta_data($html)['seekable'] ?? false) {
81+
rewind($html);
82+
}
83+
$html = stream_get_contents($html);
84+
}
85+
[$attachments, $inlines, $html] = $this->prepareAttachments($email, $html);
86+
87+
$message = [
88+
'From' => [
89+
'Email' => $envelope->getSender()->getAddress(),
90+
'Name' => $envelope->getSender()->getName(),
91+
],
92+
'To' => array_map(function (Address $recipient) {
93+
return [
94+
'Email' => $recipient->getAddress(),
95+
'Name' => $recipient->getName(),
96+
];
97+
}, $this->getRecipients($email, $envelope)),
98+
'Subject' => $email->getSubject(),
99+
'Attachments' => $attachments,
100+
'InlinedAttachments' => $inlines,
101+
];
102+
if ($emails = $email->getCc()) {
103+
$message['Cc'] = implode(',', $this->stringifyAddresses($emails));
104+
}
105+
if ($emails = $email->getBcc()) {
106+
$message['Bcc'] = implode(',', $this->stringifyAddresses($emails));
107+
}
108+
if ($email->getTextBody()) {
109+
$message['TextPart'] = $email->getTextBody();
110+
}
111+
if ($html) {
112+
$message['HTMLPart'] = $html;
113+
}
114+
115+
return [
116+
'Messages' => [$message],
117+
];
118+
}
119+
120+
private function prepareAttachments(Email $email, ?string $html): array
121+
{
122+
$attachments = $inlines = [];
123+
foreach ($email->getAttachments() as $attachment) {
124+
$headers = $attachment->getPreparedHeaders();
125+
$filename = $headers->getHeaderParameter('Content-Disposition', 'filename');
126+
$formattedAttachment = [
127+
'ContentType' => $attachment->getMediaType().'/'.$attachment->getMediaSubtype(),
128+
'Filename' => $filename,
129+
'Base64Content' => $attachment->bodyToString(),
130+
];
131+
if ('inline' === $headers->getHeaderBody('Content-Disposition')) {
132+
$inlines[] = $formattedAttachment;
133+
} else {
134+
$attachments[] = $formattedAttachment;
135+
}
136+
}
137+
138+
return [$attachments, $inlines, $html];
139+
}
140+
141+
private function getEndpoint(): ?string
142+
{
143+
return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : '');
144+
}
145+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Mailjet\Transport;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
16+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
17+
18+
class MailjetSmtpTransport extends EsmtpTransport
19+
{
20+
public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
21+
{
22+
parent::__construct('in-v3.mailjet.com', 465, true, $dispatcher, $logger);
23+
24+
$this->setUsername($username);
25+
$this->setPassword($password);
26+
}
27+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Mailjet\Transport;
13+
14+
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
15+
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
16+
use Symfony\Component\Mailer\Transport\Dsn;
17+
use Symfony\Component\Mailer\Transport\TransportInterface;
18+
19+
class MailjetTransportFactory extends AbstractTransportFactory
20+
{
21+
public function create(Dsn $dsn): TransportInterface
22+
{
23+
$scheme = $dsn->getScheme();
24+
$user = $this->getUser($dsn);
25+
$password = $this->getPassword($dsn);
26+
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
27+
28+
if ('maijlet+api' === $scheme) {
29+
return (new MailjetApiTransport($user, $password, $this->client, $this->dispatcher, $this->logger))->setHost($host);
30+
}
31+
32+
if (\in_array($scheme, ['mailjet+smtp', 'mailjet+smtps', 'mailjet'])) {
33+
return new MailjetSmtpTransport($user, $password, $this->dispatcher, $this->logger);
34+
}
35+
36+
throw new UnsupportedSchemeException($dsn, 'mailjet', $this->getSupportedSchemes());
37+
}
38+
39+
protected function getSupportedSchemes(): array
40+
{
41+
return ['mailjet', 'mailjet+api', 'mailjet+smtp', 'mailjet+smtps'];
42+
}
43+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "symfony/mailjet-mailer",
3+
"type": "symfony-bridge",
4+
"description": "Symfony Mailjet Mailer Bridge",
5+
"keywords": [],
6+
"homepage": "https://symfony.com",
7+
"license": "MIT",
8+
"authors": [
9+
{
10+
"name": "Fabien Potencier",
11+
"email": "fabien@symfony.com"
12+
},
13+
{
14+
"name": "Symfony Community",
15+
"homepage": "https://symfony.com/contributors"
16+
}
17+
],
18+
"require": {
19+
"php": "^7.2.5",
20+
"symfony/mailer": "^4.4|^5.0"
21+
},
22+
"require-dev": {
23+
"symfony/http-client": "^4.4|^5.0"
24+
},
25+
"autoload": {
26+
"psr-4": {
27+
"Symfony\\Component\\Mailer\\Bridge\\Mailjet\\": ""
28+
},
29+
"exclude-from-classmap": [
30+
"/Tests/"
31+
]
32+
},
33+
"minimum-stability": "dev",
34+
"extra": {
35+
"branch-alias": {
36+
"dev-master": "5.2-dev"
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)