diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7e692..51dcbe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [3.4.0] - 2025-07-04 +- Add Batch sending functionality (transactional, bulk and sandbox) + ## [3.3.0] - 2025-06-17 - Add Email Sending Suppressions API diff --git a/README.md b/README.md index 11a77fa..1fdecc6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -Official Mailtrap PHP client -=============== +# Mailtrap PHP client - Official + ![GitHub Actions](https://github.com/railsware/mailtrap-php/actions/workflows/ci-phpunit.yml/badge.svg) ![GitHub Actions](https://github.com/railsware/mailtrap-php/actions/workflows/ci-psalm.yaml/badge.svg) @@ -7,6 +7,35 @@ Official Mailtrap PHP client [![Latest Version on Packagist](https://img.shields.io/packagist/v/railsware/mailtrap-php.svg?style=flat)](https://packagist.org/packages/railsware/mailtrap-php) [![Total Downloads](https://img.shields.io/packagist/dt/railsware/mailtrap-php.svg?style=flat)](https://packagist.org/packages/railsware/mailtrap-php) +## Prerequisites + +To get the most of this official Mailtrap.io PHP SDK: +- [Create a Mailtrap account](https://mailtrap.io/signup) +- [Verify your domain](https://mailtrap.io/sending/domains) + +## Supported functionality + +It supports Symphony and Laravel integrations. + +Currently with this SDK you can: +- Email API/SMTP + - Send an email (Transactional and Bulk streams) + - Send an email with a Template + - Send a batch of emails (Transactional and Bulk streams) +- Email Sandbox + - Send an email + - Send an email with a template + - Send a batch of emails + - Message management + - Inbox management + - Project management +- Contact management + - Contacts CRUD + - Lists CRUD +- General + - Templates CRUD + - Suppressions management (find and delete) + ## Installation You can install the package via [composer](http://getcomposer.org/) @@ -56,13 +85,13 @@ $email = (new MailtrapEmail()) ->addCc('staging@example.com') ->bcc('mailtrapdev@example.com') ->subject('Best practices of building HTML emails') - ->text('Hey! Learn the best practices of building HTML emails and play with ready-to-go templates. Mailtrap’s Guide on How to Build HTML Email is live on our blog') + ->text('Hey! Learn the best practices of building HTML emails and play with ready-to-go templates. Mailtrap's Guide on How to Build HTML Email is live on our blog') ->html( '


Hey
Learn the best practices of building HTML emails and play with ready-to-go templates.

-

Mailtrap’s Guide on How to Build HTML Email is live on our blog

+

Mailtrap's Guide on How to Build HTML Email is live on our blog

' diff --git a/examples/sending/emails.php b/examples/sending/emails.php index 71660a3..35cbcda 100644 --- a/examples/sending/emails.php +++ b/examples/sending/emails.php @@ -226,3 +226,111 @@ } catch (Exception $e) { echo 'Caught exception: ', $e->getMessage(), "\n"; } + + +/********************************************************************************************************************** + ******************************************* EMAIL BATCH SENDING ******************************************************* + ********************************************************************************************************************** + */ + +/** + * Email Batch Sending API (Transactional OR Bulk) + * + * Batch send email (text, html, text&html, templates). + * Please note that the endpoint will return a 200-level http status, even when sending for individual messages may fail. + * Users of this endpoint should check the success and errors for each message in the response (the results are ordered the same as the original messages - requests). + * Please note that the endpoint accepts up to 500 messages per API call, and up to 50 MB payload size, including attachments. + */ +try { + // Choose either Transactional API or Bulk API + // For Transactional API + $mailtrap = MailtrapClient::initSendingEmails( + apiKey: getenv('MAILTRAP_API_KEY'), // Your API token from https://mailtrap.io/api-tokens + ); + + // OR for Bulk API (uncomment the line below and comment out the transactional initialization) + // $mailtrap = MailtrapClient::initSendingEmails( + // apiKey: getenv('MAILTRAP_API_KEY'), // Your API token from https://mailtrap.io/api-tokens + // isBulk: true // Enable bulk sending + //); + + $baseEmail = (new MailtrapEmail()) + ->from(new Address('example@YOUR-DOMAIN-HERE.com', 'Mailtrap Test')) // Use your domain installed in Mailtrap + ->subject('Batch Email Subject') + ->text('Batch email text') + ->html('

