diff --git a/CHANGELOG.md b/CHANGELOG.md index 62773f3..abe11a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [3.10.0] - 2026-03-06 +- Add Stats API with get, byDomains, byCategories, byEmailServiceProviders, byDate endpoints + ## [3.9.1] - 2025-10-27 - Improve README diff --git a/examples/sending/stats.php b/examples/sending/stats.php new file mode 100644 index 0000000..d29f72c --- /dev/null +++ b/examples/sending/stats.php @@ -0,0 +1,97 @@ +stats($accountId); + +/** + * Get aggregated sending stats. + * + * GET https://mailtrap.io/api/accounts/{account_id}/stats + */ +try { + $response = $stats->get('2026-01-01', '2026-01-31'); + + // print the response body (array) + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} + +/** + * Get aggregated sending stats with filters. + * + * GET https://mailtrap.io/api/accounts/{account_id}/stats + */ +try { + $response = $stats->get( + '2026-01-01', + '2026-01-31', + sendingDomainIds: [1, 2], + sendingStreams: ['transactional'], + categories: ['Transactional', 'Marketing'], + emailServiceProviders: ['Gmail', 'Yahoo'] + ); + + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} + +/** + * Get sending stats grouped by domains. + * + * GET https://mailtrap.io/api/accounts/{account_id}/stats/domains + */ +try { + $response = $stats->byDomains('2026-01-01', '2026-01-31'); + + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} + +/** + * Get sending stats grouped by categories. + * + * GET https://mailtrap.io/api/accounts/{account_id}/stats/categories + */ +try { + $response = $stats->byCategories('2026-01-01', '2026-01-31'); + + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} + +/** + * Get sending stats grouped by email service providers. + * + * GET https://mailtrap.io/api/accounts/{account_id}/stats/email_service_providers + */ +try { + $response = $stats->byEmailServiceProviders('2026-01-01', '2026-01-31'); + + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} + +/** + * Get sending stats grouped by date. + * + * GET https://mailtrap.io/api/accounts/{account_id}/stats/date + */ +try { + $response = $stats->byDate('2026-01-01', '2026-01-31'); + + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} diff --git a/src/Api/Sending/Stats.php b/src/Api/Sending/Stats.php new file mode 100644 index 0000000..dff65ed --- /dev/null +++ b/src/Api/Sending/Stats.php @@ -0,0 +1,192 @@ +handleResponse($this->httpGet( + $this->getBasePath(), + $this->buildQueryParams($startDate, $endDate, $sendingDomainIds, $sendingStreams, $categories, $emailServiceProviders) + )); + } + + /** + * Get sending stats grouped by domains. + * + * @param string $startDate + * @param string $endDate + * @param array $sendingDomainIds + * @param array $sendingStreams + * @param array $categories + * @param array $emailServiceProviders + * + * @return ResponseInterface + */ + public function byDomains( + string $startDate, + string $endDate, + array $sendingDomainIds = [], + array $sendingStreams = [], + array $categories = [], + array $emailServiceProviders = [] + ): ResponseInterface { + return $this->handleResponse($this->httpGet( + $this->getBasePath() . '/domains', + $this->buildQueryParams($startDate, $endDate, $sendingDomainIds, $sendingStreams, $categories, $emailServiceProviders) + )); + } + + /** + * Get sending stats grouped by categories. + * + * @param string $startDate + * @param string $endDate + * @param array $sendingDomainIds + * @param array $sendingStreams + * @param array $categories + * @param array $emailServiceProviders + * + * @return ResponseInterface + */ + public function byCategories( + string $startDate, + string $endDate, + array $sendingDomainIds = [], + array $sendingStreams = [], + array $categories = [], + array $emailServiceProviders = [] + ): ResponseInterface { + return $this->handleResponse($this->httpGet( + $this->getBasePath() . '/categories', + $this->buildQueryParams($startDate, $endDate, $sendingDomainIds, $sendingStreams, $categories, $emailServiceProviders) + )); + } + + /** + * Get sending stats grouped by email service providers. + * + * @param string $startDate + * @param string $endDate + * @param array $sendingDomainIds + * @param array $sendingStreams + * @param array $categories + * @param array $emailServiceProviders + * + * @return ResponseInterface + */ + public function byEmailServiceProviders( + string $startDate, + string $endDate, + array $sendingDomainIds = [], + array $sendingStreams = [], + array $categories = [], + array $emailServiceProviders = [] + ): ResponseInterface { + return $this->handleResponse($this->httpGet( + $this->getBasePath() . '/email_service_providers', + $this->buildQueryParams($startDate, $endDate, $sendingDomainIds, $sendingStreams, $categories, $emailServiceProviders) + )); + } + + /** + * Get sending stats grouped by date. + * + * @param string $startDate + * @param string $endDate + * @param array $sendingDomainIds + * @param array $sendingStreams + * @param array $categories + * @param array $emailServiceProviders + * + * @return ResponseInterface + */ + public function byDate( + string $startDate, + string $endDate, + array $sendingDomainIds = [], + array $sendingStreams = [], + array $categories = [], + array $emailServiceProviders = [] + ): ResponseInterface { + return $this->handleResponse($this->httpGet( + $this->getBasePath() . '/date', + $this->buildQueryParams($startDate, $endDate, $sendingDomainIds, $sendingStreams, $categories, $emailServiceProviders) + )); + } + + public function getAccountId(): int + { + return $this->accountId; + } + + private function getBasePath(): string + { + return sprintf('%s/api/accounts/%s/stats', $this->getHost(), $this->getAccountId()); + } + + private function buildQueryParams( + string $startDate, + string $endDate, + array $sendingDomainIds, + array $sendingStreams, + array $categories, + array $emailServiceProviders + ): array { + $params = [ + 'start_date' => $startDate, + 'end_date' => $endDate, + ]; + + if (count($sendingDomainIds) > 0) { + $params['sending_domain_ids'] = $sendingDomainIds; + } + + if (count($sendingStreams) > 0) { + $params['sending_streams'] = $sendingStreams; + } + + if (count($categories) > 0) { + $params['categories'] = $categories; + } + + if (count($emailServiceProviders) > 0) { + $params['email_service_providers'] = $emailServiceProviders; + } + + return $params; + } +} diff --git a/src/MailtrapSendingClient.php b/src/MailtrapSendingClient.php index 8c0ece4..609811e 100644 --- a/src/MailtrapSendingClient.php +++ b/src/MailtrapSendingClient.php @@ -8,6 +8,7 @@ * @method Api\Sending\Emails emails() * @method Api\Sending\Suppression suppressions(int $accountId) * @method Api\Sending\Domain domains(int $accountId) + * @method Api\Sending\Stats stats(int $accountId) * * Class MailtrapSendingClient */ @@ -17,5 +18,6 @@ final class MailtrapSendingClient extends AbstractMailtrapClient implements Emai 'emails' => Api\Sending\Emails::class, 'suppressions' => Api\Sending\Suppression::class, 'domains' => Api\Sending\Domain::class, + 'stats' => Api\Sending\Stats::class, ]; } diff --git a/tests/Api/Sending/StatsTest.php b/tests/Api/Sending/StatsTest.php new file mode 100644 index 0000000..70a7fa5 --- /dev/null +++ b/tests/Api/Sending/StatsTest.php @@ -0,0 +1,267 @@ +stats = $this->getMockBuilder(Stats::class) + ->onlyMethods(['httpGet']) + ->setConstructorArgs([$this->getConfigMock(), self::FAKE_ACCOUNT_ID]) + ->getMock(); + } + + protected function tearDown(): void + { + $this->stats = null; + + parent::tearDown(); + } + + public function testGet(): void + { + $expectedData = $this->getSampleStatsData(); + + $this->stats->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/stats', + ['start_date' => '2026-01-01', 'end_date' => '2026-01-31'] + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedData))); + + $response = $this->stats->get('2026-01-01', '2026-01-31'); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(150, $responseData['delivery_count']); + $this->assertEquals(0.95, $responseData['delivery_rate']); + $this->assertEquals(8, $responseData['bounce_count']); + $this->assertEquals(120, $responseData['open_count']); + $this->assertEquals(60, $responseData['click_count']); + $this->assertEquals(2, $responseData['spam_count']); + } + + public function testGetWithFilters(): void + { + $expectedData = $this->getSampleStatsData(); + + $this->stats->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/stats', + [ + 'start_date' => '2026-01-01', + 'end_date' => '2026-01-31', + 'sending_domain_ids' => [1, 2], + 'sending_streams' => ['transactional'], + 'categories' => ['Transactional'], + 'email_service_providers' => ['Gmail'], + ] + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedData))); + + $response = $this->stats->get( + '2026-01-01', + '2026-01-31', + [1, 2], + ['transactional'], + ['Transactional'], + ['Gmail'] + ); + + $this->assertInstanceOf(Response::class, $response); + } + + public function testGetForbidden(): void + { + $this->stats->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/stats', + ['start_date' => '2026-01-01', 'end_date' => '2026-01-31'] + ) + ->willReturn( + new Response(403, ['Content-Type' => 'application/json'], json_encode(['errors' => 'Access forbidden'])) + ); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage( + 'Forbidden. Make sure domain verification process is completed or check your permissions. Errors: Access forbidden.' + ); + + $this->stats->get('2026-01-01', '2026-01-31'); + } + + public function testByDomains(): void + { + $expectedData = $this->getSampleGroupedByDomainsData(); + + $this->stats->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/stats/domains', + ['start_date' => '2026-01-01', 'end_date' => '2026-01-31'] + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedData))); + + $response = $this->stats->byDomains('2026-01-01', '2026-01-31'); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertCount(2, $responseData); + $this->assertEquals(1, $responseData[0]['sending_domain_id']); + $this->assertEquals(100, $responseData[0]['stats']['delivery_count']); + $this->assertEquals(2, $responseData[1]['sending_domain_id']); + $this->assertEquals(50, $responseData[1]['stats']['delivery_count']); + } + + public function testByCategories(): void + { + $expectedData = [ + [ + 'category' => 'Transactional', + 'stats' => [ + 'delivery_count' => 100, 'delivery_rate' => 0.97, + 'bounce_count' => 3, 'bounce_rate' => 0.03, + 'open_count' => 85, 'open_rate' => 0.85, + 'click_count' => 45, 'click_rate' => 0.53, + 'spam_count' => 0, 'spam_rate' => 0.0, + ], + ], + ]; + + $this->stats->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/stats/categories', + ['start_date' => '2026-01-01', 'end_date' => '2026-01-31'] + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedData))); + + $response = $this->stats->byCategories('2026-01-01', '2026-01-31'); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals('Transactional', $responseData[0]['category']); + $this->assertEquals(100, $responseData[0]['stats']['delivery_count']); + } + + public function testByEmailServiceProviders(): void + { + $expectedData = [ + [ + 'email_service_provider' => 'Gmail', + 'stats' => [ + 'delivery_count' => 80, 'delivery_rate' => 0.97, + 'bounce_count' => 2, 'bounce_rate' => 0.03, + 'open_count' => 70, 'open_rate' => 0.88, + 'click_count' => 35, 'click_rate' => 0.5, + 'spam_count' => 1, 'spam_rate' => 0.013, + ], + ], + ]; + + $this->stats->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/stats/email_service_providers', + ['start_date' => '2026-01-01', 'end_date' => '2026-01-31'] + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedData))); + + $response = $this->stats->byEmailServiceProviders('2026-01-01', '2026-01-31'); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals('Gmail', $responseData[0]['email_service_provider']); + $this->assertEquals(80, $responseData[0]['stats']['delivery_count']); + } + + public function testByDate(): void + { + $expectedData = [ + [ + 'date' => '2026-01-01', + 'stats' => [ + 'delivery_count' => 5, 'delivery_rate' => 1.0, + 'bounce_count' => 0, 'bounce_rate' => 0.0, + 'open_count' => 4, 'open_rate' => 0.8, + 'click_count' => 2, 'click_rate' => 0.5, + 'spam_count' => 0, 'spam_rate' => 0.0, + ], + ], + ]; + + $this->stats->expects($this->once()) + ->method('httpGet') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/stats/date', + ['start_date' => '2026-01-01', 'end_date' => '2026-01-31'] + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedData))); + + $response = $this->stats->byDate('2026-01-01', '2026-01-31'); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals('2026-01-01', $responseData[0]['date']); + $this->assertEquals(5, $responseData[0]['stats']['delivery_count']); + } + + private function getSampleStatsData(): array + { + return [ + 'delivery_count' => 150, 'delivery_rate' => 0.95, + 'bounce_count' => 8, 'bounce_rate' => 0.05, + 'open_count' => 120, 'open_rate' => 0.8, + 'click_count' => 60, 'click_rate' => 0.5, + 'spam_count' => 2, 'spam_rate' => 0.013, + ]; + } + + private function getSampleGroupedByDomainsData(): array + { + return [ + [ + 'sending_domain_id' => 1, + 'stats' => [ + 'delivery_count' => 100, 'delivery_rate' => 0.96, + 'bounce_count' => 4, 'bounce_rate' => 0.04, + 'open_count' => 80, 'open_rate' => 0.8, + 'click_count' => 40, 'click_rate' => 0.5, + 'spam_count' => 1, 'spam_rate' => 0.01, + ], + ], + [ + 'sending_domain_id' => 2, + 'stats' => [ + 'delivery_count' => 50, 'delivery_rate' => 0.93, + 'bounce_count' => 4, 'bounce_rate' => 0.07, + 'open_count' => 40, 'open_rate' => 0.8, + 'click_count' => 20, 'click_rate' => 0.5, + 'spam_count' => 1, 'spam_rate' => 0.02, + ], + ], + ]; + } +} diff --git a/tests/MailtrapSendingClientTest.php b/tests/MailtrapSendingClientTest.php index 9c5a35f..8d39757 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' => [new $item($this->getConfigMock(), self::FAKE_ACCOUNT_ID)], + 'suppressions', 'domains', 'stats' => [new $item($this->getConfigMock(), self::FAKE_ACCOUNT_ID)], default => [new $item($this->getConfigMock())], }; }