Skip to content

Commit 710e16c

Browse files
committed
feature #50302 [Mailer] New Brevo mailer bridge (formerly Sendinblue) (PEtanguy)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [Mailer] New Brevo mailer bridge (formerly Sendinblue) | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | N/A | License | MIT | Doc PR | TODO Hello, This PR is aimed at updating the config for the sendinblue mailer. As you might have seen, Sendinblue has rebranded to [Brevo](https://developers.brevo.com/) and also rewrote their apis. This change ensure compatibility with the new endpoints and removes any reference to Sendinblue. This is the notifier PR: #50296 Commits ------- f537358 [Mailer] New Brevo mailer bridge (formerly Sendinblue)
2 parents 71b0c2b + f537358 commit 710e16c

22 files changed

+695
-0
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -2589,6 +2589,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
25892589
}
25902590

25912591
$classToServices = [
2592+
MailerBridge\Brevo\Transport\BrevoTransportFactory::class => 'mailer.transport_factory.brevo',
25922593
MailerBridge\Google\Transport\GmailTransportFactory::class => 'mailer.transport_factory.gmail',
25932594
MailerBridge\Infobip\Transport\InfobipTransportFactory::class => 'mailer.transport_factory.infobip',
25942595
MailerBridge\MailerSend\Transport\MailerSendTransportFactory::class => 'mailer.transport_factory.mailersend',

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

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory;
15+
use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
1516
use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory;
1617
use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory;
1718
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory;
@@ -44,6 +45,10 @@
4445
->parent('mailer.transport_factory.abstract')
4546
->tag('mailer.transport_factory')
4647

48+
->set('mailer.transport_factory.brevo', BrevoTransportFactory::class)
49+
->parent('mailer.transport_factory.abstract')
50+
->tag('mailer.transport_factory')
51+
4752
->set('mailer.transport_factory.gmail', GmailTransportFactory::class)
4853
->parent('mailer.transport_factory.abstract')
4954
->tag('mailer.transport_factory')
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
6.4
5+
---
6+
7+
* Added the bridge as a replacement of the deprecated Sendinblue one.
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Brevo Bridge
2+
============
3+
4+
Provides Brevo integration for Symfony Mailer.
5+
This was added uppon Sendinblue's rebranding to Brevo.
6+
7+
Configuration example:
8+
9+
```env
10+
# SMTP
11+
MAILER_DSN=brevo+smtp://USERNAME:PASSWORD@default
12+
13+
# API
14+
MAILER_DSN=brevo+api://KEY@default
15+
```
16+
17+
where:
18+
- `KEY` is your Brevo API Key
19+
20+
With API, you can use custom headers.
21+
22+
```php
23+
$params = ['param1' => 'foo', 'param2' => 'bar'];
24+
$json = json_encode(['"custom_header_1' => 'custom_value_1']);
25+
26+
$email = new Email();
27+
$email
28+
->getHeaders()
29+
->add(new MetadataHeader('custom', $json))
30+
->add(new TagHeader('TagInHeaders1'))
31+
->add(new TagHeader('TagInHeaders2'))
32+
->addTextHeader('sender.ip', '1.2.3.4')
33+
->addTextHeader('templateId', 1)
34+
->addParameterizedHeader('params', 'params', $params)
35+
->addTextHeader('foo', 'bar')
36+
;
37+
```
38+
39+
This example allow you to set :
40+
41+
* templateId
42+
* params
43+
* tags
44+
* headers
45+
* sender.ip
46+
* X-Mailin-Custom
47+
48+
For more informations, you can refer to [Brevo API documentation](https://developers.brevo.com/reference/sendtransacemail).
49+
50+
Resources
51+
---------
52+
53+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
54+
* [Report issues](https://github.com/symfony/symfony/issues) and
55+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
56+
in the [main Symfony repository](https://github.com/symfony/symfony)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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\Brevo\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\Brevo\Transport\BrevoApiTransport;
18+
use Symfony\Component\Mailer\Envelope;
19+
use Symfony\Component\Mailer\Exception\HttpTransportException;
20+
use Symfony\Component\Mailer\Header\MetadataHeader;
21+
use Symfony\Component\Mailer\Header\TagHeader;
22+
use Symfony\Component\Mime\Address;
23+
use Symfony\Component\Mime\Email;
24+
use Symfony\Component\Mime\Part\DataPart;
25+
use Symfony\Contracts\HttpClient\ResponseInterface;
26+
27+
class BrevoApiTransportTest extends TestCase
28+
{
29+
/**
30+
* @dataProvider getTransportData
31+
*/
32+
public function testToString(BrevoApiTransport $transport, string $expected)
33+
{
34+
$this->assertSame($expected, (string) $transport);
35+
}
36+
37+
public static function getTransportData()
38+
{
39+
yield [
40+
new BrevoApiTransport('ACCESS_KEY'),
41+
'brevo+api://api.brevo.com',
42+
];
43+
44+
yield [
45+
(new BrevoApiTransport('ACCESS_KEY'))->setHost('example.com'),
46+
'brevo+api://example.com',
47+
];
48+
49+
yield [
50+
(new BrevoApiTransport('ACCESS_KEY'))->setHost('example.com')->setPort(99),
51+
'brevo+api://example.com:99',
52+
];
53+
}
54+
55+
public function testCustomHeader()
56+
{
57+
$params = ['param1' => 'foo', 'param2' => 'bar'];
58+
$json = json_encode(['"custom_header_1' => 'custom_value_1']);
59+
60+
$email = new Email();
61+
$email->getHeaders()
62+
->add(new MetadataHeader('custom', $json))
63+
->add(new TagHeader('TagInHeaders'))
64+
->addTextHeader('templateId', 1)
65+
->addParameterizedHeader('params', 'params', $params)
66+
->addTextHeader('foo', 'bar');
67+
$envelope = new Envelope(new Address('alice@system.com', 'Alice'), [new Address('bob@system.com', 'Bob')]);
68+
69+
$transport = new BrevoApiTransport('ACCESS_KEY');
70+
$method = new \ReflectionMethod(BrevoApiTransport::class, 'getPayload');
71+
$payload = $method->invoke($transport, $email, $envelope);
72+
73+
$this->assertArrayHasKey('X-Mailin-Custom', $payload['headers']);
74+
$this->assertEquals($json, $payload['headers']['X-Mailin-Custom']);
75+
76+
$this->assertArrayHasKey('tags', $payload);
77+
$this->assertEquals('TagInHeaders', current($payload['tags']));
78+
$this->assertArrayHasKey('templateId', $payload);
79+
$this->assertEquals(1, $payload['templateId']);
80+
$this->assertArrayHasKey('params', $payload);
81+
$this->assertEquals('foo', $payload['params']['param1']);
82+
$this->assertEquals('bar', $payload['params']['param2']);
83+
$this->assertArrayHasKey('foo', $payload['headers']);
84+
$this->assertEquals('bar', $payload['headers']['foo']);
85+
}
86+
87+
public function testSendThrowsForErrorResponse()
88+
{
89+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
90+
$this->assertSame('POST', $method);
91+
$this->assertSame('https://api.brevo.com:8984/v3/smtp/email', $url);
92+
$this->assertStringContainsString('Accept: */*', $options['headers'][2] ?? $options['request_headers'][1]);
93+
94+
return new MockResponse(json_encode(['message' => 'i\'m a teapot']), [
95+
'http_code' => 418,
96+
'response_headers' => [
97+
'content-type' => 'application/json',
98+
],
99+
]);
100+
});
101+
102+
$transport = new BrevoApiTransport('ACCESS_KEY', $client);
103+
$transport->setPort(8984);
104+
105+
$mail = new Email();
106+
$mail->subject('Hello!')
107+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
108+
->from(new Address('fabpot@symfony.com', 'Fabien'))
109+
->text('Hello There!');
110+
111+
$this->expectException(HttpTransportException::class);
112+
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
113+
$transport->send($mail);
114+
}
115+
116+
public function testSend()
117+
{
118+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
119+
$this->assertSame('POST', $method);
120+
$this->assertSame('https://api.brevo.com:8984/v3/smtp/email', $url);
121+
$this->assertStringContainsString('Accept: */*', $options['headers'][2] ?? $options['request_headers'][1]);
122+
123+
return new MockResponse(json_encode(['messageId' => 'foobar']), [
124+
'http_code' => 201,
125+
]);
126+
});
127+
128+
$transport = new BrevoApiTransport('ACCESS_KEY', $client);
129+
$transport->setPort(8984);
130+
131+
$mail = new Email();
132+
$mail->subject('Hello!')
133+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
134+
->from(new Address('fabpot@symfony.com', 'Fabien'))
135+
->text('Hello here!')
136+
->html('Hello there!')
137+
->addCc('foo@bar.fr')
138+
->addBcc('foo@bar.fr')
139+
->addReplyTo('foo@bar.fr')
140+
->addPart(new DataPart('body'));
141+
142+
$message = $transport->send($mail);
143+
144+
$this->assertSame('foobar', $message->getMessageId());
145+
}
146+
}
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\Brevo\Tests\Transport;
13+
14+
use Psr\Log\NullLogger;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoApiTransport;
17+
use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoSmtpTransport;
18+
use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
19+
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
20+
use Symfony\Component\Mailer\Transport\Dsn;
21+
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
22+
23+
class BrevoTransportFactoryTest extends TransportFactoryTestCase
24+
{
25+
public function getFactory(): TransportFactoryInterface
26+
{
27+
return new BrevoTransportFactory(null, new MockHttpClient(), new NullLogger());
28+
}
29+
30+
public static function supportsProvider(): iterable
31+
{
32+
yield [
33+
new Dsn('brevo', 'default'),
34+
true,
35+
];
36+
37+
yield [
38+
new Dsn('brevo+smtp', 'default'),
39+
true,
40+
];
41+
42+
yield [
43+
new Dsn('brevo+smtp', 'example.com'),
44+
true,
45+
];
46+
47+
yield [
48+
new Dsn('brevo+api', 'default'),
49+
true,
50+
];
51+
}
52+
53+
public static function createProvider(): iterable
54+
{
55+
yield [
56+
new Dsn('brevo', 'default', self::USER, self::PASSWORD),
57+
new BrevoSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()),
58+
];
59+
60+
yield [
61+
new Dsn('brevo+smtp', 'default', self::USER, self::PASSWORD),
62+
new BrevoSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()),
63+
];
64+
65+
yield [
66+
new Dsn('brevo+smtp', 'default', self::USER, self::PASSWORD, 465),
67+
new BrevoSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()),
68+
];
69+
70+
yield [
71+
new Dsn('brevo+api', 'default', self::USER),
72+
new BrevoApiTransport(self::USER, new MockHttpClient(), null, new NullLogger()),
73+
];
74+
}
75+
76+
public static function unsupportedSchemeProvider(): iterable
77+
{
78+
yield [
79+
new Dsn('brevo+foo', 'default', self::USER, self::PASSWORD),
80+
'The "brevo+foo" scheme is not supported; supported schemes for mailer "brevo" are: "brevo", "brevo+smtp", "brevo+api".',
81+
];
82+
}
83+
84+
public static function incompleteDsnProvider(): iterable
85+
{
86+
yield [new Dsn('brevo+smtp', 'default', self::USER)];
87+
88+
yield [new Dsn('brevo+smtp', 'default', null, self::PASSWORD)];
89+
90+
yield [new Dsn('brevo+api', 'default')];
91+
}
92+
}

0 commit comments

Comments
 (0)