Batch email text

'); + + $recipientEmails = [ + (new MailtrapEmail())->to(new Address('recipient1@example.com', 'Recipient 1')), + (new MailtrapEmail())->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $response = $mailtrap->batchSend($recipientEmails, $baseEmail); + + var_dump(ResponseHelper::toArray($response)); // Output response body as array +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} + + +/** + * Email Batch Sending WITH TEMPLATE (Transactional OR Bulk) + * + * WARNING! If a template is provided, then subject, text, html, category and other params are forbidden. + * + * UUID of email template. Subject, text and html will be generated from template using optional template_variables. + * Optional template variables that will be used to generate actual subject, text and html from email template + */ +try { + // Choose either Transactional API or Bulk API + // For Transactional API + $mailtrap = MailtrapClient::initSendingEmails( + apiKey: getenv('MAILTRAP_API_KEY'), // Your API token from https://mailtrap.io/api-tokens + ); + + // OR for Bulk API (uncomment the line below and comment out the transactional initialization) + // $mailtrap = MailtrapClient::initSendingEmails( + // apiKey: getenv('MAILTRAP_API_KEY'), // Your API token from https://mailtrap.io/api-tokens + // isBulk: true // Enable bulk sending + //); + + $baseEmail = (new MailtrapEmail()) + ->from(new Address('example@YOUR-DOMAIN-HERE.com', 'Mailtrap Test')) // Use your domain installed in Mailtrap + ->templateUuid('bfa432fd-0000-0000-0000-8493da283a69') // Template UUID + ->templateVariables([ + 'user_name' => 'Jon Bush', + 'next_step_link' => 'https://mailtrap.io/', + 'get_started_link' => 'https://mailtrap.io/', + 'company' => [ + 'name' => 'Best Company', + 'address' => 'Its Address', + ], + 'products' => [ + [ + 'name' => 'Product 1', + 'price' => 100, + ], + [ + 'name' => 'Product 2', + 'price' => 200, + ], + ], + ]); + + $recipientEmails = [ + (new MailtrapEmail()) + ->to(new Address('recipient1@example.com', 'Recipient 1')) + // Optional: Override template variables for this recipient + ->templateVariables([ + 'user_name' => 'Custom User 1', + ]), + (new MailtrapEmail()) + ->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $response = $mailtrap->batchSend($recipientEmails, $baseEmail); + + var_dump(ResponseHelper::toArray($response)); // Output response body as array +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} diff --git a/examples/testing/emails.php b/examples/testing/emails.php index 6be4008..cc00953 100644 --- a/examples/testing/emails.php +++ b/examples/testing/emails.php @@ -124,3 +124,98 @@ } catch (Exception $e) { echo 'Caught exception: ', $e->getMessage(), "\n"; } + +/********************************************************************************************************************** + ******************************************* EMAIL BATCH SENDING ******************************************************* + ********************************************************************************************************************** + */ + +/** + * Test Email Batch + * + * Batch send email (text, html, text&html, templates). + * Please note that the endpoint will return a 200-level http status, even when sending for individual messages may fail. + * Users of this endpoint should check the success and errors for each message in the response (the results are ordered the same as the original messages - requests). + * Please note that the endpoint accepts up to 500 messages per API call, and up to 50 MB payload size, including attachments. + */ +try { + $mailtrap = MailtrapClient::initSendingEmails( + apiKey: getenv('MAILTRAP_API_KEY'), #your API token from here https://mailtrap.io/api-tokens + isSandbox: true, # Sandbox sending (@see https://help.mailtrap.io/article/109-getting-started-with-mailtrap-email-testing) + inboxId: getenv('MAILTRAP_INBOX_ID') # required param for sandbox sending + ); + + $baseEmail = (new MailtrapEmail()) + ->from(new Address('example@YOUR-DOMAIN-HERE.com', 'Mailtrap Test')) // Use your domain installed in Mailtrap + ->subject('Batch Email Subject') + ->text('Batch email text') + ->html('

Batch email text

