From 0cd4f2574d8a85c8eda39ad3af0cc2a1dbe18550 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Thu, 27 Aug 2026 10:52:20 +0200 Subject: [PATCH 1/3] Add tracking opt-outs and create-suppression endpoints Fills the gap against the OpenAPI spec: POST on suppressions plus the tracking opt-outs list/create/delete. Both endpoints take a flat body rather than a wrapped one, so the DTOs are posted as-is. Tracking opt-outs are their own Api\Sending\TrackingOptOut registered as trackingOptOuts. The spec path is /api/tracking_opt_outs and the account comes from the API token, so the class takes no constructor arguments. --- README.md | 3 +- examples/README.md | 1 + examples/sending/suppressions.php | 25 +++ examples/sending/tracking-opt-outs.php | 74 ++++++ src/Api/Sending/Suppression.php | 17 ++ src/Api/Sending/TrackingOptOut.php | 71 ++++++ .../Request/Suppression/CreateSuppression.php | 43 ++++ src/DTO/Request/Suppression/Suppression.php | 23 ++ .../Suppression/SuppressionInterface.php | 11 + .../TrackingOptOut/CreateTrackingOptOut.php | 31 +++ .../TrackingOptOutInterface.php | 11 + .../TrackingOptOut/TrackingOptOutsFilter.php | 50 +++++ src/MailtrapSendingClient.php | 16 +- tests/Api/Sending/SuppressionTest.php | 81 ++++++- tests/Api/Sending/TrackingOptOutTest.php | 210 ++++++++++++++++++ 15 files changed, 658 insertions(+), 9 deletions(-) create mode 100644 examples/sending/tracking-opt-outs.php create mode 100644 src/Api/Sending/TrackingOptOut.php create mode 100644 src/DTO/Request/Suppression/CreateSuppression.php create mode 100644 src/DTO/Request/Suppression/Suppression.php create mode 100644 src/DTO/Request/Suppression/SuppressionInterface.php create mode 100644 src/DTO/Request/TrackingOptOut/CreateTrackingOptOut.php create mode 100644 src/DTO/Request/TrackingOptOut/TrackingOptOutInterface.php create mode 100644 src/DTO/Request/TrackingOptOut/TrackingOptOutsFilter.php create mode 100644 tests/Api/Sending/TrackingOptOutTest.php diff --git a/README.md b/README.md index b56935f..288aa8f 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,8 @@ Email API: - 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) - Sending domain company info – [`company-info/all.php`](examples/company-info/all.php) -- Suppressions (find & delete) – [`sending/suppressions.php`](examples/sending/suppressions.php) +- Suppressions (create, find & delete) – [`sending/suppressions.php`](examples/sending/suppressions.php) +- Tracking Opt-outs (list, create & delete) – [`sending/tracking-opt-outs.php`](examples/sending/tracking-opt-outs.php) - Email Logs (list & get by message ID) – [`sending/email-logs.php`](examples/sending/email-logs.php) Email Sandbox (Testing): diff --git a/examples/README.md b/examples/README.md index bc0adaa..3826808 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,7 @@ Central index of runnable example scripts demonstrating Mailtrap PHP SDK feature | Full featured send (headers, vars, attachments) | [`sending/all.php`](sending/all.php) | | Send using a template (transactional stream) | [`sending/template.php`](sending/template.php) | | Suppressions API usage | [`sending/suppressions.php`](sending/suppressions.php) | +| Tracking Opt-outs API usage | [`sending/tracking-opt-outs.php`](sending/tracking-opt-outs.php) | | Bulk API single send (stream selection) | [`bulk/bulk.php`](bulk/bulk.php) | | Bulk API template send | [`bulk/bulk_template.php`](bulk/bulk_template.php) | diff --git a/examples/sending/suppressions.php b/examples/sending/suppressions.php index 0680524..2aba08d 100644 --- a/examples/sending/suppressions.php +++ b/examples/sending/suppressions.php @@ -3,6 +3,8 @@ declare(strict_types=1); use Mailtrap\Config; +use Mailtrap\DTO\Request\Suppression\CreateSuppression; +use Mailtrap\DTO\Request\Suppression\Suppression; use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapSendingClient; @@ -31,6 +33,29 @@ } +/** + * Create Suppression. + * + * POST https://mailtrap.io/api/accounts/{account_id}/suppressions + */ +try { + // `type` is optional and defaults to "manual import" when omitted. + $response = $mailtrapSuppression->createSuppression( + new CreateSuppression( + email: 'some_email@mail.com', + domainId: (int) $_ENV['MAILTRAP_DOMAIN_ID'], + sendingStream: Suppression::SENDING_STREAM_TRANSACTIONAL, + type: Suppression::TYPE_MANUAL_IMPORT + ) + ); + + // Print the response body (array) + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} + + /** * Delete Suppression by ID. * diff --git a/examples/sending/tracking-opt-outs.php b/examples/sending/tracking-opt-outs.php new file mode 100644 index 0000000..ad7cb55 --- /dev/null +++ b/examples/sending/tracking-opt-outs.php @@ -0,0 +1,74 @@ +trackingOptOuts(); + +/** + * Create a tracking opt-out + * + * POST https://mailtrap.io/api/tracking_opt_outs + */ +try { + $response = $trackingOptOuts->createTrackingOptOut( + new CreateTrackingOptOut(email: 'tracked@example.com', domainId: $domainId) + ); + + // print the response body (array) + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} + + +/** + * Get tracking opt-outs + * + * GET https://mailtrap.io/api/tracking_opt_outs + * + * Returns up to 1000 records per request. When `last_id` is not null, pass it + * back as a filter to fetch the next page. + */ +try { + $response = $trackingOptOuts->getTrackingOptOuts(); + + // OR filter by email and creation time + $response = $trackingOptOuts->getTrackingOptOuts( + new TrackingOptOutsFilter( + email: 'tracked@example.com', + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-12-31T23:59:59Z' + ) + ); + + // print the response body (array) + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} + + +/** + * Delete a tracking opt-out + * + * DELETE https://mailtrap.io/api/tracking_opt_outs/{tracking_opt_out_id} + */ +try { + $response = $trackingOptOuts->deleteTrackingOptOut('64d71bf3-1276-417b-86e1-8e66f138acfe'); + + // print the response body (array) — the deleted record + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} diff --git a/src/Api/Sending/Suppression.php b/src/Api/Sending/Suppression.php index 71b95b1..3b557d5 100644 --- a/src/Api/Sending/Suppression.php +++ b/src/Api/Sending/Suppression.php @@ -6,6 +6,7 @@ use Mailtrap\Api\AbstractApi; use Mailtrap\ConfigInterface; +use Mailtrap\DTO\Request\Suppression\CreateSuppression; use Psr\Http\Message\ResponseInterface; /** @@ -34,6 +35,22 @@ public function getSuppressions(?string $email = null): ResponseInterface ); } + /** + * Add an email address to the account's suppression list. + * + * @param CreateSuppression $suppression + * @return ResponseInterface + */ + public function createSuppression(CreateSuppression $suppression): ResponseInterface + { + return $this->handleResponse( + $this->httpPost( + path: $this->getBasePath(), + body: $suppression->toArray() + ) + ); + } + /** * Delete a suppression by ID (UUID). * Mailtrap will no longer prevent sending to this email unless it's recorded in suppressions again. diff --git a/src/Api/Sending/TrackingOptOut.php b/src/Api/Sending/TrackingOptOut.php new file mode 100644 index 0000000..ab6b839 --- /dev/null +++ b/src/Api/Sending/TrackingOptOut.php @@ -0,0 +1,71 @@ +handleResponse( + $this->httpGet( + $this->getBasePath(), + $filter ? $filter->toArray() : [] + ) + ); + } + + /** + * Add an email address to the tracking opt-out list for a sending domain. + * + * @param CreateTrackingOptOut $trackingOptOut + * @return ResponseInterface + */ + public function createTrackingOptOut(CreateTrackingOptOut $trackingOptOut): ResponseInterface + { + return $this->handleResponse( + $this->httpPost( + path: $this->getBasePath(), + body: $trackingOptOut->toArray() + ) + ); + } + + /** + * Remove an email address from the tracking opt-out list so open and click + * tracking can apply again. + * + * @param string $trackingOptOutId + * @return ResponseInterface + */ + public function deleteTrackingOptOut(string $trackingOptOutId): ResponseInterface + { + return $this->handleResponse( + $this->httpDelete( + sprintf('%s/%s', $this->getBasePath(), $trackingOptOutId) + ) + ); + } + + private function getBasePath(): string + { + return sprintf('%s/api/tracking_opt_outs', $this->getHost()); + } +} diff --git a/src/DTO/Request/Suppression/CreateSuppression.php b/src/DTO/Request/Suppression/CreateSuppression.php new file mode 100644 index 0000000..3a367e5 --- /dev/null +++ b/src/DTO/Request/Suppression/CreateSuppression.php @@ -0,0 +1,43 @@ + $this->email, + 'domain_id' => $this->domainId, + 'sending_stream' => $this->sendingStream, + ]; + + if ($this->type !== null) { + $payload['type'] = $this->type; + } + + return $payload; + } +} diff --git a/src/DTO/Request/Suppression/Suppression.php b/src/DTO/Request/Suppression/Suppression.php new file mode 100644 index 0000000..adae240 --- /dev/null +++ b/src/DTO/Request/Suppression/Suppression.php @@ -0,0 +1,23 @@ + $this->email, + 'domain_id' => $this->domainId, + ]; + } +} diff --git a/src/DTO/Request/TrackingOptOut/TrackingOptOutInterface.php b/src/DTO/Request/TrackingOptOut/TrackingOptOutInterface.php new file mode 100644 index 0000000..46da8c4 --- /dev/null +++ b/src/DTO/Request/TrackingOptOut/TrackingOptOutInterface.php @@ -0,0 +1,11 @@ +email !== null) { + $payload['email'] = $this->email; + } + + if ($this->startTime !== null) { + $payload['start_time'] = $this->startTime; + } + + if ($this->endTime !== null) { + $payload['end_time'] = $this->endTime; + } + + if ($this->lastId !== null) { + $payload['last_id'] = $this->lastId; + } + + return $payload; + } +} diff --git a/src/MailtrapSendingClient.php b/src/MailtrapSendingClient.php index 48e5f1b..d4994c5 100644 --- a/src/MailtrapSendingClient.php +++ b/src/MailtrapSendingClient.php @@ -5,13 +5,14 @@ namespace Mailtrap; /** - * @method Api\Sending\Emails emails() - * @method Api\Sending\Suppression suppressions(int $accountId) - * @method Api\Sending\Domain domains(int $accountId) - * @method Api\Sending\CompanyInfo companyInfo(int $domainId) - * @method Api\Sending\Stats stats(int $accountId) - * @method Api\Sending\EmailLogs emailLogs(int $accountId) - * @method Api\Sending\Webhook webhooks(int $accountId) + * @method Api\Sending\Emails emails() + * @method Api\Sending\Suppression suppressions(int $accountId) + * @method Api\Sending\TrackingOptOut trackingOptOuts() + * @method Api\Sending\Domain domains(int $accountId) + * @method Api\Sending\CompanyInfo companyInfo(int $domainId) + * @method Api\Sending\Stats stats(int $accountId) + * @method Api\Sending\EmailLogs emailLogs(int $accountId) + * @method Api\Sending\Webhook webhooks(int $accountId) * * Class MailtrapSendingClient */ @@ -20,6 +21,7 @@ final class MailtrapSendingClient extends AbstractMailtrapClient implements Emai public const API_MAPPING = [ 'emails' => Api\Sending\Emails::class, 'suppressions' => Api\Sending\Suppression::class, + 'trackingOptOuts' => Api\Sending\TrackingOptOut::class, 'domains' => Api\Sending\Domain::class, 'companyInfo' => Api\Sending\CompanyInfo::class, 'stats' => Api\Sending\Stats::class, diff --git a/tests/Api/Sending/SuppressionTest.php b/tests/Api/Sending/SuppressionTest.php index ba2f061..164ec4d 100644 --- a/tests/Api/Sending/SuppressionTest.php +++ b/tests/Api/Sending/SuppressionTest.php @@ -6,6 +6,8 @@ use Mailtrap\Api\AbstractApi; use Mailtrap\Api\Sending\Suppression; +use Mailtrap\DTO\Request\Suppression\CreateSuppression; +use Mailtrap\DTO\Request\Suppression\Suppression as SuppressionDto; use Mailtrap\Exception\HttpClientException; use Mailtrap\Helper\ResponseHelper; use Mailtrap\Tests\MailtrapTestCase; @@ -18,13 +20,15 @@ */ class SuppressionTest extends MailtrapTestCase { + private const DOMAIN_ID = 12345; + private ?Suppression $suppression; protected function setUp(): void { parent::setUp(); $this->suppression = $this->getMockBuilder(Suppression::class) - ->onlyMethods(['httpGet', 'httpDelete']) + ->onlyMethods(['httpGet', 'httpPost', 'httpDelete']) ->setConstructorArgs([$this->getConfigMock(), self::FAKE_ACCOUNT_ID]) ->getMock(); } @@ -103,6 +107,81 @@ public function testDeleteSuppressionNotFound(): void $this->suppression->deleteSuppression($suppressionId); } + public function testCreateSuppression(): void + { + $this->suppression->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/suppressions', + [], + [ + 'email' => 'recipient@example.com', + 'domain_id' => self::DOMAIN_ID, + 'sending_stream' => SuppressionDto::SENDING_STREAM_TRANSACTIONAL, + ] + ) + ->willReturn( + new Response(201, ['Content-Type' => 'application/json'], $this->getExpectedCreateResponse()) + ); + + $response = $this->suppression->createSuppression( + new CreateSuppression( + email: 'recipient@example.com', + domainId: self::DOMAIN_ID, + sendingStream: SuppressionDto::SENDING_STREAM_TRANSACTIONAL + ) + ); + $responseData = ResponseHelper::toArray($response); + + $this->assertSame(201, $response->getStatusCode()); + $this->assertArrayHasKey('data', $responseData); + $this->assertSame('recipient@example.com', $responseData['data']['email']); + } + + public function testCreateSuppressionWithExplicitType(): void + { + $this->suppression->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/suppressions', + [], + [ + 'email' => 'recipient@example.com', + 'domain_id' => self::DOMAIN_ID, + 'sending_stream' => SuppressionDto::SENDING_STREAM_BULK, + 'type' => SuppressionDto::TYPE_SPAM_COMPLAINT, + ] + ) + ->willReturn( + new Response(201, ['Content-Type' => 'application/json'], $this->getExpectedCreateResponse()) + ); + + $response = $this->suppression->createSuppression( + new CreateSuppression( + email: 'recipient@example.com', + domainId: self::DOMAIN_ID, + sendingStream: SuppressionDto::SENDING_STREAM_BULK, + type: SuppressionDto::TYPE_SPAM_COMPLAINT + ) + ); + + $this->assertSame(201, $response->getStatusCode()); + } + + private function getExpectedCreateResponse(): string + { + return json_encode([ + 'data' => [ + 'id' => '25594eef-87e0-49c7-a647-cc316f9fdb42', + 'type' => 'manual import', + 'created_at' => '2025-05-25T10:07:49Z', + 'email' => 'recipient@example.com', + 'sending_stream' => 'transactional', + 'domain_name' => 'example.com', + ] + ]); + } + private function getExpectedResponse(string $email): string { return json_encode([ diff --git a/tests/Api/Sending/TrackingOptOutTest.php b/tests/Api/Sending/TrackingOptOutTest.php new file mode 100644 index 0000000..4eac6f3 --- /dev/null +++ b/tests/Api/Sending/TrackingOptOutTest.php @@ -0,0 +1,210 @@ +trackingOptOut = $this->getMockBuilder(TrackingOptOut::class) + ->onlyMethods(['httpGet', 'httpPost', 'httpDelete']) + ->setConstructorArgs([$this->getConfigMock()]) + ->getMock(); + } + + protected function tearDown(): void + { + $this->trackingOptOut = null; + parent::tearDown(); + } + + public function testGetTrackingOptOuts(): void + { + $this->trackingOptOut->expects($this->once()) + ->method('httpGet') + ->with($this->getExpectedPath(), []) + ->willReturn( + new Response(200, ['Content-Type' => 'application/json'], $this->getExpectedListResponse()) + ); + + $response = $this->trackingOptOut->getTrackingOptOuts(); + $responseData = ResponseHelper::toArray($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertArrayHasKey('data', $responseData); + $this->assertArrayHasKey('last_id', $responseData); + $this->assertSame('tracked@example.com', $responseData['data'][0]['email']); + } + + public function testGetTrackingOptOutsWithFilter(): void + { + $this->trackingOptOut->expects($this->once()) + ->method('httpGet') + ->with( + $this->getExpectedPath(), + [ + 'email' => 'tracked@example.com', + 'start_time' => '2025-01-01T00:00:00Z', + 'end_time' => '2025-12-31T23:59:59Z', + 'last_id' => self::OPT_OUT_ID, + ] + ) + ->willReturn( + new Response(200, ['Content-Type' => 'application/json'], $this->getExpectedListResponse()) + ); + + $response = $this->trackingOptOut->getTrackingOptOuts( + new TrackingOptOutsFilter( + email: 'tracked@example.com', + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-12-31T23:59:59Z', + lastId: self::OPT_OUT_ID + ) + ); + + $this->assertSame(200, $response->getStatusCode()); + } + + public function testGetTrackingOptOutsOmitsUnsetFilters(): void + { + $this->trackingOptOut->expects($this->once()) + ->method('httpGet') + ->with($this->getExpectedPath(), ['email' => 'tracked@example.com']) + ->willReturn( + new Response(200, ['Content-Type' => 'application/json'], $this->getExpectedListResponse()) + ); + + $response = $this->trackingOptOut->getTrackingOptOuts( + new TrackingOptOutsFilter(email: 'tracked@example.com') + ); + + $this->assertSame(200, $response->getStatusCode()); + } + + public function testCreateTrackingOptOut(): void + { + $this->trackingOptOut->expects($this->once()) + ->method('httpPost') + ->with( + $this->getExpectedPath(), + [], + ['email' => 'tracked@example.com', 'domain_id' => self::DOMAIN_ID] + ) + ->willReturn( + new Response(201, ['Content-Type' => 'application/json'], $this->getExpectedCreateResponse()) + ); + + $response = $this->trackingOptOut->createTrackingOptOut( + new CreateTrackingOptOut(email: 'tracked@example.com', domainId: self::DOMAIN_ID) + ); + $responseData = ResponseHelper::toArray($response); + + $this->assertSame(201, $response->getStatusCode()); + $this->assertArrayHasKey('data', $responseData); + $this->assertSame(self::OPT_OUT_ID, $responseData['data']['id']); + } + + public function testCreateTrackingOptOutInvalid(): void + { + $this->trackingOptOut->expects($this->once()) + ->method('httpPost') + ->willReturn( + new Response( + 422, + ['Content-Type' => 'application/json'], + json_encode(['errors' => 'Email is invalid']) + ) + ); + + $this->expectException(HttpClientException::class); + + $this->trackingOptOut->createTrackingOptOut( + new CreateTrackingOptOut(email: 'not-an-email', domainId: self::DOMAIN_ID) + ); + } + + public function testDeleteTrackingOptOut(): void + { + $this->trackingOptOut->expects($this->once()) + ->method('httpDelete') + ->with($this->getExpectedPath() . '/' . self::OPT_OUT_ID) + ->willReturn( + new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($this->getOptOutPayload()) + ) + ); + + $response = $this->trackingOptOut->deleteTrackingOptOut(self::OPT_OUT_ID); + $responseData = ResponseHelper::toArray($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(self::OPT_OUT_ID, $responseData['id']); + } + + public function testDeleteTrackingOptOutNotFound(): void + { + $this->trackingOptOut->expects($this->once()) + ->method('httpDelete') + ->with($this->getExpectedPath() . '/missing') + ->willReturn( + new Response( + 404, + ['Content-Type' => 'application/json'], + json_encode(['errors' => 'Tracking opt-out not found']) + ) + ); + + $this->expectException(HttpClientException::class); + + $this->trackingOptOut->deleteTrackingOptOut('missing'); + } + + private function getExpectedPath(): string + { + return AbstractApi::DEFAULT_HOST . '/api/tracking_opt_outs'; + } + + private function getOptOutPayload(): array + { + return [ + 'id' => self::OPT_OUT_ID, + 'email' => 'tracked@example.com', + 'created_at' => '2025-01-15T10:30:00Z', + 'domain_name' => 'example.com', + ]; + } + + private function getExpectedListResponse(): string + { + return json_encode(['data' => [$this->getOptOutPayload()], 'last_id' => null]); + } + + private function getExpectedCreateResponse(): string + { + return json_encode(['data' => $this->getOptOutPayload()]); + } +} From c23b7e94c0a8c4af7bfd14c688fedf90a5d653e8 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Thu, 27 Aug 2026 10:52:41 +0200 Subject: [PATCH 2/3] Expose all suppressions list filters The endpoint accepts email, start_time, end_time and last_id, but getSuppressions only ever sent email, leaving three filters unreachable -- including last_id, which is how a caller pages a 1000-record list. It now also accepts a SuppressionsFilter carrying all four, following EmailLogs::getList, which takes either an array or a filter object. The string form stays valid, so existing calls are unaffected. --- examples/sending/suppressions.php | 10 ++++ src/Api/Sending/Suppression.php | 20 +++++--- .../Suppression/SuppressionsFilter.php | 50 +++++++++++++++++++ tests/Api/Sending/SuppressionTest.php | 47 +++++++++++++++++ 4 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 src/DTO/Request/Suppression/SuppressionsFilter.php diff --git a/examples/sending/suppressions.php b/examples/sending/suppressions.php index 2aba08d..a75afbe 100644 --- a/examples/sending/suppressions.php +++ b/examples/sending/suppressions.php @@ -5,6 +5,7 @@ use Mailtrap\Config; use Mailtrap\DTO\Request\Suppression\CreateSuppression; use Mailtrap\DTO\Request\Suppression\Suppression; +use Mailtrap\DTO\Request\Suppression\SuppressionsFilter; use Mailtrap\Helper\ResponseHelper; use Mailtrap\MailtrapSendingClient; @@ -26,6 +27,15 @@ // OR get suppressions by email $response = $mailtrapSuppression->getSuppressions('some_email@mail.com'); + // OR filter by email and creation time + $response = $mailtrapSuppression->getSuppressions( + new SuppressionsFilter( + email: 'some_email@mail.com', + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-12-31T23:59:59Z' + ) + ); + // Print the response body (array) var_dump(ResponseHelper::toArray($response)); } catch (Exception $e) { diff --git a/src/Api/Sending/Suppression.php b/src/Api/Sending/Suppression.php index 3b557d5..221586e 100644 --- a/src/Api/Sending/Suppression.php +++ b/src/Api/Sending/Suppression.php @@ -7,6 +7,7 @@ use Mailtrap\Api\AbstractApi; use Mailtrap\ConfigInterface; use Mailtrap\DTO\Request\Suppression\CreateSuppression; +use Mailtrap\DTO\Request\Suppression\SuppressionsFilter; use Psr\Http\Message\ResponseInterface; /** @@ -20,18 +21,23 @@ public function __construct(ConfigInterface $config, private int $accountId) } /** - * List and search suppressions by email. The endpoint returns up to 1000 suppressions per request. + * List and search suppressions. The endpoint returns up to 1000 suppressions per request. * - * @param string|null $email The email to filter suppressions by, or null to get all. + * @param string|SuppressionsFilter|null $filter Either an email to filter by, a + * SuppressionsFilter for the full set of + * filters, or null to get all. * @return ResponseInterface */ - public function getSuppressions(?string $email = null): ResponseInterface + public function getSuppressions(string|SuppressionsFilter|null $filter = null): ResponseInterface { + $queryParams = match (true) { + $filter instanceof SuppressionsFilter => $filter->toArray(), + is_string($filter) && $filter !== '' => ['email' => $filter], + default => [], + }; + return $this->handleResponse( - $this->httpGet( - $this->getBasePath(), - $email ? ['email' => $email] : [] - ) + $this->httpGet($this->getBasePath(), $queryParams) ); } diff --git a/src/DTO/Request/Suppression/SuppressionsFilter.php b/src/DTO/Request/Suppression/SuppressionsFilter.php new file mode 100644 index 0000000..270b5ff --- /dev/null +++ b/src/DTO/Request/Suppression/SuppressionsFilter.php @@ -0,0 +1,50 @@ +email !== null) { + $payload['email'] = $this->email; + } + + if ($this->startTime !== null) { + $payload['start_time'] = $this->startTime; + } + + if ($this->endTime !== null) { + $payload['end_time'] = $this->endTime; + } + + if ($this->lastId !== null) { + $payload['last_id'] = $this->lastId; + } + + return $payload; + } +} diff --git a/tests/Api/Sending/SuppressionTest.php b/tests/Api/Sending/SuppressionTest.php index 164ec4d..53f85be 100644 --- a/tests/Api/Sending/SuppressionTest.php +++ b/tests/Api/Sending/SuppressionTest.php @@ -8,6 +8,7 @@ use Mailtrap\Api\Sending\Suppression; use Mailtrap\DTO\Request\Suppression\CreateSuppression; use Mailtrap\DTO\Request\Suppression\Suppression as SuppressionDto; +use Mailtrap\DTO\Request\Suppression\SuppressionsFilter; use Mailtrap\Exception\HttpClientException; use Mailtrap\Helper\ResponseHelper; use Mailtrap\Tests\MailtrapTestCase; @@ -107,6 +108,52 @@ public function testDeleteSuppressionNotFound(): void $this->suppression->deleteSuppression($suppressionId); } + public function testGetSuppressionsWithFilter(): void + { + $this->suppression->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/suppressions', + [ + 'email' => 'john@example.com', + 'start_time' => '2025-01-01T00:00:00Z', + 'end_time' => '2025-12-31T23:59:59Z', + 'last_id' => '25594eef-87e0-49c7-a647-cc316f9fdb42', + ] + ) + ->willReturn( + new Response(200, ['Content-Type' => 'application/json'], $this->getExpectedResponse('john@example.com')) + ); + + $response = $this->suppression->getSuppressions( + new SuppressionsFilter( + email: 'john@example.com', + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-12-31T23:59:59Z', + lastId: '25594eef-87e0-49c7-a647-cc316f9fdb42' + ) + ); + + $this->assertSame(200, $response->getStatusCode()); + } + + public function testGetSuppressionsWithFilterOmitsUnsetFields(): void + { + $this->suppression->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/suppressions', + ['email' => 'john@example.com'] + ) + ->willReturn( + new Response(200, ['Content-Type' => 'application/json'], $this->getExpectedResponse('john@example.com')) + ); + + $response = $this->suppression->getSuppressions(new SuppressionsFilter(email: 'john@example.com')); + + $this->assertSame(200, $response->getStatusCode()); + } + public function testCreateSuppression(): void { $this->suppression->expects($this->once()) From c2904c40684caa4041f6418908b7b3d545c1a73a Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Thu, 27 Aug 2026 10:52:55 +0200 Subject: [PATCH 3/3] Move the company info example next to sending domains Company info is a sending-domain sub-resource, so the example belongs under sending-domains/ with the other per-resource files rather than in a folder of its own. --- README.md | 2 +- examples/README.md | 2 +- .../{company-info/all.php => sending-domains/company-info.php} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename examples/{company-info/all.php => sending-domains/company-info.php} (100%) diff --git a/README.md b/README.md index 288aa8f..86f4575 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ 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) -- Sending domain company info – [`company-info/all.php`](examples/company-info/all.php) +- Sending domain company info – [`sending-domains/company-info.php`](examples/sending-domains/company-info.php) - Suppressions (create, find & delete) – [`sending/suppressions.php`](examples/sending/suppressions.php) - Tracking Opt-outs (list, create & delete) – [`sending/tracking-opt-outs.php`](examples/sending/tracking-opt-outs.php) - Email Logs (list & get by message ID) – [`sending/email-logs.php`](examples/sending/email-logs.php) diff --git a/examples/README.md b/examples/README.md index 3826808..c0aaa95 100644 --- a/examples/README.md +++ b/examples/README.md @@ -66,7 +66,7 @@ Central index of runnable example scripts demonstrating Mailtrap PHP SDK feature |---------|------| | Templates CRUD | [`templates/all.php`](templates/all.php) | | Sending domains CRUD | [`sending-domains/all.php`](sending-domains/all.php) | -| Sending domain company info | [`company-info/all.php`](company-info/all.php) | +| Sending domain company info | [`sending-domains/company-info.php`](sending-domains/company-info.php) | ### General API diff --git a/examples/company-info/all.php b/examples/sending-domains/company-info.php similarity index 100% rename from examples/company-info/all.php rename to examples/sending-domains/company-info.php