From 87fbd98a80f527b51bc7420b2da497fdb0714572 Mon Sep 17 00:00:00 2001 From: drixs6o9 Date: Thu, 18 Jun 2020 22:22:21 +0200 Subject: [PATCH 1/3] feat(transport): added headers support --- Transport/SendinblueApiTransport.php | 110 +++++++++++++++-------- Transport/SendinblueTransportFactory.php | 9 +- 2 files changed, 78 insertions(+), 41 deletions(-) diff --git a/Transport/SendinblueApiTransport.php b/Transport/SendinblueApiTransport.php index 081e33f..340b21a 100644 --- a/Transport/SendinblueApiTransport.php +++ b/Transport/SendinblueApiTransport.php @@ -5,10 +5,13 @@ 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\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -50,60 +53,50 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e return $response; } - private function getPayload(Email $email, Envelope $envelope): array + protected function stringifyAddresses(array $addresses): array { - $addressStringifier = function (Address $address) { - $stringified = ['email' => $address->getAddress()]; - - if ($address->getName()) { - $stringified['name'] = $address->getName(); - } + $stringifiedAddresses = []; + foreach ($addresses as $address) { + $stringifiedAddresses[] = $this->stringifyAddress($address); + } - return $stringified; - }; + return $stringifiedAddresses; + } + private function getPayload(Email $email, Envelope $envelope): array + { $payload = [ - 'sender' => $addressStringifier($envelope->getSender()), - 'subject' => $email->getSubject() + 'sender' => $this->stringifyAddress($envelope->getSender()), + 'to' => $this->stringifyAddresses($this->getRecipients($email, $envelope)), + 'subject' => $email->getSubject(), ]; - - // To - if ($emails = array_map($addressStringifier, $email->getTo())) { - $payload['to'] = $emails; + if ($attachements = $this->prepareAttachments($email)) { + $payload['attachment'] = $attachements; } - - // CC - if ($emails = array_map($addressStringifier, $email->getCc())) { - $payload['cc'] = $emails; + if ($emails = $email->getReplyTo()) { + $payload['replyTo'] = current($this->stringifyAddresses($emails)); } - - // BCC - if ($emails = array_map($addressStringifier, $email->getBcc())) { - $payload['bcc'] = $emails; + if ($emails = $email->getCc()) { + $payload['cc'] = $this->stringifyAddresses($emails); } - - // ReplyTo - if ($emails = array_map($addressStringifier, $email->getReplyTo())) { - $payload['replyTo'] = $emails; + if ($emails = $email->getBcc()) { + $payload['bcc'] = $this->stringifyAddresses($emails); } - - // Body - if (null !== $email->getTextBody()) { + if ($email->getTextBody()) { $payload['textContent'] = $email->getTextBody(); } - if (null !== $email->getHtmlBody()) { + if ($email->getHtmlBody()) { $payload['htmlContent'] = $email->getHtmlBody(); } - - // Attachments - if ($email->getAttachments()) { - $payload['attachment'] = $this->getAttachments($email); + if ($headersAndTags = $this->prepareHeadersAndTags($email->getHeaders())) + { + $payload = array_merge($payload, $headersAndTags); } return $payload; } - private function getAttachments(Email $email): array + private function prepareAttachments(Email $email): array { $attachments = []; foreach ($email->getAttachments() as $attachment) { @@ -121,6 +114,51 @@ private function getAttachments(Email $email): array return $attachments; } + private function prepareHeadersAndTags(Headers $headers): array + { + $headersAndTags = []; + $headersToBypass = ['from', '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'][$name] = $header->getBodyAsString(); + } + + return $headersAndTags; + } + + private function stringifyAddress(Address $address): array + { + $stringifiedAddress = ['email' => $address->getAddress()]; + + if ($address->getName()) { + $stringifiedAddress['name'] = $address->getName(); + } + + return $stringifiedAddress; + } + private function getEndpoint(): ?string { return ($this->host ?: self::SENDINBLUE_API_HOST).($this->port ? ':'.$this->port : ''); diff --git a/Transport/SendinblueTransportFactory.php b/Transport/SendinblueTransportFactory.php index e52b7d0..64324f6 100644 --- a/Transport/SendinblueTransportFactory.php +++ b/Transport/SendinblueTransportFactory.php @@ -35,11 +35,10 @@ public function create(Dsn $dsn): TransportInterface $transport = sprintf(self::NAMESPACE, 'SendinblueSmtpsTransport'); break; case 'sendinblue+api': - $key = $this->getUser($dsn); - $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); - $port = $dsn->getPort(); - return (new SendinblueApiTransport($key, $this->client, $this->dispatcher, $this->logger)) - ->setHost($host)->setPort($port); + return (new SendinblueApiTransport($this->getUser($dsn), $this->client, $this->dispatcher, $this->logger)) + ->setHost('default' === $dsn->getHost() ? null : $dsn->getHost()) + ->setPort($dsn->getPort()) + ; } return new $transport( From 347ca65660ef18091b31823ae21c5b16a29d7030 Mon Sep 17 00:00:00 2001 From: drixs6o9 Date: Thu, 18 Jun 2020 22:23:08 +0200 Subject: [PATCH 2/3] feat(transport): added test for api transport --- .../Transport/SendinblueApiTransportTest.php | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 Tests/Transport/SendinblueApiTransportTest.php diff --git a/Tests/Transport/SendinblueApiTransportTest.php b/Tests/Transport/SendinblueApiTransportTest.php new file mode 100644 index 0000000..4e25202 --- /dev/null +++ b/Tests/Transport/SendinblueApiTransportTest.php @@ -0,0 +1,142 @@ +assertSame($expected, (string) $transport); + } + + public function getTransportData() + { + yield [ + new SendinblueApiTransport('ACCESS_KEY'), + 'sendinblue+api://api.sendinblue.com', + ]; + + yield [ + (new SendinblueApiTransport('ACCESS_KEY'))->setHost('example.com'), + 'sendinblue+api://example.com', + ]; + + yield [ + (new SendinblueApiTransport('ACCESS_KEY'))->setHost('example.com')->setPort(99), + 'sendinblue+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 SendinblueApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(SendinblueApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $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.sendinblue.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 SendinblueApiTransport('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.sendinblue.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 SendinblueApiTransport('ACCESS_KEY', $client); + $transport->setPort(8984); + + $dataPart = new DataPart('body'); + $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') + ->attachPart($dataPart) + ; + + $message = $transport->send($mail); + + $this->assertSame('foobar', $message->getMessageId()); + } +} From 4f2f0f63f23895f36b0b7241cde3350c2ef9ebbf Mon Sep 17 00:00:00 2001 From: drixs6o9 Date: Thu, 18 Jun 2020 22:37:16 +0200 Subject: [PATCH 3/3] feat(transport): added documentation --- CHANGELOG.md | 9 +++++++++ README.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 281aae3..6de3be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,3 +16,12 @@ CHANGELOG ----- * Added SendinblueApiTransport (Thanks to [@ptondereau](https://github.com/ptondereau)) + +1.2.0 +----- + +* Added advanced options support for SendinblueApiTransport : + * Headers + * Tags + * TemplateId + * Params diff --git a/README.md b/README.md index 63e46ed..5b819a3 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Finally, add your Sendinblue credentials into your `.env.local` file of your pro Your MAILER_DSN can be configured as SMTP with **sendinblue** or **sendinblue+smtp** key, or be configured as STMPS with **sendinblue+smtps** key. Exemple: + ```env ###> drixs6o9/sendinblue-mailer ### SENDINBLUE_USERNAME=username @@ -55,6 +56,36 @@ MAILER_DSN=sendinblue+api://$SENDINBLUE_API_KEY@default ###< drixs6o9/sendinblue-mailer ### ``` +With HTTP 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 [Sendinblue API documentation](https://developers.sendinblue.com/reference#sendtransacemail). + Resources ---------