From 4050ad713327cf8492f71f27fc7b84a84deee694 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Tue, 17 Mar 2026 07:21:48 +0000 Subject: [PATCH 1/2] Fix autoload.php path in examples The README says to run examples from the project root, e.g. php examples/sending/minimal.php. Scripts under examples// (e.g. examples/batch/, examples/sending/) used: require __DIR__ . '/../vendor/autoload.php'; That resolves to examples/vendor/autoload.php, while vendor/ actually lives at the project root, so the path was incorrect. From examples//, you need to go up two levels to reach the project root, then into vendor/: require __DIR__ . '/../../vendor/autoload.php'; --- examples/batch/bulk.php | 2 +- examples/batch/bulk_template.php | 2 +- examples/batch/sandbox.php | 2 +- examples/batch/sandbox_template.php | 2 +- examples/batch/transactional.php | 2 +- examples/batch/transactional_template.php | 2 +- examples/bulk/bulk.php | 2 +- examples/bulk/bulk_template.php | 2 +- examples/config/all.php | 2 +- examples/contact-fields/all.php | 2 +- examples/contact-lists/all.php | 2 +- examples/contacts/all.php | 2 +- examples/general/accounts.php | 2 +- examples/general/billing.php | 2 +- examples/general/permissions.php | 2 +- examples/general/users.php | 2 +- examples/sending/all.php | 2 +- examples/sending/minimal.php | 2 +- examples/sending/suppressions.php | 2 +- examples/sending/template.php | 2 +- examples/templates/all.php | 2 +- examples/testing/attachments.php | 2 +- examples/testing/inboxes.php | 2 +- examples/testing/messages.php | 2 +- examples/testing/projects.php | 2 +- examples/testing/send-mail.php | 2 +- examples/testing/template.php | 2 +- 27 files changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/batch/bulk.php b/examples/batch/bulk.php index efd6708..188479b 100644 --- a/examples/batch/bulk.php +++ b/examples/batch/bulk.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Email Batch Sending API (Bulk) diff --git a/examples/batch/bulk_template.php b/examples/batch/bulk_template.php index 2d84fe3..ff851de 100644 --- a/examples/batch/bulk_template.php +++ b/examples/batch/bulk_template.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Email Batch Sending WITH TEMPLATE (Bulk) diff --git a/examples/batch/sandbox.php b/examples/batch/sandbox.php index a0d7e00..133a0e9 100644 --- a/examples/batch/sandbox.php +++ b/examples/batch/sandbox.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Test Email Batch diff --git a/examples/batch/sandbox_template.php b/examples/batch/sandbox_template.php index f9e280c..329629b 100644 --- a/examples/batch/sandbox_template.php +++ b/examples/batch/sandbox_template.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Test Email Batch WITH TEMPLATE diff --git a/examples/batch/transactional.php b/examples/batch/transactional.php index ab0278c..028448f 100644 --- a/examples/batch/transactional.php +++ b/examples/batch/transactional.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Email Batch Sending API (Transactional) diff --git a/examples/batch/transactional_template.php b/examples/batch/transactional_template.php index 391d6fd..83c646e 100644 --- a/examples/batch/transactional_template.php +++ b/examples/batch/transactional_template.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Email Batch Sending WITH TEMPLATE (Transactional) diff --git a/examples/bulk/bulk.php b/examples/bulk/bulk.php index 8152f07..862dc27 100644 --- a/examples/bulk/bulk.php +++ b/examples/bulk/bulk.php @@ -7,7 +7,7 @@ use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Header\UnstructuredHeader; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Email Bulk Sending API diff --git a/examples/bulk/bulk_template.php b/examples/bulk/bulk_template.php index d4d5181..d6cfa5c 100644 --- a/examples/bulk/bulk_template.php +++ b/examples/bulk/bulk_template.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** * Email Bulk Sending WITH TEMPLATE diff --git a/examples/config/all.php b/examples/config/all.php index 6725fad..d98f5e4 100644 --- a/examples/config/all.php +++ b/examples/config/all.php @@ -8,7 +8,7 @@ use Symfony\Component\HttpClient\Psr18Client; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $apiToken = $_ENV['MAILTRAP_API_KEY']; diff --git a/examples/contact-fields/all.php b/examples/contact-fields/all.php index c791a0c..4f8d3fe 100644 --- a/examples/contact-fields/all.php +++ b/examples/contact-fields/all.php @@ -9,7 +9,7 @@ use Mailtrap\MailtrapGeneralClient; use Mailtrap\DTO\Request\Contact\ContactExportFilter; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/contact-lists/all.php b/examples/contact-lists/all.php index 79571c6..edc7bc1 100644 --- a/examples/contact-lists/all.php +++ b/examples/contact-lists/all.php @@ -9,7 +9,7 @@ use Mailtrap\MailtrapGeneralClient; use Mailtrap\DTO\Request\Contact\ContactExportFilter; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/contacts/all.php b/examples/contacts/all.php index df0ca1f..178d302 100644 --- a/examples/contacts/all.php +++ b/examples/contacts/all.php @@ -9,7 +9,7 @@ use Mailtrap\MailtrapGeneralClient; use Mailtrap\DTO\Request\Contact\ContactExportFilter; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/general/accounts.php b/examples/general/accounts.php index 9730cf3..e4a3a2a 100644 --- a/examples/general/accounts.php +++ b/examples/general/accounts.php @@ -4,7 +4,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapGeneralClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens $generalAccounts = (new MailtrapGeneralClient($config))->accounts(); diff --git a/examples/general/billing.php b/examples/general/billing.php index 4d6b584..4180ac6 100644 --- a/examples/general/billing.php +++ b/examples/general/billing.php @@ -4,7 +4,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapGeneralClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = (int) $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/general/permissions.php b/examples/general/permissions.php index fceadec..49b4e73 100644 --- a/examples/general/permissions.php +++ b/examples/general/permissions.php @@ -8,7 +8,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapGeneralClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/general/users.php b/examples/general/users.php index bd1e875..ad72b69 100644 --- a/examples/general/users.php +++ b/examples/general/users.php @@ -4,7 +4,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapGeneralClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/sending/all.php b/examples/sending/all.php index fd399a0..6b43f20 100644 --- a/examples/sending/all.php +++ b/examples/sending/all.php @@ -7,7 +7,7 @@ use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Header\UnstructuredHeader; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** diff --git a/examples/sending/minimal.php b/examples/sending/minimal.php index 981ccc0..b836edf 100644 --- a/examples/sending/minimal.php +++ b/examples/sending/minimal.php @@ -5,7 +5,7 @@ use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\Mime\Address; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** diff --git a/examples/sending/suppressions.php b/examples/sending/suppressions.php index 01b7c07..0680524 100644 --- a/examples/sending/suppressions.php +++ b/examples/sending/suppressions.php @@ -6,7 +6,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapSendingClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); // Your API token from https://mailtrap.io/api-tokens diff --git a/examples/sending/template.php b/examples/sending/template.php index c11a5a2..1068b80 100644 --- a/examples/sending/template.php +++ b/examples/sending/template.php @@ -7,7 +7,7 @@ use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Header\UnstructuredHeader; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** diff --git a/examples/templates/all.php b/examples/templates/all.php index 8094bf3..3d6c405 100644 --- a/examples/templates/all.php +++ b/examples/templates/all.php @@ -7,7 +7,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapGeneralClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); // Your API token from https://mailtrap.io/api-tokens diff --git a/examples/testing/attachments.php b/examples/testing/attachments.php index 8d944a5..1a10c74 100644 --- a/examples/testing/attachments.php +++ b/examples/testing/attachments.php @@ -4,7 +4,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapSandboxClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $inboxId = $_ENV['MAILTRAP_INBOX_ID']; diff --git a/examples/testing/inboxes.php b/examples/testing/inboxes.php index e5fa377..a9cff8d 100644 --- a/examples/testing/inboxes.php +++ b/examples/testing/inboxes.php @@ -5,7 +5,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapSandboxClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/testing/messages.php b/examples/testing/messages.php index 2528512..6217196 100644 --- a/examples/testing/messages.php +++ b/examples/testing/messages.php @@ -4,7 +4,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapSandboxClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; // your API token from here https://mailtrap.io/api-tokens $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; diff --git a/examples/testing/projects.php b/examples/testing/projects.php index a404bc1..e0dea6a 100644 --- a/examples/testing/projects.php +++ b/examples/testing/projects.php @@ -4,7 +4,7 @@ use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapSandboxClient; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; $accountId = $_ENV['MAILTRAP_ACCOUNT_ID']; $config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens diff --git a/examples/testing/send-mail.php b/examples/testing/send-mail.php index a5af9fb..5e0c1da 100644 --- a/examples/testing/send-mail.php +++ b/examples/testing/send-mail.php @@ -6,7 +6,7 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\UnstructuredHeader; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** diff --git a/examples/testing/template.php b/examples/testing/template.php index c1235d2..24a4638 100644 --- a/examples/testing/template.php +++ b/examples/testing/template.php @@ -6,7 +6,7 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\UnstructuredHeader; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../../vendor/autoload.php'; /** From aa1cd7f48d5556d0073a5570e0e2669c7930937a Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Tue, 17 Mar 2026 07:15:35 +0000 Subject: [PATCH 2/2] Add support for Email Logs API --- README.md | 3 +- examples/sending/email-logs.php | 81 ++++++ src/Api/Sending/EmailLogs.php | 84 ++++++ .../EmailLogs/EmailLogsFilterOperator.php | 53 ++++ .../EmailLogs/EmailLogsFilterValue.php | 37 +++ .../EmailLogs/EmailLogsListFilters.php | 91 ++++++ src/DTO/Request/EmailLogs/FilterCriterion.php | 56 ++++ src/MailtrapSendingClient.php | 2 + tests/Api/AbstractApiTest.php | 45 +++ tests/Api/Sending/EmailLogsTest.php | 265 ++++++++++++++++++ tests/MailtrapSendingClientTest.php | 2 +- 11 files changed, 717 insertions(+), 2 deletions(-) create mode 100644 examples/sending/email-logs.php create mode 100644 src/Api/Sending/EmailLogs.php create mode 100644 src/DTO/Request/EmailLogs/EmailLogsFilterOperator.php create mode 100644 src/DTO/Request/EmailLogs/EmailLogsFilterValue.php create mode 100644 src/DTO/Request/EmailLogs/EmailLogsListFilters.php create mode 100644 src/DTO/Request/EmailLogs/FilterCriterion.php create mode 100644 tests/Api/AbstractApiTest.php create mode 100644 tests/Api/Sending/EmailLogsTest.php diff --git a/README.md b/README.md index 4d49e9d..3339368 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,8 @@ Email API: - Batch send with Template (Transactional) – [`batch/transactional_template.php`](examples/batch/transactional_template.php) - Batch send with Template (Bulk) – [`batch/bulk_template.php`](examples/batch/bulk_template.php) - Sending domain management CRUD – [`sending-domains/all.php`](examples/sending-domains/all.php) +- Suppressions (find & delete) – [`sending/suppressions.php`](examples/sending/suppressions.php) +- Email Logs (list & get by message ID) – [`sending/email-logs.php`](examples/sending/email-logs.php) Email Sandbox (Testing): - Send an email (Sandbox) – [`testing/send-mail.php`](examples/testing/send-mail.php) @@ -260,7 +262,6 @@ Contact management: General API: - Templates CRUD – [`templates/all.php`](examples/templates/all.php) -- Suppressions (find & delete) – [`sending/suppressions.php`](examples/sending/suppressions.php) - Billing info – [`general/billing.php`](examples/general/billing.php) - Accounts info – [`general/accounts.php`](examples/general/accounts.php) - Permissions listing – [`general/permissions.php`](examples/general/permissions.php) diff --git a/examples/sending/email-logs.php b/examples/sending/email-logs.php new file mode 100644 index 0000000..78e1269 --- /dev/null +++ b/examples/sending/email-logs.php @@ -0,0 +1,81 @@ +emailLogs($accountId); + +/** + * List email logs (paginated). + * + * GET https://mailtrap.io/api/accounts/{account_id}/email_logs + */ +try { + // Get first page (no filters) + $response = $emailLogs->getList(); + $data = ResponseHelper::toArray($response); + + echo "Total count: " . $data['total_count'] . PHP_EOL; + foreach ($data['messages'] as $msg) { + echo " - " . $msg['message_id'] . " | " . $msg['status'] . " | " . ($msg['subject'] ?? '') . PHP_EOL; + } + + // Optional: filter by date range (last 2 days), recipient, categories – using the filters model + $sentBefore = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); + $sentAfter = (new DateTimeImmutable('-2 days', new DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); + $filters = EmailLogsListFilters::create( + $sentAfter, + $sentBefore, + [ + 'subject' => FilterCriterion::withoutValue(EmailLogsFilterOperator::NOT_EMPTY), + 'to' => FilterCriterion::withValue(EmailLogsFilterOperator::CI_EQUAL, 'recipient@example.com'), + 'category' => FilterCriterion::withValue(EmailLogsFilterOperator::EQUAL, ['Welcome Email', 'Order Confirmation']), + ] + ); + $response = $emailLogs->getList($filters); + $data = ResponseHelper::toArray($response); + + // Next page (use next_page_cursor from previous response) + $cursor = $data['next_page_cursor'] ?? null; + if ($cursor !== null) { + $response = $emailLogs->getList($filters, $cursor); + $data = ResponseHelper::toArray($response); + } +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} + +/** + * Get a single email log message by ID. + * + * GET https://mailtrap.io/api/accounts/{account_id}/email_logs/{sending_message_id} + */ +try { + $listResponse = $emailLogs->getList(); + $listData = ResponseHelper::toArray($listResponse); + $messageId = isset($listData['messages'][0]) ? $listData['messages'][0]['message_id'] : null; + + if ($messageId !== null) { + $response = $emailLogs->getMessage($messageId); + $message = ResponseHelper::toArray($response); + + echo "Message: " . $message['message_id'] . PHP_EOL; + echo "Status: " . $message['status'] . PHP_EOL; + echo "Subject: " . ($message['subject'] ?? '') . PHP_EOL; + echo "Events: " . count($message['events'] ?? []) . PHP_EOL; + } else { + echo "No messages in the list." . PHP_EOL; + } +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} diff --git a/src/Api/Sending/EmailLogs.php b/src/Api/Sending/EmailLogs.php new file mode 100644 index 0000000..5a6da29 --- /dev/null +++ b/src/Api/Sending/EmailLogs.php @@ -0,0 +1,84 @@ +|EmailLogsListFilters $filters Filter criteria: either an EmailLogsListFilters instance + * or a raw array (deep object). Array keys: sent_after, + * sent_before, to, from, subject, status, events, + * clicks_count, opens_count, client_ip, sending_ip, + * email_service_provider_response, email_service_provider, + * recipient_mx, category, sending_domain_id, sending_stream. + * @param string|null $searchAfter Cursor for the next page (message_id UUID from previous response next_page_cursor). + * @return ResponseInterface 200 with body: { messages: array, total_count: int, next_page_cursor: string|null } + */ + public function getList(array|EmailLogsListFilters $filters = [], ?string $searchAfter = null): ResponseInterface + { + $params = []; + if ($searchAfter !== null && $searchAfter !== '') { + $params['search_after'] = $searchAfter; + } + $filterArray = $filters instanceof EmailLogsListFilters ? $filters->toArray() : $filters; + if ($filterArray !== []) { + $params['filters'] = $filterArray; + } + + return $this->handleResponse( + $this->httpGet($this->getBasePath(), $params) + ); + } + + /** + * Get a single email log message by ID. + * + * Returns message details including events. Message must belong to the account and a sending domain + * the token can access. + * + * @param string $sendingMessageId Message UUID (sending_message_id). + * @return ResponseInterface 200 with SendingMessage body (message_id, status, subject, from, to, sent_at, events, etc.) + */ + public function getMessage(string $sendingMessageId): ResponseInterface + { + if (trim($sendingMessageId) === '') { + throw new InvalidArgumentException('sending_message_id must not be empty.'); + } + + return $this->handleResponse( + $this->httpGet( + sprintf('%s/%s', $this->getBasePath(), $sendingMessageId) + ) + ); + } + + private function getBasePath(): string + { + return sprintf('%s/api/accounts/%s/email_logs', $this->getHost(), $this->accountId); + } +} diff --git a/src/DTO/Request/EmailLogs/EmailLogsFilterOperator.php b/src/DTO/Request/EmailLogs/EmailLogsFilterOperator.php new file mode 100644 index 0000000..df37f48 --- /dev/null +++ b/src/DTO/Request/EmailLogs/EmailLogsFilterOperator.php @@ -0,0 +1,53 @@ + $criteria Map of filter name => FilterCriterion (e.g. 'to' => FilterCriterion::withValue('ci_equal', 'a@b.com')) + */ + public function __construct( + private ?string $sentAfter = null, + private ?string $sentBefore = null, + private array $criteria = [] + ) { + } + + public static function create( + ?string $sentAfter = null, + ?string $sentBefore = null, + array $criteria = [] + ): self { + return new self($sentAfter, $sentBefore, $criteria); + } + + public function withSentAfter(string $iso8601): self + { + return new self($iso8601, $this->sentBefore, $this->criteria); + } + + public function withSentBefore(string $iso8601): self + { + return new self($this->sentAfter, $iso8601, $this->criteria); + } + + public function withCriterion(string $name, FilterCriterion $criterion): self + { + $criteria = $this->criteria; + $criteria[$name] = $criterion; + + return new self($this->sentAfter, $this->sentBefore, $criteria); + } + + public function toArray(): array + { + $result = []; + if ($this->sentAfter !== null && $this->sentAfter !== '') { + $result['sent_after'] = $this->sentAfter; + } + if ($this->sentBefore !== null && $this->sentBefore !== '') { + $result['sent_before'] = $this->sentBefore; + } + foreach ($this->criteria as $name => $criterion) { + if (\in_array($name, self::VALID_CRITERIA_KEYS, true) && $criterion instanceof FilterCriterion) { + $result[$name] = $criterion->toArray(); + } + } + + return $result; + } +} diff --git a/src/DTO/Request/EmailLogs/FilterCriterion.php b/src/DTO/Request/EmailLogs/FilterCriterion.php new file mode 100644 index 0000000..408b031 --- /dev/null +++ b/src/DTO/Request/EmailLogs/FilterCriterion.php @@ -0,0 +1,56 @@ +operator; + } + + public function getValue(): mixed + { + return $this->value; + } + + public function toArray(): array + { + $result = ['operator' => $this->operator]; + if ($this->value !== null) { + $result['value'] = $this->value; + } + + return $result; + } +} diff --git a/src/MailtrapSendingClient.php b/src/MailtrapSendingClient.php index 609811e..58f4a07 100644 --- a/src/MailtrapSendingClient.php +++ b/src/MailtrapSendingClient.php @@ -9,6 +9,7 @@ * @method Api\Sending\Suppression suppressions(int $accountId) * @method Api\Sending\Domain domains(int $accountId) * @method Api\Sending\Stats stats(int $accountId) + * @method Api\Sending\EmailLogs emailLogs(int $accountId) * * Class MailtrapSendingClient */ @@ -19,5 +20,6 @@ final class MailtrapSendingClient extends AbstractMailtrapClient implements Emai 'suppressions' => Api\Sending\Suppression::class, 'domains' => Api\Sending\Domain::class, 'stats' => Api\Sending\Stats::class, + 'emailLogs' => Api\Sending\EmailLogs::class, ]; } diff --git a/tests/Api/AbstractApiTest.php b/tests/Api/AbstractApiTest.php new file mode 100644 index 0000000..ff82990 --- /dev/null +++ b/tests/Api/AbstractApiTest.php @@ -0,0 +1,45 @@ + [ + 'category' => ['operator' => 'equal', 'value' => ['Cat1', 'Cat2']], + ], + ]; + $api = new EmailLogs($this->getConfigMock(), self::FAKE_ACCOUNT_ID); + $reflection = new ReflectionClass(AbstractApi::class); + $method = $reflection->getMethod('normalizeArrayParams'); + if (PHP_VERSION_ID < 80100) { + $method->setAccessible(true); + } + $queryString = $method->invoke($api, $params); + + $this->assertStringNotContainsString('[0]', $queryString, 'Numeric indices must be normalized to bracket notation'); + $this->assertStringNotContainsString('[1]', $queryString, 'Numeric indices must be normalized to bracket notation'); + $this->assertMatchesRegularExpression( + '/filters%5Bcategory%5D%5Bvalue%5D%5B%5D=Cat1&filters%5Bcategory%5D%5Bvalue%5D%5B%5D=Cat2/', + $queryString, + 'Query must use filters[category][value][]=Cat1&filters[category][value][]=Cat2' + ); + } +} diff --git a/tests/Api/Sending/EmailLogsTest.php b/tests/Api/Sending/EmailLogsTest.php new file mode 100644 index 0000000..5f25e2c --- /dev/null +++ b/tests/Api/Sending/EmailLogsTest.php @@ -0,0 +1,265 @@ +emailLogs = $this->getMockBuilder(EmailLogs::class) + ->onlyMethods(['httpGet']) + ->setConstructorArgs([$this->getConfigMock(), self::FAKE_ACCOUNT_ID]) + ->getMock(); + } + + protected function tearDown(): void + { + $this->emailLogs = null; + parent::tearDown(); + } + + public function testGetListWithoutFilters(): void + { + $basePath = AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/email_logs'; + $body = $this->getListResponseBody(); + + $this->emailLogs->expects($this->once()) + ->method('httpGet') + ->with($basePath, []) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($body))); + + $response = $this->emailLogs->getList(); + $data = ResponseHelper::toArray($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertArrayHasKey('messages', $data); + $this->assertArrayHasKey('total_count', $data); + $this->assertArrayHasKey('next_page_cursor', $data); + $this->assertSame(150, $data['total_count']); + $this->assertCount(2, $data['messages']); + $this->assertSame('a1b2c3d4-e5f6-7890-abcd-ef1234567890', $data['messages'][0]['message_id']); + } + + public function testGetListWithFiltersAndSearchAfter(): void + { + $basePath = AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/email_logs'; + $filters = [ + 'sent_after' => '2025-01-01T00:00:00Z', + 'sent_before' => '2025-01-31T23:59:59Z', + 'to' => ['operator' => 'ci_equal', 'value' => 'recipient@example.com'], + 'sending_domain_id' => ['operator' => 'equal', 'value' => [3938, 3939]], + ]; + $searchAfter = 'b2c3d4e5-f6a7-8901-bcde-f12345678901'; + $params = [ + 'search_after' => $searchAfter, + 'filters' => $filters, + ]; + $body = $this->getListResponseBody(); + + $this->emailLogs->expects($this->once()) + ->method('httpGet') + ->with($basePath, $params) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($body))); + + $response = $this->emailLogs->getList($filters, $searchAfter); + $data = ResponseHelper::toArray($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertArrayHasKey('messages', $data); + } + + public function testGetListWithEmailLogsListFiltersModel(): void + { + $basePath = AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/email_logs'; + $filters = EmailLogsListFilters::create( + '2025-01-01T00:00:00Z', + '2025-01-31T23:59:59Z', + [ + 'to' => FilterCriterion::withValue(EmailLogsFilterOperator::CI_EQUAL, 'recipient@example.com'), + 'subject' => FilterCriterion::withoutValue(EmailLogsFilterOperator::NOT_EMPTY), + 'category' => FilterCriterion::withValue(EmailLogsFilterOperator::EQUAL, ['Welcome Email', 'Order Confirmation']), + ] + ); + $expectedParams = [ + 'filters' => $filters->toArray(), + ]; + $body = $this->getListResponseBody(); + + $this->emailLogs->expects($this->once()) + ->method('httpGet') + ->with($basePath, $expectedParams) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($body))); + + $response = $this->emailLogs->getList($filters); + $data = ResponseHelper::toArray($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertArrayHasKey('messages', $data); + $this->assertSame('2025-01-01T00:00:00Z', $expectedParams['filters']['sent_after']); + $this->assertSame(EmailLogsFilterOperator::NOT_EMPTY, $expectedParams['filters']['subject']['operator']); + $this->assertArrayNotHasKey('value', $expectedParams['filters']['subject']); + } + + public function testGetListWithOperatorAndValueConstants(): void + { + $basePath = AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/email_logs'; + $filters = EmailLogsListFilters::create( + null, + null, + [ + 'status' => FilterCriterion::withValue(EmailLogsFilterOperator::EQUAL, EmailLogsFilterValue::STATUS_DELIVERED), + 'events' => FilterCriterion::withValue(EmailLogsFilterOperator::INCLUDE_EVENT, EmailLogsFilterValue::EVENT_OPEN), + 'sending_stream' => FilterCriterion::withValue(EmailLogsFilterOperator::EQUAL, EmailLogsFilterValue::STREAM_TRANSACTIONAL), + ] + ); + $expectedParams = ['filters' => $filters->toArray()]; + $body = $this->getListResponseBody(); + + $this->emailLogs->expects($this->once()) + ->method('httpGet') + ->with($basePath, $expectedParams) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($body))); + + $response = $this->emailLogs->getList($filters); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(EmailLogsFilterValue::STATUS_DELIVERED, $expectedParams['filters']['status']['value']); + $this->assertSame(EmailLogsFilterValue::EVENT_OPEN, $expectedParams['filters']['events']['value']); + $this->assertSame(EmailLogsFilterValue::STREAM_TRANSACTIONAL, $expectedParams['filters']['sending_stream']['value']); + } + + public function testGetMessage(): void + { + $messageId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; + $basePath = AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/email_logs/' . $messageId; + $body = $this->getMessageResponseBody(); + + $this->emailLogs->expects($this->once()) + ->method('httpGet') + ->with($basePath, []) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($body))); + + $response = $this->emailLogs->getMessage($messageId); + $data = ResponseHelper::toArray($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame($messageId, $data['message_id']); + $this->assertSame('delivered', $data['status']); + $this->assertArrayHasKey('events', $data); + } + + public function testGetMessageThrowsWhenSendingMessageIdIsEmpty(): void + { + $emailLogs = new EmailLogs($this->getConfigMock(), self::FAKE_ACCOUNT_ID); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('sending_message_id must not be empty.'); + + $emailLogs->getMessage(''); + } + + public function testGetMessageThrowsWhenSendingMessageIdIsWhitespaceOnly(): void + { + $emailLogs = new EmailLogs($this->getConfigMock(), self::FAKE_ACCOUNT_ID); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('sending_message_id must not be empty.'); + + $emailLogs->getMessage(' '); + } + + private function getListResponseBody(): array + { + return [ + 'messages' => [ + [ + 'message_id' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + 'status' => 'delivered', + 'subject' => 'Welcome to our service', + 'from' => 'sender@example.com', + 'to' => 'recipient@example.com', + 'sent_at' => '2025-01-15T10:30:00Z', + 'client_ip' => '203.0.113.42', + 'category' => 'Welcome Email', + 'custom_variables' => [], + 'sending_stream' => 'transactional', + 'sending_domain_id' => 3938, + 'template_id' => 100, + 'template_variables' => [], + 'opens_count' => 2, + 'clicks_count' => 1, + ], + [ + 'message_id' => 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + 'status' => 'delivered', + 'subject' => 'Your order confirmation', + 'from' => 'orders@example.com', + 'to' => 'customer@example.com', + 'sent_at' => '2025-01-15T11:00:00Z', + 'client_ip' => null, + 'category' => 'Order Confirmation', + 'custom_variables' => ['order_id' => '12345'], + 'sending_stream' => 'transactional', + 'sending_domain_id' => 3938, + 'template_id' => null, + 'template_variables' => [], + 'opens_count' => 0, + 'clicks_count' => 0, + ], + ], + 'total_count' => 150, + 'next_page_cursor' => 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + ]; + } + + private function getMessageResponseBody(): array + { + return [ + 'message_id' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + 'status' => 'delivered', + 'subject' => 'Welcome to our service', + 'from' => 'sender@example.com', + 'to' => 'recipient@example.com', + 'sent_at' => '2025-01-15T10:30:00Z', + 'client_ip' => '203.0.113.42', + 'category' => 'Welcome Email', + 'custom_variables' => [], + 'sending_stream' => 'transactional', + 'sending_domain_id' => 3938, + 'template_id' => 100, + 'template_variables' => [], + 'opens_count' => 2, + 'clicks_count' => 1, + 'raw_message_url' => 'https://storage.example.com/signed/eml/a1b2c3d4-e5f6-7890-abcd-ef1234567890?token=...', + 'events' => [ + [ + 'event_type' => 'click', + 'created_at' => '2025-01-15T10:35:00Z', + 'details' => [ + 'click_url' => 'https://example.com/track/click/abc123', + 'web_ip_address' => '198.51.100.50', + ], + ], + ], + ]; + } +} diff --git a/tests/MailtrapSendingClientTest.php b/tests/MailtrapSendingClientTest.php index 8d39757..ae34b8c 100644 --- a/tests/MailtrapSendingClientTest.php +++ b/tests/MailtrapSendingClientTest.php @@ -30,7 +30,7 @@ public function mapInstancesProvider(): iterable { foreach (MailtrapSendingClient::API_MAPPING as $key => $item) { yield match ($key) { - 'suppressions', 'domains', 'stats' => [new $item($this->getConfigMock(), self::FAKE_ACCOUNT_ID)], + 'suppressions', 'domains', 'stats', 'emailLogs' => [new $item($this->getConfigMock(), self::FAKE_ACCOUNT_ID)], default => [new $item($this->getConfigMock())], }; }