'); + + $recipientEmails = [ + (new MailtrapEmail())->to(new Address('recipient1@example.com', 'Recipient 1')), + (new MailtrapEmail())->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $response = $mailtrap->batchSend($recipientEmails, $baseEmail); + + var_dump(ResponseHelper::toArray($response)); // Output response body as array +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} + + +/** + * Test Email Batch WITH TEMPLATE + * + * WARNING! If a template is provided, then subject, text, html, category and other params are forbidden. + * + * UUID of email template. Subject, text and html will be generated from template using optional template_variables. + * Optional template variables that will be used to generate actual subject, text and html from email template + */ +try { + $mailtrap = MailtrapClient::initSendingEmails( + apiKey: getenv('MAILTRAP_API_KEY'), #your API token from here https://mailtrap.io/api-tokens + isSandbox: true, # Sandbox sending (@see https://help.mailtrap.io/article/109-getting-started-with-mailtrap-email-testing) + inboxId: getenv('MAILTRAP_INBOX_ID') # required param for sandbox sending + ); + + $baseEmail = (new MailtrapEmail()) + ->from(new Address('example@YOUR-DOMAIN-HERE.com', 'Mailtrap Test')) // Use your domain installed in Mailtrap + ->templateUuid('bfa432fd-0000-0000-0000-8493da283a69') // Template UUID + ->templateVariables([ + 'user_name' => 'Jon Bush', + 'next_step_link' => 'https://mailtrap.io/', + 'get_started_link' => 'https://mailtrap.io/', + 'company' => [ + 'name' => 'Best Company', + 'address' => 'Its Address', + ], + 'products' => [ + [ + 'name' => 'Product 1', + 'price' => 100, + ], + [ + 'name' => 'Product 2', + 'price' => 200, + ], + ], + ]); + + $recipientEmails = [ + (new MailtrapEmail()) + ->to(new Address('recipient1@example.com', 'Recipient 1')) + // Optional: Override template variables for this recipient + ->templateVariables([ + 'user_name' => 'Custom User 1', + ]), + (new MailtrapEmail()) + ->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $response = $mailtrap->batchSend($recipientEmails, $baseEmail); + + var_dump(ResponseHelper::toArray($response)); // Output response body as array +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} diff --git a/src/Api/AbstractEmails.php b/src/Api/AbstractEmails.php index cd7266b..72c3abb 100644 --- a/src/Api/AbstractEmails.php +++ b/src/Api/AbstractEmails.php @@ -13,6 +13,7 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Header\MailboxListHeader; /** * Class AbstractEmails @@ -21,10 +22,15 @@ abstract class AbstractEmails extends AbstractApi implements EmailsSendApiInterf { protected function getPayload(Email $email): array { - $payload = [ - 'from' => $this->getStringifierAddress($this->getSender($email->getHeaders())), - 'to' => array_map([$this, 'getStringifierAddress'], $this->getRecipients($email->getHeaders(), $email)), - ]; + $payload = []; + + if (null !== $this->getSender($email->getHeaders())) { + $payload['from'] = $this->getStringifierAddress($this->getSender($email->getHeaders())); + } + + if (!empty($this->getRecipients($email->getHeaders(), $email))) { + $payload['to'] = array_map([$this, 'getStringifierAddress'], $this->getRecipients($email->getHeaders(), $email)); + } if (null !== $email->getSubject()) { $payload['subject'] = $email->getSubject(); @@ -89,6 +95,39 @@ protected function getPayload(Email $email): array return $payload; } + protected function getBatchBasePayload(Email $email): array + { + $payload = $this->getPayload($email); + if (!empty($payload['to']) || !empty($payload['cc']) || !empty($payload['bcc'])) { + throw new LogicException( + "Batch base email does not support 'to', 'cc', or 'bcc' fields. Please use individual batch email requests to specify recipients." + ); + } + + if (!empty($this->getFirstReplyTo($email->getHeaders()))) { + $payload['reply_to'] = $this->getStringifierAddress( + $this->getFirstReplyTo($email->getHeaders()) + ); + } + + return $payload; + } + + protected function getBatchBody(array $recipientEmails, ?Email $baseEmail = null): array + { + $body = []; + if ($baseEmail !== null) { + $body['base'] = $this->getBatchBasePayload($baseEmail); + } + + $body['requests'] = array_map( + fn(Email $email) => $this->getPayload($email), + $recipientEmails + ); + + return $body; + } + private function getAttachments(Email $email): array { $attachments = []; @@ -125,7 +164,7 @@ private function getStringifierAddress(Address $address): array return $res; } - private function getSender(Headers $headers): Address + private function getSender(Headers $headers): ?Address { if ($sender = $headers->get('Sender')) { return $sender->getAddress(); @@ -137,7 +176,7 @@ private function getSender(Headers $headers): Address return $from->getAddresses()[0]; } - throw new LogicException('Unable to determine the sender of the message.'); + return null; } /** @@ -162,4 +201,23 @@ private function getRecipients(Headers $headers, Email $email): array static fn (Address $address) => false === in_array($address, array_merge($email->getCc(), $email->getBcc()), true) ); } + + /** + * Returns the first address from the 'Reply-To' header, if it exists. + * + * @param Headers $headers + * + * @return Address|null + */ + private function getFirstReplyTo(Headers $headers): ?Address + { + /** @var MailboxListHeader|null $replyToHeader */ + $replyToHeader = $headers->get('Reply-To'); + + if (empty($replyToHeader) || empty($replyToHeader->getAddresses())) { + return null; + } + + return $replyToHeader->getAddresses()[0]; + } } diff --git a/src/Api/BulkSending/Emails.php b/src/Api/BulkSending/Emails.php index 092e37e..1b7de3f 100644 --- a/src/Api/BulkSending/Emails.php +++ b/src/Api/BulkSending/Emails.php @@ -20,6 +20,17 @@ public function send(Email $email): ResponseInterface ); } + public function batchSend(array $recipientEmails, ?Email $baseEmail = null): ResponseInterface + { + return $this->handleResponse( + $this->httpPost( + sprintf('%s/api/batch', $this->getHost()), + [], + $this->getBatchBody($recipientEmails, $baseEmail), + ) + ); + } + protected function getHost(): string { return $this->config->getHost() ?: self::SENDMAIL_BULK_HOST; diff --git a/src/Api/EmailsSendApiInterface.php b/src/Api/EmailsSendApiInterface.php index 3d31e22..ae0d9f4 100644 --- a/src/Api/EmailsSendApiInterface.php +++ b/src/Api/EmailsSendApiInterface.php @@ -10,5 +10,15 @@ interface EmailsSendApiInterface { public function send(Email $email): ResponseInterface; + + /** + * Sends a batch of emails. + * + * @param Email[] $recipientEmails The list of emails. Each of them requires recipients (one of to, cc, or bcc). Each email inherits properties from base but can override them. + * @param Email|null $baseEmail General properties of all emails in the batch. Each of them can be overridden in requests for individual emails. + * + * @return ResponseInterface The response from the API. + */ + public function batchSend(array $recipientEmails, ?Email $baseEmail = null): ResponseInterface; } diff --git a/src/Api/Sandbox/Emails.php b/src/Api/Sandbox/Emails.php index 0fcbc9c..d0dc42c 100644 --- a/src/Api/Sandbox/Emails.php +++ b/src/Api/Sandbox/Emails.php @@ -26,6 +26,17 @@ public function send(Email $email): ResponseInterface ); } + public function batchSend(array $recipientEmails, ?Email $baseEmail = null): ResponseInterface + { + return $this->handleResponse( + $this->httpPost( + sprintf('%s/api/batch/%s', $this->getHost(), $this->getInboxId()), + [], + $this->getBatchBody($recipientEmails, $baseEmail), + ) + ); + } + protected function getHost(): string { return $this->config->getHost() ?: self::SENDMAIL_SANDBOX_HOST; diff --git a/src/Api/Sending/Emails.php b/src/Api/Sending/Emails.php index 0bba8c1..d1f956b 100644 --- a/src/Api/Sending/Emails.php +++ b/src/Api/Sending/Emails.php @@ -20,6 +20,17 @@ public function send(Email $email): ResponseInterface ); } + public function batchSend(array $recipientEmails, ?Email $baseEmail = null): ResponseInterface + { + return $this->handleResponse( + $this->httpPost( + sprintf('%s/api/batch', $this->getHost()), + [], + $this->getBatchBody($recipientEmails, $baseEmail), + ) + ); + } + protected function getHost(): string { return $this->config->getHost() ?: self::SENDMAIL_TRANSACTIONAL_HOST; diff --git a/src/Bridge/Laravel/README.md b/src/Bridge/Laravel/README.md index 6748429..90c411d 100644 --- a/src/Bridge/Laravel/README.md +++ b/src/Bridge/Laravel/README.md @@ -279,11 +279,57 @@ Artisan::command('send-template-mail', function () { })->purpose('Send Template Mail'); ``` -After that just call this CLI command, and it will send your template email +After that, just call this CLI command, and it will send your template email ```bash php artisan send-template-mail ``` +### Batch Sending (Transactional OR Bulk) + +Add CLI command +```php +# app/routes/console.php +from(new Address('example@YOUR-DOMAIN-HERE.com', 'Mailtrap Test')) // Use your domain installed in Mailtrap + ->subject('Batch Email Subject') + ->text('Batch email text') + ->html('

