diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 981a865672296..291dfc40cac9e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2589,6 +2589,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co } $classToServices = [ + MailerBridge\Brevo\Transport\BrevoTransportFactory::class => 'mailer.transport_factory.brevo', MailerBridge\Google\Transport\GmailTransportFactory::class => 'mailer.transport_factory.gmail', MailerBridge\Infobip\Transport\InfobipTransportFactory::class => 'mailer.transport_factory.infobip', MailerBridge\MailerSend\Transport\MailerSendTransportFactory::class => 'mailer.transport_factory.mailersend', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index d352eb5bee856..fa2166e6f0878 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; @@ -44,6 +45,10 @@ ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.brevo', BrevoTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.gmail', GmailTransportFactory::class) ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/.gitattributes b/src/Symfony/Component/Mailer/Bridge/Brevo/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/.gitignore b/src/Symfony/Component/Mailer/Bridge/Brevo/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Brevo/CHANGELOG.md new file mode 100644 index 0000000000000..3788fe4239c0d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +6.4 +--- + +* Added the bridge as a replacement of the deprecated Sendinblue one. diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/LICENSE b/src/Symfony/Component/Mailer/Bridge/Brevo/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present 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/Brevo/README.md b/src/Symfony/Component/Mailer/Bridge/Brevo/README.md new file mode 100644 index 0000000000000..4e63caa391f1b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/README.md @@ -0,0 +1,56 @@ +Brevo Bridge +============ + +Provides Brevo integration for Symfony Mailer. +This was added uppon Sendinblue's rebranding to Brevo. + +Configuration example: + +```env +# SMTP +MAILER_DSN=brevo+smtp://USERNAME:PASSWORD@default + +# API +MAILER_DSN=brevo+api://KEY@default +``` + +where: +- `KEY` is your Brevo API Key + +With API, you can use custom headers. + +```php +$params = ['param1' => 'foo', 'param2' => 'bar']; +$json = json_encode(['"custom_header_1' => 'custom_value_1']); + +$email = new Email(); +$email + ->getHeaders() + ->add(new MetadataHeader('custom', $json)) + ->add(new TagHeader('TagInHeaders1')) + ->add(new TagHeader('TagInHeaders2')) + ->addTextHeader('sender.ip', '1.2.3.4') + ->addTextHeader('templateId', 1) + ->addParameterizedHeader('params', 'params', $params) + ->addTextHeader('foo', 'bar') +; +``` + +This example allow you to set : + +* templateId +* params +* tags +* headers + * sender.ip + * X-Mailin-Custom + +For more informations, you can refer to [Brevo API documentation](https://developers.brevo.com/reference/sendtransacemail). + +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/Brevo/Tests/Transport/BrevoApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Transport/BrevoApiTransportTest.php new file mode 100644 index 0000000000000..f7fc0b7b91976 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Transport/BrevoApiTransportTest.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Brevo\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\Header\MetadataHeader; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class BrevoApiTransportTest extends TestCase +{ + /** + * @dataProvider getTransportData + */ + public function testToString(BrevoApiTransport $transport, string $expected) + { + $this->assertSame($expected, (string) $transport); + } + + public static function getTransportData() + { + yield [ + new BrevoApiTransport('ACCESS_KEY'), + 'brevo+api://api.brevo.com', + ]; + + yield [ + (new BrevoApiTransport('ACCESS_KEY'))->setHost('example.com'), + 'brevo+api://example.com', + ]; + + yield [ + (new BrevoApiTransport('ACCESS_KEY'))->setHost('example.com')->setPort(99), + 'brevo+api://example.com:99', + ]; + } + + public function testCustomHeader() + { + $params = ['param1' => 'foo', 'param2' => 'bar']; + $json = json_encode(['"custom_header_1' => 'custom_value_1']); + + $email = new Email(); + $email->getHeaders() + ->add(new MetadataHeader('custom', $json)) + ->add(new TagHeader('TagInHeaders')) + ->addTextHeader('templateId', 1) + ->addParameterizedHeader('params', 'params', $params) + ->addTextHeader('foo', 'bar'); + $envelope = new Envelope(new Address('alice@system.com', 'Alice'), [new Address('bob@system.com', 'Bob')]); + + $transport = new BrevoApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(BrevoApiTransport::class, 'getPayload'); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('X-Mailin-Custom', $payload['headers']); + $this->assertEquals($json, $payload['headers']['X-Mailin-Custom']); + + $this->assertArrayHasKey('tags', $payload); + $this->assertEquals('TagInHeaders', current($payload['tags'])); + $this->assertArrayHasKey('templateId', $payload); + $this->assertEquals(1, $payload['templateId']); + $this->assertArrayHasKey('params', $payload); + $this->assertEquals('foo', $payload['params']['param1']); + $this->assertEquals('bar', $payload['params']['param2']); + $this->assertArrayHasKey('foo', $payload['headers']); + $this->assertEquals('bar', $payload['headers']['foo']); + } + + public function testSendThrowsForErrorResponse() + { + $client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.brevo.com:8984/v3/smtp/email', $url); + $this->assertStringContainsString('Accept: */*', $options['headers'][2] ?? $options['request_headers'][1]); + + return new MockResponse(json_encode(['message' => 'i\'m a teapot']), [ + 'http_code' => 418, + 'response_headers' => [ + 'content-type' => 'application/json', + ], + ]); + }); + + $transport = new BrevoApiTransport('ACCESS_KEY', $client); + $transport->setPort(8984); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello There!'); + + $this->expectException(HttpTransportException::class); + $this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).'); + $transport->send($mail); + } + + public function testSend() + { + $client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.brevo.com:8984/v3/smtp/email', $url); + $this->assertStringContainsString('Accept: */*', $options['headers'][2] ?? $options['request_headers'][1]); + + return new MockResponse(json_encode(['messageId' => 'foobar']), [ + 'http_code' => 201, + ]); + }); + + $transport = new BrevoApiTransport('ACCESS_KEY', $client); + $transport->setPort(8984); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello here!') + ->html('Hello there!') + ->addCc('foo@bar.fr') + ->addBcc('foo@bar.fr') + ->addReplyTo('foo@bar.fr') + ->addPart(new DataPart('body')); + + $message = $transport->send($mail); + + $this->assertSame('foobar', $message->getMessageId()); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Transport/BrevoTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Transport/BrevoTransportFactoryTest.php new file mode 100644 index 0000000000000..0d0f00e0a804c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Transport/BrevoTransportFactoryTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Brevo\Tests\Transport; + +use Psr\Log\NullLogger; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoApiTransport; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoSmtpTransport; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; +use Symfony\Component\Mailer\Test\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class BrevoTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new BrevoTransportFactory(null, new MockHttpClient(), new NullLogger()); + } + + public static function supportsProvider(): iterable + { + yield [ + new Dsn('brevo', 'default'), + true, + ]; + + yield [ + new Dsn('brevo+smtp', 'default'), + true, + ]; + + yield [ + new Dsn('brevo+smtp', 'example.com'), + true, + ]; + + yield [ + new Dsn('brevo+api', 'default'), + true, + ]; + } + + public static function createProvider(): iterable + { + yield [ + new Dsn('brevo', 'default', self::USER, self::PASSWORD), + new BrevoSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()), + ]; + + yield [ + new Dsn('brevo+smtp', 'default', self::USER, self::PASSWORD), + new BrevoSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()), + ]; + + yield [ + new Dsn('brevo+smtp', 'default', self::USER, self::PASSWORD, 465), + new BrevoSmtpTransport(self::USER, self::PASSWORD, null, new NullLogger()), + ]; + + yield [ + new Dsn('brevo+api', 'default', self::USER), + new BrevoApiTransport(self::USER, new MockHttpClient(), null, new NullLogger()), + ]; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield [ + new Dsn('brevo+foo', 'default', self::USER, self::PASSWORD), + 'The "brevo+foo" scheme is not supported; supported schemes for mailer "brevo" are: "brevo", "brevo+smtp", "brevo+api".', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield [new Dsn('brevo+smtp', 'default', self::USER)]; + + yield [new Dsn('brevo+smtp', 'default', null, self::PASSWORD)]; + + yield [new Dsn('brevo+api', 'default')]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoApiTransport.php new file mode 100644 index 0000000000000..d5facfaf69231 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoApiTransport.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Brevo\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\Header\MetadataHeader; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\AbstractApiTransport; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Pierre TANGUY + */ +final class BrevoApiTransport extends AbstractApiTransport +{ + private string $key; + + public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + $this->key = $key; + + parent::__construct($client, $dispatcher, $logger); + } + + public function __toString(): string + { + return sprintf('brevo+api://%s', $this->getEndpoint()); + } + + protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface + { + $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/v3/smtp/email', [ + 'json' => $this->getPayload($email, $envelope), + 'headers' => [ + 'api-key' => $this->key, + ], + ]); + + try { + $statusCode = $response->getStatusCode(); + $result = $response->toArray(false); + } catch (DecodingExceptionInterface) { + throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).sprintf(' (code %d).', $statusCode), $response); + } catch (TransportExceptionInterface $e) { + throw new HttpTransportException('Could not reach the remote Brevo server.', $response, 0, $e); + } + + if (201 !== $statusCode) { + throw new HttpTransportException('Unable to send an email: '.$result['message'].sprintf(' (code %d).', $statusCode), $response); + } + + $sentMessage->setMessageId($result['messageId']); + + return $response; + } + + /** + * @return list + */ + private function formatAddresses(array $addresses): array + { + $formattedAddresses = []; + foreach ($addresses as $address) { + $formattedAddresses[] = $this->formatAddress($address); + } + + return $formattedAddresses; + } + + private function getPayload(Email $email, Envelope $envelope): array + { + $payload = [ + 'sender' => $this->formatAddress($envelope->getSender()), + 'to' => $this->formatAddresses($this->getRecipients($email, $envelope)), + 'subject' => $email->getSubject(), + ]; + if ($attachements = $this->prepareAttachments($email)) { + $payload['attachment'] = $attachements; + } + if ($emails = $email->getReplyTo()) { + $payload['replyTo'] = current($this->formatAddresses($emails)); + } + if ($emails = $email->getCc()) { + $payload['cc'] = $this->formatAddresses($emails); + } + if ($emails = $email->getBcc()) { + $payload['bcc'] = $this->formatAddresses($emails); + } + if ($email->getTextBody()) { + $payload['textContent'] = $email->getTextBody(); + } + if ($email->getHtmlBody()) { + $payload['htmlContent'] = $email->getHtmlBody(); + } + if ($headersAndTags = $this->prepareHeadersAndTags($email->getHeaders())) { + $payload = array_merge($payload, $headersAndTags); + } + + return $payload; + } + + private function prepareAttachments(Email $email): array + { + $attachments = []; + foreach ($email->getAttachments() as $attachment) { + $headers = $attachment->getPreparedHeaders(); + $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); + + $att = [ + 'content' => str_replace("\r\n", '', $attachment->bodyToString()), + 'name' => $filename, + ]; + + $attachments[] = $att; + } + + return $attachments; + } + + private function prepareHeadersAndTags(Headers $headers): array + { + $headersAndTags = []; + $headersToBypass = ['from', 'sender', 'to', 'cc', 'bcc', 'subject', 'reply-to', 'content-type', 'accept', 'api-key']; + foreach ($headers->all() as $name => $header) { + if (\in_array($name, $headersToBypass, true)) { + continue; + } + if ($header instanceof TagHeader) { + $headersAndTags['tags'][] = $header->getValue(); + + continue; + } + if ($header instanceof MetadataHeader) { + $headersAndTags['headers']['X-Mailin-'.ucfirst(strtolower($header->getKey()))] = $header->getValue(); + + continue; + } + if ('templateid' === $name) { + $headersAndTags[$header->getName()] = (int) $header->getValue(); + + continue; + } + if ('params' === $name) { + $headersAndTags[$header->getName()] = $header->getParameters(); + + continue; + } + $headersAndTags['headers'][$header->getName()] = $header->getBodyAsString(); + } + + return $headersAndTags; + } + + private function formatAddress(Address $address): array + { + $formattedAddress = ['email' => $address->getAddress()]; + + if ($address->getName()) { + $formattedAddress['name'] = $address->getName(); + } + + return $formattedAddress; + } + + private function getEndpoint(): ?string + { + return ($this->host ?: 'api.brevo.com').($this->port ? ':'.$this->port : ''); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoSmtpTransport.php new file mode 100644 index 0000000000000..4094ca20d42bc --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoSmtpTransport.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Brevo\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * @author Pierre TANGUY + */ +final class BrevoSmtpTransport extends EsmtpTransport +{ + public function __construct(string $username, #[\SensitiveParameter] string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + // This is not a typo: For now the smtp relay is still under sendinblue.com unlike the api. + parent::__construct('smtp-relay.sendinblue.com', 465, true, $dispatcher, $logger); + + $this->setUsername($username); + $this->setPassword($password); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoTransportFactory.php new file mode 100644 index 0000000000000..3bc6c23d63dc2 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Transport/BrevoTransportFactory.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Brevo\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; + +/** + * @author Pierre TANGUY + */ +final class BrevoTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { + throw new UnsupportedSchemeException($dsn, 'brevo', $this->getSupportedSchemes()); + } + + switch ($dsn->getScheme()) { + default: + case 'brevo': + case 'brevo+smtp': + $transport = BrevoSmtpTransport::class; + break; + case 'brevo+api': + return (new BrevoApiTransport($this->getUser($dsn), $this->client, $this->dispatcher, $this->logger)) + ->setHost('default' === $dsn->getHost() ? null : $dsn->getHost()) + ->setPort($dsn->getPort()) + ; + } + + return new $transport($this->getUser($dsn), $this->getPassword($dsn), $this->dispatcher, $this->logger); + } + + protected function getSupportedSchemes(): array + { + return ['brevo', 'brevo+smtp', 'brevo+api']; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/composer.json b/src/Symfony/Component/Mailer/Bridge/Brevo/composer.json new file mode 100644 index 0000000000000..dc5a0c996b2ff --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/brevo-mailer", + "type": "symfony-mailer-bridge", + "description": "Symfony Brevo Mailer Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Pierre Tanguy", + "homepage": "https://github.com/petanguy" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/mailer": "^5.4.21|^6.2.7" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0" + }, + "conflict": { + "symfony/mime": "<6.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Brevo\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/phpunit.xml.dist b/src/Symfony/Component/Mailer/Bridge/Brevo/phpunit.xml.dist new file mode 100644 index 0000000000000..e7fba0dbaf8e8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/Mailer/Bridge/Sendinblue/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Sendinblue/CHANGELOG.md index 0d994e934e55a..3aa1d08e33277 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendinblue/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Sendinblue/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.3 +----- + +* Deprecated the bridge and replaced it with Brevo in reaction to their rebranding. + 5.2.0 ----- diff --git a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueApiTransportTest.php index 2a6825b3a623a..9962005775fd3 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueApiTransportTest.php @@ -24,6 +24,9 @@ use Symfony\Component\Mime\Part\DataPart; use Symfony\Contracts\HttpClient\ResponseInterface; +/** + * @group legacy + */ class SendinblueApiTransportTest extends TestCase { /** diff --git a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueTransportFactoryTest.php index 08a5048c53d9e..fbcc27cc40d9e 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Tests/Transport/SendinblueTransportFactoryTest.php @@ -20,6 +20,9 @@ use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\TransportFactoryInterface; +/** + * @group legacy + */ class SendinblueTransportFactoryTest extends TransportFactoryTestCase { public function getFactory(): TransportFactoryInterface diff --git a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueApiTransport.php index d233769a37394..740de29ebbf8b 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueApiTransport.php @@ -29,6 +29,8 @@ /** * @author Yann LUCAS + * + * @deprecated since Symfony 6.3, use BrevoApiTransport instead */ final class SendinblueApiTransport extends AbstractApiTransport { diff --git a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueSmtpTransport.php index 797cf7c3b0b65..9bee7af91d497 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueSmtpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueSmtpTransport.php @@ -17,6 +17,8 @@ /** * @author Yann LUCAS + * + * @deprecated since Symfony 6.3, use BrevoApiTransport instead */ final class SendinblueSmtpTransport extends EsmtpTransport { diff --git a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueTransportFactory.php index 400c25f194115..e0d35c7bd69a3 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueTransportFactory.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendinblue/Transport/SendinblueTransportFactory.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Mailer\Bridge\Sendinblue\Transport; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; use Symfony\Component\Mailer\Transport\AbstractTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; @@ -18,11 +19,15 @@ /** * @author Yann LUCAS + * + * @deprecated since Symfony 6.3, use BrevoTransportFactory instead */ final class SendinblueTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { + trigger_deprecation('symfony/sendinblue-mailer', '6.3', 'The "%s" class is deprecated, use "%s" instead.', self::class, BrevoTransportFactory::class); + if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { throw new UnsupportedSchemeException($dsn, 'sendinblue', $this->getSupportedSchemes()); } diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php index b0612b23808fe..e323fece5ca1e 100644 --- a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php @@ -20,6 +20,10 @@ class UnsupportedSchemeException extends LogicException { private const SCHEME_TO_PACKAGE_MAP = [ + 'brevo' => [ + 'class' => Bridge\Brevo\Transport\BrevoTransportFactory::class, + 'package' => 'symfony/brevo-mailer', + ], 'gmail' => [ 'class' => Bridge\Google\Transport\GmailTransportFactory::class, 'package' => 'symfony/google-mailer', diff --git a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php index 5dcf0f1bbfd7c..30850429af9d2 100644 --- a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ClassExistsMock; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; @@ -36,6 +37,7 @@ public static function setUpBeforeClass(): void { ClassExistsMock::register(__CLASS__); ClassExistsMock::withMockedClasses([ + BrevoTransportFactory::class => false, GmailTransportFactory::class => false, InfobipTransportFactory::class => false, MailerSendTransportFactory::class => false, @@ -65,6 +67,7 @@ public function testMessageWhereSchemeIsPartOfSchemeToPackageMap(string $scheme, public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \Generator { + yield ['brevo', 'symfony/brevo-mailer']; yield ['gmail', 'symfony/google-mailer']; yield ['infobip', 'symfony/infobip-mailer']; yield ['mailersend', 'symfony/mailersend-mailer'];