diff --git a/README.md b/README.md index f28a44e..f97fc6e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ It is very easy to use and to integrate into your application as demonstrated be * [SendGrid](https://sendgrid.com) * [MailGun](https://www.mailgun.com) * [Mandrill](https://mandrill.com/) +* [Mailjet](https://www.mailjet.com) ```php + */ +class Mailjet extends Mailer +{ + /** + * {@inheritdoc} + */ + protected function getEndpoint() + { + return 'https://api.mailjet.com/v3.1/send'; + } + + /** + * {@inheritdoc} + * + * @throws \InvalidArgumentException + */ + public function setServerToken($serverToken) + { + if (false === strpos($serverToken, ':')) { + throw new \InvalidArgumentException('Mailjet uses a "publicApiKey:privateApiKey" based ServerToken'); + } + + parent::setServerToken($serverToken); + } + + /** + * {@inheritdoc} + */ + protected function getHeaders() + { + return [ + 'Accept' => 'application/json', + 'Authorization' => sprintf('Basic %s', base64_encode($this->getServerToken())), + 'Content-Type' => 'application/json', + ]; + } + + /** + * {@inheritdoc} + */ + protected function format(MessageInterface $message) + { + $parameters = [ + 'From' => $this->buildSenderField($message->getFrom()), + 'To' => $this->buildRecipientsField($message->getTo()), + 'Cc' => $this->buildRecipientsField($message->getCc()), + 'Bcc' => $this->buildRecipientsField($message->getBcc()), + 'Subject' => $message->getSubject(), + 'Headers' => $message->getHeaders(), + 'HTMLPart' => $message->getHtml(), + 'TextPart' => $message->getText(), + 'ReplyTo' => $this->buildSenderField($message->getReplyTo()), + ]; + + $attachments = $this->processAttachments($message); + if ($attachments) { + if ($attachments['attached']) { + $parameters['Attachments'] = $attachments['attached']; + } + + if ($attachments['inlined']) { + $parameters['InlinedAttachments'] = $attachments['inlined']; + } + } + + if ($message instanceof MetadataAwareInterface) { + $parameters['EventPayload'] = $message->getMetadata(); + } + + if ($message instanceof TaggableInterface) { + $parameters['MonitoringCategory'] = $message->getTag(); + } + + return json_encode(['Messages' => [array_filter($parameters)]]); + } + + /** + * {@inheritdoc} + */ + protected function handle(ResponseInterface $response) + { + $statusCode = $response->getStatusCode(); + $httpException = new HttpException($statusCode, $response->getReasonPhrase()); + + if (!in_array($statusCode, [400, 401, 403], true)) { + throw $httpException; + } + + $error = json_decode((string) $response->getBody(), true); + + if (isset($error['ErrorMessage'])) { + throw new ApiException($error['ErrorMessage'], $httpException); + } + + $errorMessages = []; + foreach ($error['Messages'] as $message) { + if ('error' !== $message['Status']) { + continue; + } + + foreach ($message['Errors'] as $mailError) { + $errorMessages[] = $mailError['ErrorMessage']; + } + } + + throw new ApiException(implode(', ', $errorMessages), $httpException); + } + + /** + * @param IdentityInterface|string $identity + * + * @return array + */ + protected function buildSenderField($identity) + { + if (null === $identity) { + return []; + } + + if (is_string($identity)) { + return [ + 'Email' => $identity, + ]; + } + + $sender = [ + 'Email' => $identity->getEmail(), + ]; + + if (null !== $name = $identity->getName()) { + $sender['Name'] = $name; + } + + return $sender; + } + + /** + * @param IdentityInterface[]|string $identities + * + * @return array + */ + protected function buildRecipientsField($identities) + { + if (null === $identities) { + return []; + } + + if (is_string($identities)) { + return [ + [ + 'Email' => $identities, + ], + ]; + } + + $identities = (array) $identities; + + $recipients = []; + foreach ($identities as $identity) { + if (is_string($identity)) { + $recipients[] = [ + 'Email' => $identity, + ]; + + continue; + } + + $recipient = [ + 'Email' => $identity->getEmail(), + ]; + + if (null !== $name = $identity->getName()) { + $recipient['Name'] = $name; + } + + $recipients[] = $recipient; + } + + return $recipients; + } + + protected function processAttachments(MessageInterface $message) + { + if (!$message instanceof AttachmentsAwareInterface) { + return []; + } + + $processedAttachments = [ + 'attached' => [], + 'inlined' => [], + ]; + + $attachments = AttachmentUtils::processAttachments($message->getAttachments()); + foreach ($attachments as $name => $attachment) { + $item = [ + 'Filename' => $name, + 'Base64Content' => base64_encode($this->getAttachmentContent($attachment)), + 'ContentType' => $attachment->getType(), + ]; + + if (null !== $id = $attachment->getId()) { + $item['ContentID'] = $id; + $processedAttachments['inlined'][] = $item; + + continue; + } + + $processedAttachments['attached'][] = $item; + } + + return $processedAttachments; + } + + /** + * @param Attachment $attachment + * + * @return string + */ + protected function getAttachmentContent(Attachment $attachment) + { + return file_get_contents($attachment->getPath()); + } +} diff --git a/tests/Stampie/Tests/Mailer/MailjetTest.php b/tests/Stampie/Tests/Mailer/MailjetTest.php new file mode 100644 index 0000000..be41dbe --- /dev/null +++ b/tests/Stampie/Tests/Mailer/MailjetTest.php @@ -0,0 +1,406 @@ +httpClient = $this->getMockBuilder(HttpClient::class)->getMock(); + $this->mailer = new Mailjet($this->httpClient, self::SERVER_TOKEN); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Mailjet uses a "publicApiKey:privateApiKey" based ServerToken + */ + public function testServerTokenMissingDelimiter() + { + new Mailjet($this->httpClient, 'missingDelimiter'); + } + + public function testServerToken() + { + $this->assertEquals(self::SERVER_TOKEN, $this->mailer->getServerToken()); + } + + public function testSend() + { + $message = $this->getMessageMock('bob@example.com', 'alice@example.com', 'Stampie is awesome!', 'Trying out Stampie!', null, [ + 'X-Custom-Header' => 'My Custom Header Value', + ]); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (Request $request) { + $body = json_decode((string) $request->getBody(), true); + + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('https://api.mailjet.com/v3.1/send', (string) $request->getUri()); + $this->assertEquals('application/json', $request->getHeaderLine('Accept')); + $this->assertEquals('application/json', $request->getHeaderLine('Content-Type')); + $this->assertEquals(sprintf('Basic %s', base64_encode(self::SERVER_TOKEN)), $request->getHeaderLine('Authorization')); + $this->assertEquals([ + 'Messages' => [ + [ + 'From' => [ + 'Email' => 'bob@example.com', + ], + 'To' => [ + [ + 'Email' => 'alice@example.com', + ], + ], + 'Subject' => 'Stampie is awesome!', + 'HTMLPart' => 'Trying out Stampie!', + 'Headers' => [ + 'X-Custom-Header' => 'My Custom Header Value', + ], + ], + ], + ], $body); + + return true; + })) + ->willReturn(new Response()); + + $this->mailer->send($message); + } + + public function testSendTaggable() + { + $message = $this->getTaggableMessageMock('bob@example.com', 'alice@example.com', 'Stampie is awesome!', 'Trying out Stampie!', null, [], 'tag'); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (Request $request) { + $body = json_decode((string) $request->getBody(), true); + + $this->assertEquals([ + 'Messages' => [ + [ + 'From' => [ + 'Email' => 'bob@example.com', + ], + 'To' => [ + [ + 'Email' => 'alice@example.com', + ], + ], + 'Subject' => 'Stampie is awesome!', + 'HTMLPart' => 'Trying out Stampie!', + 'MonitoringCategory' => 'tag', + ], + ], + ], $body); + + return true; + })) + ->willReturn(new Response()); + + $this->mailer->send($message); + } + + public function testSendMetadataAware() + { + $message = $this->getMetadataAwareMessageMock('bob@example.com', 'alice@example.com', 'Stampie is awesome!', 'Trying out Stampie!', null, [], [ + 'client_name' => 'Stampie', + ]); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (Request $request) { + $body = json_decode((string) $request->getBody(), true); + + $this->assertEquals([ + 'Messages' => [ + [ + 'From' => [ + 'Email' => 'bob@example.com', + ], + 'To' => [ + [ + 'Email' => 'alice@example.com', + ], + ], + 'Subject' => 'Stampie is awesome!', + 'HTMLPart' => 'Trying out Stampie!', + 'EventPayload' => [ + 'client_name' => 'Stampie', + ], + ], + ], + ], $body); + + return true; + })) + ->willReturn(new Response()); + + $this->mailer->send($message); + } + + public function testSendWithAttachments() + { + $message = $this->getAttachmentsMessageMock('bob@example.com', 'alice@example.com', 'Stampie is awesome!', null, null, [], [ + $this->getAttachmentMock('path-1.txt', 'path1.txt', 'text/plain', null), + $this->getAttachmentMock('path-2.txt', 'path2.txt', 'text/plain', null), + $this->getAttachmentMock('logo.png', 'logo.png', 'image/png', 'contentid1'), + ]); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (Request $request) { + $body = json_decode((string) $request->getBody(), true); + + $this->assertEquals('application/json', $request->getHeaderLine('Content-Type')); + $this->assertEquals([ + 'Messages' => [ + [ + 'From' => [ + 'Email' => 'bob@example.com', + ], + 'To' => [ + [ + 'Email' => 'alice@example.com', + ], + ], + 'Subject' => 'Stampie is awesome!', + 'Attachments' => [ + [ + 'ContentType' => 'text/plain', + 'Filename' => 'path1.txt', + 'Base64Content' => base64_encode(file_get_contents(__DIR__.'/../../../Fixtures/path-1.txt')), + ], + [ + 'ContentType' => 'text/plain', + 'Filename' => 'path2.txt', + 'Base64Content' => base64_encode(file_get_contents(__DIR__.'/../../../Fixtures/path-2.txt')), + ], + ], + 'InlinedAttachments' => [ + [ + 'ContentType' => 'image/png', + 'Filename' => 'logo.png', + 'Base64Content' => base64_encode(file_get_contents(__DIR__.'/../../../Fixtures/logo.png')), + 'ContentID' => 'contentid1', + ], + ], + ], + ], + ], $body); + + return true; + })) + ->willReturn(new Response()); + + $this->mailer->send($message); + } + + /** + * @dataProvider senderProvider + */ + public function testFormatSender($sender, $expectedFormat) + { + $message = $this->getMessageMock($sender, 'alice@example.com', 'Stampie is awesome!'); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (Request $request) use ($expectedFormat) { + $body = json_decode((string) $request->getBody(), true); + + $this->assertEquals([ + 'Messages' => [ + [ + 'From' => $expectedFormat, + 'To' => [ + [ + 'Email' => 'alice@example.com', + ], + ], + 'Subject' => 'Stampie is awesome!', + ], + ], + ], $body); + + return true; + })) + ->willReturn(new Response()); + + $this->mailer->send($message); + } + + /** + * @dataProvider recipientsProvider + */ + public function testFormatRecipients($recipients, $expectedFormat) + { + $message = $this->getMessageMock('bob@example.com', $recipients, 'Stampie is awesome!'); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (Request $request) use ($expectedFormat) { + $body = json_decode((string) $request->getBody(), true); + + $this->assertEquals([ + 'Messages' => [ + [ + 'From' => [ + 'Email' => 'bob@example.com', + ], + 'To' => $expectedFormat, + 'Subject' => 'Stampie is awesome!', + ], + ], + ], $body); + + return true; + })) + ->willReturn(new Response()); + + $this->mailer->send($message); + } + + /** + * @dataProvider errorProvider + */ + public function testHandleError($statusCode, $content, $exceptionType, $exceptionMessage) + { + $response = new Response($statusCode, [], $content); + + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionType); + $this->expectExceptionMessage($exceptionMessage); + } else { + $this->setExpectedException($exceptionType, $exceptionMessage); + } + + $message = $this->getMessageMock('bob@example.com', 'alice@example.com', 'Stampie is awesome!'); + + $this->httpClient + ->method('sendRequest') + ->willReturn($response); + + $this->mailer->send($message); + } + + public function senderProvider() + { + return [ + [ + 'alice@example.com', + ['Email' => 'alice@example.com'], + ], + [ + new Identity('alice@example.com'), + ['Email' => 'alice@example.com'], + ], + [ + new Identity('alice@example.com', 'Alice Example'), + ['Email' => 'alice@example.com', 'Name' => 'Alice Example'], + ], + ]; + } + + public function recipientsProvider() + { + return [ + [ + 'alice@example.com', + [ + ['Email' => 'alice@example.com'], + ], + ], + [ + [new Identity('alice@example.com')], + [ + ['Email' => 'alice@example.com'], + ], + ], + [ + [new Identity('alice@example.com', 'Alice Example')], + [ + ['Email' => 'alice@example.com', 'Name' => 'Alice Example'], + ], + ], + [ + ['toto@example.com', new Identity('alice@example.com'), new Identity('henrik@bjrnskov.dk', 'Henrik Bjrnskov')], + [ + ['Email' => 'toto@example.com'], + ['Email' => 'alice@example.com'], + ['Email' => 'henrik@bjrnskov.dk', 'Name' => 'Henrik Bjrnskov'], + ], + ], + ]; + } + + public function errorProvider() + { + return [ + [500, '', HttpException::class, 'Internal Server Error'], + [400, <<