Batch email text

'); + + $recipientEmails = [ + (new MailtrapEmail())->to(new Address('recipient1@example.com', 'Recipient 1')), + (new MailtrapEmail())->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $mailtrap->batchSend($recipientEmails, $baseEmail); +})->purpose('Send Batch Mail'); +``` + +After that, just call this CLI command, and it will send your batch emails +```bash +php artisan batch-send-mail +``` + + ## Compatibility The Mailtrap library is fully compatible with **Laravel 9.x and above**. > Laravel did one of the largest changes in Laravel 9.x is the transition from SwiftMailer, which is no longer maintained as of December 2021, to Symfony Mailer. diff --git a/src/Bridge/Symfony/README.md b/src/Bridge/Symfony/README.md index 5613623..54b3f4d 100644 --- a/src/Bridge/Symfony/README.md +++ b/src/Bridge/Symfony/README.md @@ -153,6 +153,43 @@ final class SomeController extends AbstractController return JsonResponse::create(ResponseHelper::toArray($response)); } + + /** + * WARNING! To send Batch Email, you should use the native library and its methods + * + * @Route(name="send-batch-email", path="/send-batch-email", methods={"GET"}) + * + * @return JsonResponse + */ + public function sendBatchEmail(): JsonResponse + { + // Choose either Transactional API or Bulk API + // For Transactional API + $mailtrap = MailtrapClient::initSendingEmails( + apiKey: getenv('MAILTRAP_API_KEY'), // Your API token from https://mailtrap.io/api-tokens + ); + + // OR for Bulk API (uncomment the line below and comment out the transactional initialization) + // $mailtrap = MailtrapClient::initSendingEmails( + // apiKey: getenv('MAILTRAP_API_KEY'), // Your API token from https://mailtrap.io/api-tokens + // isBulk: true // Enable bulk sending + //); + + $baseEmail = (new MailtrapEmail()) + ->from(new Address('example@YOUR-DOMAIN-HERE.com', 'Mailtrap Test')) // Use your domain installed in Mailtrap + ->subject('Batch Email Subject') + ->text('Batch email text') + ->html('

