Skip to content

Commit b87aa86

Browse files
feature #49461 [Mailer] Add MailerSend bridge (doobas)
This PR was squashed before being merged into the 6.3 branch. Discussion ---------- [Mailer] Add MailerSend bridge | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT | Doc PR | symfony/symfony-docs#17926 | Recipes PR | symfony/recipes#1177 Added MailerSend as new email provider for Symfony Mailer! Commits ------- fdc22ef [Mailer] Add MailerSend bridge
2 parents 4c0c383 + fdc22ef commit b87aa86

File tree

17 files changed

+666
-0
lines changed

17 files changed

+666
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory;
9999
use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory as InfobipMailerTransportFactory;
100100
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory;
101+
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory;
101102
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
102103
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
103104
use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory;
@@ -2458,6 +2459,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
24582459
$classToServices = [
24592460
GmailTransportFactory::class => 'mailer.transport_factory.gmail',
24602461
InfobipMailerTransportFactory::class => 'mailer.transport_factory.infobip',
2462+
MailerSendTransportFactory::class => 'mailer.transport_factory.mailersend',
24612463
MailgunTransportFactory::class => 'mailer.transport_factory.mailgun',
24622464
MailjetTransportFactory::class => 'mailer.transport_factory.mailjet',
24632465
MailPaceTransportFactory::class => 'mailer.transport_factory.mailpace',

src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory;
1616
use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory;
1717
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory;
18+
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory;
1819
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
1920
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
2021
use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory;
@@ -51,6 +52,10 @@
5152
->parent('mailer.transport_factory.abstract')
5253
->tag('mailer.transport_factory')
5354

55+
->set('mailer.transport_factory.mailersend', MailerSendTransportFactory::class)
56+
->parent('mailer.transport_factory.abstract')
57+
->tag('mailer.transport_factory')
58+
5459
->set('mailer.transport_factory.mailchimp', MandrillTransportFactory::class)
5560
->parent('mailer.transport_factory.abstract')
5661
->tag('mailer.transport_factory')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
vendor/
2+
composer.lock
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
6.3
5+
---
6+
7+
* Add the bridge
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2023-present 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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MailerSend Bridge
2+
=================
3+
4+
Provides MailerSend integration for Symfony Mailer.
5+
6+
## Configuration example:
7+
8+
```env
9+
# API
10+
MAILER_DSN=mailersend+api://$MAILERSEND_API_KEY@default
11+
# SMTP
12+
MAILER_DSN=mailersend+smtp://$MAILERSEND_SMTP_USERNAME:$MAILERSEND_SMTP_PASSWORD@default
13+
```
14+
15+
Resources
16+
---------
17+
18+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
19+
* [Report issues](https://github.com/symfony/symfony/issues) and
20+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
21+
in the [main Symfony repository](https://github.com/symfony/symfony)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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\MailerSend\Tests\Transport;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\HttpClient\Response\MockResponse;
17+
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendApiTransport;
18+
use Symfony\Component\Mailer\Exception\HttpTransportException;
19+
use Symfony\Component\Mime\Address;
20+
use Symfony\Component\Mime\Email;
21+
use Symfony\Component\Mime\Part\DataPart;
22+
use Symfony\Contracts\HttpClient\ResponseInterface;
23+
24+
class MailerSendApiTransportTest extends TestCase
25+
{
26+
/**
27+
* @dataProvider getTransportData
28+
*/
29+
public function testToString(MailerSendApiTransport $transport, string $expected)
30+
{
31+
$this->assertSame($expected, (string) $transport);
32+
}
33+
34+
public static function getTransportData()
35+
{
36+
yield [
37+
new MailerSendApiTransport('ACCESS_KEY'),
38+
'mailersend+api://api.mailersend.com',
39+
];
40+
41+
yield [
42+
(new MailerSendApiTransport('ACCESS_KEY'))->setHost('example.com'),
43+
'mailersend+api://example.com',
44+
];
45+
46+
yield [
47+
(new MailerSendApiTransport('ACCESS_KEY'))->setHost('example.com')->setPort(99),
48+
'mailersend+api://example.com:99',
49+
];
50+
}
51+
52+
public function testSendBasicEmail()
53+
{
54+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
55+
$this->assertSame('POST', $method);
56+
$this->assertSame('https://api.mailersend.com/v1/email', $url);
57+
58+
$body = json_decode($options['body'], true);
59+
$this->assertSame('test_from@example.com', $body['from']['email']);
60+
$this->assertSame('Test from name', $body['from']['name']);
61+
$this->assertSame('test_to@example.com', $body['to'][0]['email']);
62+
$this->assertSame('Test to name', $body['to'][0]['name']);
63+
$this->assertSame('Test subject', $body['subject']);
64+
$this->assertSame('Lorem ipsum.', $body['text']);
65+
$this->assertSame('<html><body><p>Lorem ipsum.</p></body></html>', $body['html']);
66+
67+
return new MockResponse('', [
68+
'http_code' => 202,
69+
'response_headers' => ['x-message-id' => 'test_message_id'],
70+
]);
71+
});
72+
73+
$transport = new MailerSendApiTransport('ACCESS_KEY', $client);
74+
75+
$mail = new Email();
76+
$mail->subject('Test subject')
77+
->to(new Address('test_to@example.com', 'Test to name'))
78+
->from(new Address('test_from@example.com', 'Test from name'))
79+
->addCc('test_cc@example.com')
80+
->addBcc('test_bcc@example.com')
81+
->addReplyTo('test_reply_to@example.com')
82+
->text('Lorem ipsum.')
83+
->html('<html><body><p>Lorem ipsum.</p></body></html>');
84+
85+
$message = $transport->send($mail);
86+
87+
$this->assertSame('test_message_id', $message->getMessageId());
88+
}
89+
90+
public function testSendEmailWithAttachment()
91+
{
92+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
93+
$this->assertSame('POST', $method);
94+
$this->assertSame('https://api.mailersend.com/v1/email', $url);
95+
96+
$body = json_decode($options['body'], true);
97+
98+
$this->assertSame('content', base64_decode($body['attachments'][0]['content']));
99+
$this->assertSame('attachment.txt', $body['attachments'][0]['filename']);
100+
$this->assertSame('inline content', base64_decode($body['attachments'][1]['content']));
101+
$this->assertSame('inline.txt', $body['attachments'][1]['filename']);
102+
$this->assertSame('inline', $body['attachments'][1]['disposition']);
103+
$this->assertSame('test_cid@symfony', $body['attachments'][1]['id']);
104+
105+
return new MockResponse('', [
106+
'http_code' => 202,
107+
'response_headers' => ['x-message-id' => 'test_message_id'],
108+
]);
109+
});
110+
111+
$transport = new MailerSendApiTransport('ACCESS_KEY', $client);
112+
113+
$mail = new Email();
114+
$mail->subject('Test subject')
115+
->to(new Address('test_to@example.com', 'Test to name'))
116+
->from(new Address('test_from@example.com', 'Test from name'))
117+
->addCc('test_cc@example.com')
118+
->addBcc('test_bcc@example.com')
119+
->addReplyTo('test_reply_to@example.com')
120+
->html('<html><body><p>Lorem ipsum.</p><img src="cid:test_cid@symfony"></body></html>')
121+
->addPart(new DataPart('content', 'attachment.txt', 'text/plain'))
122+
->addPart((new DataPart('inline content', 'inline.txt', 'text/plain'))->asInline()->setContentId('test_cid@symfony'));
123+
124+
$message = $transport->send($mail);
125+
126+
$this->assertSame('test_message_id', $message->getMessageId());
127+
}
128+
129+
public function testSendThrowsForErrorResponse()
130+
{
131+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
132+
return new MockResponse(json_encode(['message' => 'i\'m a teapot']), [
133+
'http_code' => 418,
134+
]);
135+
});
136+
137+
$transport = new MailerSendApiTransport('ACCESS_KEY', $client);
138+
139+
$mail = new Email();
140+
$mail->subject('Test subject')
141+
->to(new Address('test_to@example.com', 'Test to name'))
142+
->from(new Address('test_from@example.com', 'Test from name'))
143+
->text('Lorem ipsum.');
144+
145+
$this->expectException(HttpTransportException::class);
146+
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
147+
$transport->send($mail);
148+
}
149+
150+
public function testSendThrowsForAllSuppressed()
151+
{
152+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
153+
return new MockResponse(json_encode([
154+
'message' => 'There are some warnings for your request.',
155+
'warnings' => [
156+
[
157+
'type' => 'ALL_SUPPRESSED',
158+
],
159+
],
160+
], \JSON_THROW_ON_ERROR), [
161+
'http_code' => 202,
162+
]);
163+
});
164+
165+
$transport = new MailerSendApiTransport('ACCESS_KEY', $client);
166+
167+
$mail = new Email();
168+
$mail->subject('Test subject')
169+
->to(new Address('test_to@example.com', 'Test to name'))
170+
->from(new Address('test_from@example.com', 'Test from name'))
171+
->text('Lorem ipsum.');
172+
173+
$this->expectException(HttpTransportException::class);
174+
$this->expectExceptionMessage('Unable to send an email: There are some warnings for your request.');
175+
$transport->send($mail);
176+
}
177+
178+
public function testSendThrowsForBadResponse()
179+
{
180+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
181+
return new MockResponse('test', [
182+
'http_code' => 202,
183+
]);
184+
});
185+
186+
$transport = new MailerSendApiTransport('ACCESS_KEY', $client);
187+
188+
$mail = new Email();
189+
$mail->subject('Test subject')
190+
->to(new Address('test_to@example.com', 'Test to name'))
191+
->from(new Address('test_from@example.com', 'Test from name'))
192+
->text('Lorem ipsum.');
193+
194+
$this->expectException(HttpTransportException::class);
195+
$this->expectExceptionMessage('Unable to send an email: "test" (code 202).');
196+
$transport->send($mail);
197+
}
198+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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\MailerSend\Tests\Transport;
13+
14+
use Psr\Log\NullLogger;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendApiTransport;
17+
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendSmtpTransport;
18+
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory;
19+
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
20+
use Symfony\Component\Mailer\Transport\Dsn;
21+
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
22+
23+
class MailerSendTransportFactoryTest extends TransportFactoryTestCase
24+
{
25+
public function getFactory(): TransportFactoryInterface
26+
{
27+
return new MailerSendTransportFactory(null, new MockHttpClient(), new NullLogger());
28+
}
29+
30+
public static function supportsProvider(): iterable
31+
{
32+
yield [
33+
new Dsn('mailersend', 'default'),
34+
true,
35+
];
36+
37+
yield [
38+
new Dsn('mailersend+smtp', 'default'),
39+
true,
40+
];
41+
42+
yield [
43+
new Dsn('mailersend+smtp', 'example.com'),
44+
true,
45+
];
46+
47+
yield [
48+
new Dsn('mailersend+api', 'default'),
49+
true,
50+
];
51+
}
52+
53+
public static function createProvider(): iterable
54+
{
55+
yield [
56+
new Dsn('mailersend', 'default', self::USER, self::PASSWORD),
57+
new MailerSendSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()),
58+
];
59+
60+
yield [
61+
new Dsn('mailersend+smtp', 'default', self::USER, self::PASSWORD),
62+
new MailerSendSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()),
63+
];
64+
65+
yield [
66+
new Dsn('mailersend+smtp', 'default', self::USER, self::PASSWORD, 465),
67+
new MailerSendSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()),
68+
];
69+
70+
yield [
71+
new Dsn('mailersend+api', 'default', self::USER),
72+
new MailerSendApiTransport(self::USER, new MockHttpClient(), null, new NullLogger()),
73+
];
74+
}
75+
76+
public static function unsupportedSchemeProvider(): iterable
77+
{
78+
yield [
79+
new Dsn('mailersend+foo', 'default', self::USER, self::PASSWORD),
80+
'The "mailersend+foo" scheme is not supported; supported schemes for mailer "mailersend" are: "mailersend", "mailersend+smtp", "mailersend+api".',
81+
];
82+
}
83+
84+
public static function incompleteDsnProvider(): iterable
85+
{
86+
yield [new Dsn('mailersend+smtp', 'default', self::USER)];
87+
88+
yield [new Dsn('mailersend+smtp', 'default', null, self::PASSWORD)];
89+
90+
yield [new Dsn('mailersend+api', 'default')];
91+
}
92+
}

0 commit comments

Comments
 (0)