Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ Contact management:

Email marketing:
- Email campaigns CRUD, listing, lifecycle & stats – [`email-campaigns/all.php`](examples/email-campaigns/all.php)
- Email marketing stats (campaigns & automations) – [`marketing-stats/all.php`](examples/marketing-stats/all.php)

General API:
- Templates CRUD, paginated (experimental) – [`templates/templates.php`](examples/templates/templates.php)
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Central index of runnable example scripts demonstrating Mailtrap PHP SDK feature
| Contact lists CRUD | [`contact-lists/all.php`](contact-lists/all.php) |
| Custom fields CRUD | [`contact-fields/all.php`](contact-fields/all.php) |
| Email campaigns CRUD + list + lifecycle + stats | [`email-campaigns/all.php`](email-campaigns/all.php) |
| Email marketing stats (campaigns + automations) | [`marketing-stats/all.php`](marketing-stats/all.php) |

### Templates & Domains

Expand Down
58 changes: 58 additions & 0 deletions examples/marketing-stats/all.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

use Mailtrap\Config;
use Mailtrap\DTO\Request\MarketingStats\MarketingStatsFilter;
use Mailtrap\DTO\Request\MarketingStats\MarketingStatsInterface;
use Mailtrap\Helper\ResponseHelper;
use Mailtrap\MailtrapGeneralClient;

require __DIR__ . '/../../vendor/autoload.php';

$config = new Config($_ENV['MAILTRAP_API_KEY']); // your API token from https://mailtrap.io/api-tokens
$marketingStats = (new MailtrapGeneralClient($config))->marketingStats();

// The last 30 days, today included (both dates are inclusive)
$today = new DateTimeImmutable();
$startDate = $today->modify('-29 days')->format('Y-m-d');
$endDate = $today->format('Y-m-d');

/**
* Get aggregated Email Marketing stats (campaigns and automations), wrapped in `data`.
* Delivery and bounce rates are relative to `sent_count`; open, click, spam and
* unsubscription rates to `delivery_count`.
*
* GET https://mailtrap.io/api/marketing/stats
*/
try {
$response = $marketingStats->getMarketingStats(
new MarketingStatsFilter(startDate: $startDate, endDate: $endDate)
);

// print the response body (array)
var_dump(ResponseHelper::toArray($response));
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), PHP_EOL;
}

/**
* Get Email Marketing stats with filters: campaigns only, sent to Google and Yahoo.
* Pass `sendingDomainIds` as well to narrow the stats to specific sending domains.
*
* GET https://mailtrap.io/api/marketing/stats
*/
try {
$response = $marketingStats->getMarketingStats(
new MarketingStatsFilter(
startDate: $startDate,
endDate: $endDate,
sources: [MarketingStatsInterface::SOURCE_CAMPAIGNS],
emailServiceProviders: ['Google', 'Yahoo'],
)
);

var_dump(ResponseHelper::toArray($response));
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), PHP_EOL;
}
34 changes: 34 additions & 0 deletions src/Api/General/MarketingStats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Mailtrap\Api\General;

use Mailtrap\Api\AbstractApi;
use Mailtrap\DTO\Request\MarketingStats\MarketingStatsFilter;
use Psr\Http\Message\ResponseInterface;

/**
* Class MarketingStats
*
* Aggregated Email Marketing stats (campaigns and automations). This resource is
* token-scoped: the account is resolved from the API token, so it takes no account id.
*/
class MarketingStats extends AbstractApi implements GeneralInterface
{
/**
* Get aggregated Email Marketing stats for a date range, wrapped in `data`: sent,
* delivery, bounce, open, click, spam and unsubscription counts and rates.
* Delivery and bounce rates are relative to sent_count; open, click, spam and
* unsubscription rates to delivery_count. Every count and rate is 0 when nothing matched.
*
* @param MarketingStatsFilter $filter
* @return ResponseInterface
*/
public function getMarketingStats(MarketingStatsFilter $filter): ResponseInterface
{
return $this->handleResponse(
$this->httpGet($this->getHost() . '/api/marketing/stats', $filter->toArray())
);
}
}
56 changes: 56 additions & 0 deletions src/DTO/Request/MarketingStats/MarketingStatsFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Mailtrap\DTO\Request\MarketingStats;

use Mailtrap\Exception\InvalidArgumentException;