Batch email text

'); + + $recipientEmails = [ + (new MailtrapEmail())->to(new Address('recipient1@example.com', 'Recipient 1')), + (new MailtrapEmail())->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $response = $mailtrap->batchSend($recipientEmails, $baseEmail); + + return JsonResponse::create(ResponseHelper::toArray($response)); + } } ``` diff --git a/tests/Api/AbstractEmailsTest.php b/tests/Api/AbstractEmailsTest.php index a276281..cc4e0af 100644 --- a/tests/Api/AbstractEmailsTest.php +++ b/tests/Api/AbstractEmailsTest.php @@ -12,6 +12,7 @@ use Mailtrap\EmailHeader\Template\TemplateUuidHeader; use Mailtrap\EmailHeader\Template\TemplateVariableHeader; use Mailtrap\Exception\HttpClientException; +use Mailtrap\Exception\LogicException; use Mailtrap\Exception\RuntimeException; use Mailtrap\Helper\ResponseHelper; use Mailtrap\Mime\MailtrapEmail; @@ -121,7 +122,6 @@ public function testInValidSend(): void 'email' => 'foo@example.com', 'name' => 'Ms. Foo Bar', ], - 'to' => [], 'text' => 'Some text', 'html' => '

Some text

', 'headers' => [ @@ -557,6 +557,436 @@ public function testTemplateVariablesNewEmailWrapper($name, $value): void $this->assertEquals($value, $payload[TemplateVariableHeader::VAR_NAME][$name]); } + public function testBatchSend(): void + { + $baseEmail = (new Email()) + ->from(new Address('foo@example.com', 'Ms. Foo Bar')) + ->subject('Batch Email Subject') + ->text('Batch email text') + ->html('

