├── CHANGELOG.md ├── LICENSE ├── README.md ├── Transport ├── SendinblueApiTransport.php ├── SendinblueSmtpTransport.php └── SendinblueTransportFactory.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 6.4 5 | --- 6 | 7 | * Deprecate the bridge (use Brevo instead) 8 | 9 | 5.2.0 10 | ----- 11 | 12 | * Added the bridge 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-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. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sendinblue Bridge 2 | ================= 3 | 4 | Provides Sendinblue integration for Symfony Mailer. 5 | 6 | Configuration example: 7 | 8 | ```env 9 | # SMTP 10 | MAILER_DSN=sendinblue+smtp://USERNAME:PASSWORD@default 11 | 12 | # API 13 | MAILER_DSN=sendinblue+api://KEY@default 14 | ``` 15 | 16 | where: 17 | - `KEY` is your Sendinblue API Key 18 | 19 | With API, you can use custom headers. 20 | 21 | ```php 22 | $params = ['param1' => 'foo', 'param2' => 'bar']; 23 | $json = json_encode(['custom_header_1' => 'custom_value_1']); 24 | 25 | $email = new Email(); 26 | $email 27 | ->getHeaders() 28 | ->add(new MetadataHeader('custom', $json)) 29 | ->add(new TagHeader('TagInHeaders1')) 30 | ->add(new TagHeader('TagInHeaders2')) 31 | ->addTextHeader('sender.ip', '1.2.3.4') 32 | ->addTextHeader('templateId', 1) 33 | ->addParameterizedHeader('params', 'params', $params) 34 | ->addTextHeader('foo', 'bar') 35 | ; 36 | ``` 37 | 38 | This example allow you to set: 39 | 40 | * templateId 41 | * params 42 | * tags 43 | * headers 44 | * sender.ip 45 | * X-Mailin-Custom 46 | 47 | For more information, you can refer to [Sendinblue API documentation](https://developers.sendinblue.com/reference#sendtransacemail). 48 | 49 | Resources 50 | --------- 51 | 52 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 53 | * [Report issues](https://github.com/symfony/symfony/issues) and 54 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 55 | in the [main Symfony repository](https://github.com/symfony/symfony) 56 | -------------------------------------------------------------------------------- /Transport/SendinblueApiTransport.php: -------------------------------------------------------------------------------- 1 | 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\Sendinblue\Transport; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Mailer\Envelope; 17 | use Symfony\Component\Mailer\Exception\HttpTransportException; 18 | use Symfony\Component\Mailer\Header\MetadataHeader; 19 | use Symfony\Component\Mailer\Header\TagHeader; 20 | use Symfony\Component\Mailer\SentMessage; 21 | use Symfony\Component\Mailer\Transport\AbstractApiTransport; 22 | use Symfony\Component\Mime\Address; 23 | use Symfony\Component\Mime\Email; 24 | use Symfony\Component\Mime\Header\Headers; 25 | use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; 26 | use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; 27 | use Symfony\Contracts\HttpClient\HttpClientInterface; 28 | use Symfony\Contracts\HttpClient\ResponseInterface; 29 | 30 | /** 31 | * @author Yann LUCAS 32 | * 33 | * @deprecated since Symfony 6.4, use BrevoApiTransport instead 34 | */ 35 | final class SendinblueApiTransport extends AbstractApiTransport 36 | { 37 | private string $key; 38 | 39 | public function __construct(string $key, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) 40 | { 41 | $this->key = $key; 42 | 43 | parent::__construct($client, $dispatcher, $logger); 44 | } 45 | 46 | public function __toString(): string 47 | { 48 | return sprintf('sendinblue+api://%s', $this->getEndpoint()); 49 | } 50 | 51 | protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface 52 | { 53 | $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/v3/smtp/email', [ 54 | 'json' => $this->getPayload($email, $envelope), 55 | 'headers' => [ 56 | 'api-key' => $this->key, 57 | ], 58 | ]); 59 | 60 | try { 61 | $statusCode = $response->getStatusCode(); 62 | $result = $response->toArray(false); 63 | } catch (DecodingExceptionInterface) { 64 | throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).sprintf(' (code %d).', $statusCode), $response); 65 | } catch (TransportExceptionInterface $e) { 66 | throw new HttpTransportException('Could not reach the remote Sendinblue server.', $response, 0, $e); 67 | } 68 | 69 | if (201 !== $statusCode) { 70 | throw new HttpTransportException('Unable to send an email: '.($result['message'] ?? $response->getContent(false)).sprintf(' (code %d).', $statusCode), $response); 71 | } 72 | 73 | $sentMessage->setMessageId($result['messageId']); 74 | 75 | return $response; 76 | } 77 | 78 | protected function stringifyAddresses(array $addresses): array 79 | { 80 | $stringifiedAddresses = []; 81 | foreach ($addresses as $address) { 82 | $stringifiedAddresses[] = $this->stringifyAddress($address); 83 | } 84 | 85 | return $stringifiedAddresses; 86 | } 87 | 88 | private function getPayload(Email $email, Envelope $envelope): array 89 | { 90 | $payload = [ 91 | 'sender' => $this->stringifyAddress($envelope->getSender()), 92 | 'to' => $this->stringifyAddresses($this->getRecipients($email, $envelope)), 93 | 'subject' => $email->getSubject(), 94 | ]; 95 | if ($attachements = $this->prepareAttachments($email)) { 96 | $payload['attachment'] = $attachements; 97 | } 98 | if ($emails = $email->getReplyTo()) { 99 | $payload['replyTo'] = current($this->stringifyAddresses($emails)); 100 | } 101 | if ($emails = $email->getCc()) { 102 | $payload['cc'] = $this->stringifyAddresses($emails); 103 | } 104 | if ($emails = $email->getBcc()) { 105 | $payload['bcc'] = $this->stringifyAddresses($emails); 106 | } 107 | if ($email->getTextBody()) { 108 | $payload['textContent'] = $email->getTextBody(); 109 | } 110 | if ($email->getHtmlBody()) { 111 | $payload['htmlContent'] = $email->getHtmlBody(); 112 | } 113 | if ($headersAndTags = $this->prepareHeadersAndTags($email->getHeaders())) { 114 | $payload = array_merge($payload, $headersAndTags); 115 | } 116 | 117 | return $payload; 118 | } 119 | 120 | private function prepareAttachments(Email $email): array 121 | { 122 | $attachments = []; 123 | foreach ($email->getAttachments() as $attachment) { 124 | $headers = $attachment->getPreparedHeaders(); 125 | $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); 126 | 127 | $att = [ 128 | 'content' => str_replace("\r\n", '', $attachment->bodyToString()), 129 | 'name' => $filename, 130 | ]; 131 | 132 | $attachments[] = $att; 133 | } 134 | 135 | return $attachments; 136 | } 137 | 138 | private function prepareHeadersAndTags(Headers $headers): array 139 | { 140 | $headersAndTags = []; 141 | $headersToBypass = ['from', 'sender', 'to', 'cc', 'bcc', 'subject', 'reply-to', 'content-type', 'accept', 'api-key']; 142 | foreach ($headers->all() as $name => $header) { 143 | if (\in_array($name, $headersToBypass, true)) { 144 | continue; 145 | } 146 | if ($header instanceof TagHeader) { 147 | $headersAndTags['tags'][] = $header->getValue(); 148 | 149 | continue; 150 | } 151 | if ($header instanceof MetadataHeader) { 152 | $headersAndTags['headers']['X-Mailin-'.ucfirst(strtolower($header->getKey()))] = $header->getValue(); 153 | 154 | continue; 155 | } 156 | if ('templateid' === $name) { 157 | $headersAndTags[$header->getName()] = (int) $header->getValue(); 158 | 159 | continue; 160 | } 161 | if ('params' === $name) { 162 | $headersAndTags[$header->getName()] = $header->getParameters(); 163 | 164 | continue; 165 | } 166 | $headersAndTags['headers'][$header->getName()] = $header->getBodyAsString(); 167 | } 168 | 169 | return $headersAndTags; 170 | } 171 | 172 | private function stringifyAddress(Address $address): array 173 | { 174 | $stringifiedAddress = ['email' => $address->getEncodedAddress()]; 175 | 176 | if ($address->getName()) { 177 | $stringifiedAddress['name'] = $address->getName(); 178 | } 179 | 180 | return $stringifiedAddress; 181 | } 182 | 183 | private function getEndpoint(): ?string 184 | { 185 | return ($this->host ?: 'api.brevo.com').($this->port ? ':'.$this->port : ''); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Transport/SendinblueSmtpTransport.php: -------------------------------------------------------------------------------- 1 | 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\Sendinblue\Transport; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; 17 | 18 | /** 19 | * @author Yann LUCAS 20 | * 21 | * @deprecated since Symfony 6.4, use BrevoSmtpTransport instead 22 | */ 23 | final class SendinblueSmtpTransport extends EsmtpTransport 24 | { 25 | public function __construct(string $username, #[\SensitiveParameter] string $password, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) 26 | { 27 | parent::__construct('smtp-relay.brevo.com', 465, true, $dispatcher, $logger); 28 | 29 | $this->setUsername($username); 30 | $this->setPassword($password); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Transport/SendinblueTransportFactory.php: -------------------------------------------------------------------------------- 1 | 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\Sendinblue\Transport; 13 | 14 | use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; 15 | use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; 16 | use Symfony\Component\Mailer\Transport\AbstractTransportFactory; 17 | use Symfony\Component\Mailer\Transport\Dsn; 18 | use Symfony\Component\Mailer\Transport\TransportInterface; 19 | 20 | /** 21 | * @author Yann LUCAS 22 | * 23 | * @deprecated since Symfony 6.4, use BrevoTransportFactory instead 24 | */ 25 | final class SendinblueTransportFactory extends AbstractTransportFactory 26 | { 27 | public function create(Dsn $dsn): TransportInterface 28 | { 29 | trigger_deprecation('symfony/sendinblue-mailer', '6.4', 'The "%s" class is deprecated, use "%s" instead.', self::class, BrevoTransportFactory::class); 30 | 31 | if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { 32 | throw new UnsupportedSchemeException($dsn, 'sendinblue', $this->getSupportedSchemes()); 33 | } 34 | 35 | switch ($dsn->getScheme()) { 36 | default: 37 | case 'sendinblue': 38 | case 'sendinblue+smtp': 39 | $transport = SendinblueSmtpTransport::class; 40 | break; 41 | case 'sendinblue+api': 42 | return (new SendinblueApiTransport($this->getUser($dsn), $this->client, $this->dispatcher, $this->logger)) 43 | ->setHost('default' === $dsn->getHost() ? null : $dsn->getHost()) 44 | ->setPort($dsn->getPort()) 45 | ; 46 | } 47 | 48 | return new $transport($this->getUser($dsn), $this->getPassword($dsn), $this->dispatcher, $this->logger); 49 | } 50 | 51 | protected function getSupportedSchemes(): array 52 | { 53 | return ['sendinblue', 'sendinblue+smtp', 'sendinblue+api']; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/sendinblue-mailer", 3 | "type": "symfony-mailer-bridge", 4 | "description": "Symfony Sendinblue 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": ">=8.1", 20 | "symfony/deprecation-contracts": "^2.5|^3", 21 | "symfony/mailer": "^5.4.21|^6.2.7|^7.0" 22 | }, 23 | "require-dev": { 24 | "symfony/http-client": "^6.3|^7.0" 25 | }, 26 | "conflict": { 27 | "symfony/mime": "<6.2" 28 | }, 29 | "autoload": { 30 | "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Sendinblue\\": "" }, 31 | "exclude-from-classmap": [ 32 | "/Tests/" 33 | ] 34 | }, 35 | "minimum-stability": "dev" 36 | } 37 | --------------------------------------------------------------------------------