/**
* Class MarketingStatsFilter
*
* Query filters for the aggregated Email Marketing stats. The date range is required;
* every other filter is optional and left out of the query when empty.
*/
final class MarketingStatsFilter implements MarketingStatsInterface
{
/**
* @param string $startDate Start of the date range (inclusive), `YYYY-MM-DD`
* @param string $endDate End of the date range (inclusive), `YYYY-MM-DD`
* @param int[] $sendingDomainIds Only traffic sent from these sending domains
* @param string[] $sources Subset of MarketingStatsInterface::SOURCES; one value narrows
* to that source, none or both mean all Email Marketing traffic
* @param string[] $emailServiceProviders Only traffic to these email service providers (e.g. "Google")
*/
public function __construct(
private string $startDate,
private string $endDate,
private array $sendingDomainIds = [],
private array $sources = [],
private array $emailServiceProviders = [],
) {
foreach ($sources as $source) {
if (!in_array($source, self::SOURCES, true)) {
throw new InvalidArgumentException(sprintf(
'"sources" must contain only "%s"; "%s" given',
implode('", "', self::SOURCES),
is_string($source) ? $source : get_debug_type($source)
));
}
}
}

public function toArray(): array
{
return array_filter(
[
'start_date' => $this->startDate,
'end_date' => $this->endDate,
'sending_domain_ids' => array_values($this->sendingDomainIds),
'sources' => array_values($this->sources),
'email_service_providers' => array_values($this->emailServiceProviders),
],
fn ($value) => $value !== []
);
}
}
19 changes: 19 additions & 0 deletions src/DTO/Request/MarketingStats/MarketingStatsInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Mailtrap\DTO\Request\MarketingStats;

use Mailtrap\DTO\Request\RequestInterface;

interface MarketingStatsInterface extends RequestInterface
{
// Email Marketing traffic sources (the `sources[]` filter)
public const SOURCE_CAMPAIGNS = 'campaigns';
public const SOURCE_AUTOMATIONS = 'automations';

public const SOURCES = [
self::SOURCE_CAMPAIGNS,
self::SOURCE_AUTOMATIONS,
];
}
22 changes: 12 additions & 10 deletions src/MailtrapGeneralClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
namespace Mailtrap;

/**
* @method Api\General\Account accounts()
* @method Api\General\User users(int $accountId)
* @method Api\General\Permission permissions(int $accountId)
* @method Api\General\Contact contacts(int $accountId)
* @method Api\General\EmailTemplate emailTemplates(int $accountId)
* @method Api\General\Template templates(int $accountId)
* @method Api\General\Billing billing(int $accountId)
* @method Api\General\ApiToken apiTokens(int $accountId)
* @method Api\General\Organization organization(int $organizationId)
* @method Api\General\EmailCampaign emailCampaigns(int $accountId)
* @method Api\General\Account accounts()
* @method Api\General\User users(int $accountId)
* @method Api\General\Permission permissions(int $accountId)
* @method Api\General\Contact contacts(int $accountId)
* @method Api\General\EmailTemplate emailTemplates(int $accountId)
* @method Api\General\Template templates(int $accountId)
* @method Api\General\Billing billing(int $accountId)
* @method Api\General\ApiToken apiTokens(int $accountId)
* @method Api\General\Organization organization(int $organizationId)
* @method Api\General\EmailCampaign emailCampaigns(int $accountId)
* @method Api\General\MarketingStats marketingStats()
*
* Class MailtrapGeneralClient
*/
Expand All @@ -31,5 +32,6 @@ final class MailtrapGeneralClient extends AbstractMailtrapClient
'apiTokens' => Api\General\ApiToken::class,
'organization' => Api\General\Organization::class,
'emailCampaigns' => Api\General\EmailCampaign::class,
'marketingStats' => Api\General\MarketingStats::class,
];
}
37 changes: 30 additions & 7 deletions tests/Api/AbstractApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,7 @@ public function testNormalizeArrayParamsUsesBracketNotationForMultipleValues():
'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);
$queryString = $this->normalizeArrayParams($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');
Expand All @@ -42,4 +36,33 @@ public function testNormalizeArrayParamsUsesBracketNotationForMultipleValues():
'Query must use filters[category][value][]=Cat1&filters[category][value][]=Cat2'
);
}

/**
* Ensures top-level list params (such as the marketing stats `sources[]`) are sent as
* key[]=a&key[]=b, whatever their numeric indices.
*/
public function testNormalizeArrayParamsUsesBracketNotationForListParams(): void
{
$params = [
'start_date' => '2026-05-01',
'sources' => ['campaigns', 'automations'],
'sending_domain_ids' => [3 => 10, 7 => 20],
];

$this->assertSame(
'start_date=2026-05-01&sources[]=campaigns&sources[]=automations'
. '&sending_domain_ids[]=10&sending_domain_ids[]=20',
urldecode($this->normalizeArrayParams($params))
);
}

private function normalizeArrayParams(array $params): string
{
$method = (new ReflectionClass(AbstractApi::class))->getMethod('normalizeArrayParams');
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}

return $method->invoke(new EmailLogs($this->getConfigMock(), self::FAKE_ACCOUNT_ID), $params);
}
}
Loading
Loading