Batch email text

'); + + $recipientEmails = [ + (new Email())->to(new Address('recipient1@example.com', 'Recipient 1')), + (new Email())->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $expectedPayload = [ + 'base' => [ + 'from' => [ + 'email' => 'foo@example.com', + 'name' => 'Ms. Foo Bar', + ], + 'subject' => 'Batch Email Subject', + 'text' => 'Batch email text', + 'html' => '

Batch email text

', + ], + 'requests' => [ + [ + 'to' => [[ + 'email' => 'recipient1@example.com', + 'name' => 'Recipient 1', + ]], + ], + [ + 'to' => [[ + 'email' => 'recipient2@example.com', + 'name' => 'Recipient 2', + ]], + ], + ], + ]; + + $expectedResponse = [ + 'success' => true, + 'responses' => [ + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0000-f185d7639148', + ], + ], + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0001-f185d7639148', + ], + ], + ], + ]; + + $this->email + ->expects($this->once()) + ->method('httpPost') + ->with($this->getHost() . '/api/batch', [], $expectedPayload) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->email->batchSend($recipientEmails, $baseEmail); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('success', $responseData); + $this->assertTrue($responseData['success']); + $this->assertArrayHasKey('responses', $responseData); + $this->assertCount(2, $responseData['responses']); + } + + public function testBatchSendWithoutBaseParam(): void + { + $recipientEmails = [ + (new Email()) + ->from(new Address('sender@example.com', 'Sender Name')) + ->to(new Address('recipient1@example.com', 'Recipient 1')) + ->subject('Test Subject 1') + ->text('Test email body 1'), + (new Email()) + ->from(new Address('sender@example.com', 'Sender Name')) + ->to(new Address('recipient2@example.com', 'Recipient 2')) + ->subject('Test Subject 2') + ->html('

Test email body 2

'), + ]; + + $expectedPayload = [ + 'requests' => [ + [ + 'from' => [ + 'email' => 'sender@example.com', + 'name' => 'Sender Name', + ], + 'to' => [[ + 'email' => 'recipient1@example.com', + 'name' => 'Recipient 1', + ]], + 'subject' => 'Test Subject 1', + 'text' => 'Test email body 1', + ], + [ + 'from' => [ + 'email' => 'sender@example.com', + 'name' => 'Sender Name', + ], + 'to' => [[ + 'email' => 'recipient2@example.com', + 'name' => 'Recipient 2', + ]], + 'subject' => 'Test Subject 2', + 'html' => '

Test email body 2

', + ], + ], + ]; + + $expectedResponse = [ + 'success' => true, + 'responses' => [ + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0000-f185d7639148', + ], + ], + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0001-f185d7639148', + ], + ], + ], + ]; + + $this->email + ->expects($this->once()) + ->method('httpPost') + ->with($this->getHost() . '/api/batch', [], $expectedPayload) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->email->batchSend($recipientEmails); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('success', $responseData); + $this->assertTrue($responseData['success']); + $this->assertArrayHasKey('responses', $responseData); + $this->assertCount(2, $responseData['responses']); + } + + public function testBatchSendInvalidWithoutBaseAndRequiredFields(): void + { + $recipientEmails = [ + (new Email())->to(new Address('recipient1@example.com', 'Recipient 1')), + (new Email())->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $expectedPayload = [ + 'requests' => [ + [ + 'to' => [[ + 'email' => 'recipient1@example.com', + 'name' => 'Recipient 1', + ]], + ], + [ + 'to' => [[ + 'email' => 'recipient2@example.com', + 'name' => 'Recipient 2', + ]], + ], + ], + ]; + + $expectedResponse = [ + 'success' => true, + 'responses' => [ + [ + 'success' => false, + 'errors' => [ + "'from' is required", + "'subject' is required", + "must specify either text or html body", + ], + ], + [ + 'success' => false, + 'errors' => [ + "'from' is required", + "'subject' is required", + "must specify either text or html body", + ], + ], + ], + ]; + + $this->email + ->expects($this->once()) + ->method('httpPost') + ->with($this->getHost() . '/api/batch', [], $expectedPayload) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->email->batchSend($recipientEmails); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('success', $responseData); + $this->assertTrue($responseData['success']); + $this->assertArrayHasKey('responses', $responseData); + $this->assertCount(2, $responseData['responses']); + foreach ($responseData['responses'] as $recipientResponse) { + $this->assertFalse($recipientResponse['success']); + $this->assertArrayHasKey('errors', $recipientResponse); + $this->assertContains("'from' is required", $recipientResponse['errors']); + $this->assertContains("'subject' is required", $recipientResponse['errors']); + $this->assertContains("must specify either text or html body", $recipientResponse['errors']); + } + } + + public function testBatchSendWithTemplateId(): void + { + $baseEmail = (new MailtrapEmail()) + ->from(new Address('sender@example.com', 'Sender Name')) + ->templateUuid('bfa432fd-0000-413d-9d6e-8493da283a69') + ->templateVariables([ + 'user_name' => 'John Doe', + 'next_step_link' => 'https://example.com/next-step', + 'company' => [ + 'name' => 'Example Company', + 'address' => '123 Example Street', + ], + ]); + + $recipientEmails = [ + (new MailtrapEmail()) + ->to(new Address('recipient1@example.com', 'Recipient One')) + ->templateVariables([ + 'user_name' => 'Custom User 1', + ]), + (new MailtrapEmail()) + ->to(new Address('recipient2@example.com', 'Recipient Two')), + ]; + + $expectedPayload = [ + 'base' => [ + 'from' => [ + 'email' => 'sender@example.com', + 'name' => 'Sender Name', + ], + 'template_uuid' => 'bfa432fd-0000-413d-9d6e-8493da283a69', + 'template_variables' => [ + 'user_name' => 'John Doe', + 'next_step_link' => 'https://example.com/next-step', + 'company' => [ + 'name' => 'Example Company', + 'address' => '123 Example Street', + ], + ], + ], + 'requests' => [ + [ + 'to' => [[ + 'email' => 'recipient1@example.com', + 'name' => 'Recipient One', + ]], + 'template_variables' => [ + 'user_name' => 'Custom User 1', + ], + ], + [ + 'to' => [[ + 'email' => 'recipient2@example.com', + 'name' => 'Recipient Two', + ]], + ], + ], + ]; + + $expectedResponse = [ + 'success' => true, + 'responses' => [ + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0000-f185d7639148', + ], + ], + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0001-f185d7639148', + ], + ], + ], + ]; + + $this->email + ->expects($this->once()) + ->method('httpPost') + ->with($this->getHost() . '/api/batch', [], $expectedPayload) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->email->batchSend($recipientEmails, $baseEmail); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('success', $responseData); + $this->assertTrue($responseData['success']); + $this->assertArrayHasKey('responses', $responseData); + $this->assertCount(2, $responseData['responses']); + } + + public function testBatchSendWithTemplateUuidFailsDueToSubjectInRecipientEmails(): void + { + $baseEmail = (new MailtrapEmail()) + ->from(new Address('sender@example.com', 'Sender Name')) + ->templateUuid('bfa432fd-0000-413d-9d6e-8493da283a69') + ->templateVariables([ + 'user_name' => 'John Doe', + 'next_step_link' => 'https://example.com/next-step', + 'company' => [ + 'name' => 'Example Company', + 'address' => '123 Example Street', + ], + ]); + + $recipientEmails = [ + (new MailtrapEmail()) + ->to(new Address('recipient1@example.com', 'Recipient One')) + ->subject('Invalid Subject'), // Invalid field + (new MailtrapEmail()) + ->to(new Address('recipient2@example.com', 'Recipient Two')) + ->subject('Invalid Subject'), // Invalid field + ]; + + $expectedPayload = [ + 'base' => [ + 'from' => [ + 'email' => 'sender@example.com', + 'name' => 'Sender Name', + ], + 'template_uuid' => 'bfa432fd-0000-413d-9d6e-8493da283a69', + 'template_variables' => [ + 'user_name' => 'John Doe', + 'next_step_link' => 'https://example.com/next-step', + 'company' => [ + 'name' => 'Example Company', + 'address' => '123 Example Street', + ], + ], + ], + 'requests' => [ + [ + 'to' => [[ + 'email' => 'recipient1@example.com', + 'name' => 'Recipient One', + ]], + 'subject' => 'Invalid Subject', + ], + [ + 'to' => [[ + 'email' => 'recipient2@example.com', + 'name' => 'Recipient Two', + ]], + 'subject' => 'Invalid Subject', + ], + ], + ]; + + $expectedResponse = [ + 'success' => true, + 'responses' => [ + [ + 'success' => false, + 'errors' => [ + "'subject' is not allowed with 'template_uuid'", + ], + ], + [ + 'success' => false, + 'errors' => [ + "'subject' is not allowed with 'template_uuid'", + ], + ], + ], + ]; + + $this->email + ->expects($this->once()) + ->method('httpPost') + ->with($this->getHost() . '/api/batch', [], $expectedPayload) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->email->batchSend($recipientEmails, $baseEmail); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('success', $responseData); + $this->assertTrue($responseData['success']); + $this->assertArrayHasKey('responses', $responseData); + $this->assertCount(2, $responseData['responses']); + foreach ($responseData['responses'] as $recipientResponse) { + $this->assertFalse($recipientResponse['success']); + $this->assertArrayHasKey('errors', $recipientResponse); + $this->assertContains("'subject' is not allowed with 'template_uuid'", $recipientResponse['errors']); + } + } + + public function testBatchSendThrowsLogicExceptionForInvalidBaseEmail(): void + { + $baseEmail = (new Email()) + ->from(new Address('foo@example.com', 'Ms. Foo Bar')) + ->to(new Address('recipient@example.com', 'Recipient')) // Invalid field + ->subject('Batch Email Subject') + ->text('Batch email text') + ->html('

Batch email text

'); + + $recipientEmails = [ + (new Email())->to(new Address('recipient1@example.com', 'Recipient 1')), + ]; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + "Batch base email does not support 'to', 'cc', or 'bcc' fields. Please use individual batch email requests to specify recipients." + ); + + $this->email->batchSend($recipientEmails, $baseEmail); + } + // public function validHeadersDataProvider(): array diff --git a/tests/Api/Sandbox/EmailsTest.php b/tests/Api/Sandbox/EmailsTest.php index 6394958..c57eab5 100644 --- a/tests/Api/Sandbox/EmailsTest.php +++ b/tests/Api/Sandbox/EmailsTest.php @@ -274,4 +274,77 @@ public function testValidSendTemplateToSandboxNewEmailWrapper(): void $this->assertArrayHasKey('success', $responseData); $this->assertArrayHasKey('message_ids', $responseData); } + + public function testBatchSend(): void + { + $baseEmail = (new Email()) + ->from(new Address('foo@example.com', 'Ms. Foo Bar')) + ->subject('Batch Email Subject') + ->text('Batch email text') + ->html('

Batch email text

'); + + $recipientEmails = [ + (new Email())->to(new Address('recipient1@example.com', 'Recipient 1')), + (new Email())->to(new Address('recipient2@example.com', 'Recipient 2')), + ]; + + $expectedPayload = [ + 'base' => [ + 'from' => [ + 'email' => 'foo@example.com', + 'name' => 'Ms. Foo Bar', + ], + 'subject' => 'Batch Email Subject', + 'text' => 'Batch email text', + 'html' => '

Batch email text

', + ], + 'requests' => [ + [ + 'to' => [[ + 'email' => 'recipient1@example.com', + 'name' => 'Recipient 1', + ]], + ], + [ + 'to' => [[ + 'email' => 'recipient2@example.com', + 'name' => 'Recipient 2', + ]], + ], + ], + ]; + + $expectedResponse = [ + 'success' => true, + 'responses' => [ + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0000-f185d7639148', + ], + ], + [ + 'success' => true, + 'message_ids' => [ + '53f764a0-4dca-11f0-0001-f185d7639148', + ], + ], + ], + ]; + + $this->email + ->expects($this->once()) + ->method('httpPost') + ->with(AbstractApi::SENDMAIL_SANDBOX_HOST . '/api/batch/' . self::FAKE_INBOX_ID, [], $expectedPayload) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->email->batchSend($recipientEmails, $baseEmail); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('success', $responseData); + $this->assertTrue($responseData['success']); + $this->assertArrayHasKey('responses', $responseData); + $this->assertCount(2, $responseData['responses']); + } }