Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [3.9.0] - 2025-10-14
- Add Contact Export functionality

## [3.8.0] - 2025-09-22
- Add Create Contact event API functionality

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Currently with this SDK you can:
- Fields CRUD
- Contacts CRUD
- Lists CRUD
- Import
- Import/Export
- Events
- General
- Templates CRUD
Expand Down
38 changes: 38 additions & 0 deletions examples/general/contacts.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Mailtrap\DTO\Request\Contact\UpdateContact;
use Mailtrap\Helper\ResponseHelper;
use Mailtrap\MailtrapGeneralClient;
use Mailtrap\DTO\Request\Contact\ContactExportFilter;

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

Expand Down Expand Up @@ -348,3 +349,40 @@
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), PHP_EOL;
}

/**
* Create a new Contact Export (asynchronous task)
*
* POST https://mailtrap.io/api/accounts/{account_id}/contacts/exports
*/
try {
$filters = [
// Export contacts that belong to lists 1 or 2
ContactExportFilter::init('list_id', 'equal', [1, 2]),
// Only subscribed contacts
ContactExportFilter::init('subscription_status', 'equal', 'subscribed'),
];

$response = $contacts->createContactExport($filters);

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

/**
* Get Contact Export status / download URL
* (Poll this endpoint until status becomes `finished` and `url` is not null)
*
* GET https://mailtrap.io/api/accounts/{account_id}/contacts/exports/{export_id}
*/
try {
$exportId = 1; // Replace 1 with the actual export ID obtained from createContactExport
$response = $contacts->getContactExport($exportId);

// print the response body (array)
var_dump(ResponseHelper::toArray($response));
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), PHP_EOL;
}
45 changes: 45 additions & 0 deletions src/Api/General/Contact.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Mailtrap\ConfigInterface;
use Mailtrap\DTO\Request\Contact\CreateContact;
use Mailtrap\DTO\Request\Contact\CreateContactEvent;
use Mailtrap\DTO\Request\Contact\ContactExportFilter;
use Mailtrap\DTO\Request\Contact\ImportContact;
use Mailtrap\DTO\Request\Contact\UpdateContact;
use Mailtrap\Exception\InvalidArgumentException;
Expand Down Expand Up @@ -308,6 +309,50 @@ public function createContactEvent(string $contactIdentifier, CreateContactEvent
);
}

/**
* Create a new Contact Export.
*
* POST https://mailtrap.io/api/accounts/{account_id}/contacts/exports
*
* @param ContactExportFilter[] $filters
* @return ResponseInterface
*/
public function createContactExport(array $filters = []): ResponseInterface
{
return $this->handleResponse(
$this->httpPost(
path: $this->getBasePath() . '/exports',
body: [
'filters' => array_map(
function ($filter): array {
if (!$filter instanceof ContactExportFilter) {
throw new InvalidArgumentException('Each filter must be an instance of ContactExportFilter.');
}

return $filter->toArray();
},
$filters
)
]
)
);
}

/**
* Get Contact Export status/info by ID.
*
* GET https://mailtrap.io/api/accounts/{account_id}/contacts/exports/{export_id}
*
* @param int $exportId
* @return ResponseInterface
*/
public function getContactExport(int $exportId): ResponseInterface
{
return $this->handleResponse(
$this->httpGet($this->getBasePath() . '/exports/' . $exportId)
);
}

public function getAccountId(): int
{
return $this->accountId;
Expand Down
50 changes: 50 additions & 0 deletions src/DTO/Request/Contact/ContactExportFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Mailtrap\DTO\Request\Contact;

use Mailtrap\DTO\Request\RequestInterface;

/**
* Represents a single filter for Contact Export.
*/
final class ContactExportFilter implements RequestInterface
{
public function __construct(
private string $name,
private string $operator,
private mixed $value
) {
}

public static function init(string $name, string $operator, mixed $value): self
{
return new self($name, $operator, $value);
}

public function getName(): string
{
return $this->name;
}

public function getOperator(): string
{
return $this->operator;
}

public function getValue(): mixed
{
return $this->value;
}

public function toArray(): array
{
return [
'name' => $this->getName(),
'operator' => $this->getOperator(),
'value' => $this->getValue(),
];
}
}

134 changes: 134 additions & 0 deletions tests/Api/General/ContactTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Mailtrap\Api\General\Contact;
use Mailtrap\DTO\Request\Contact\CreateContact;
use Mailtrap\DTO\Request\Contact\CreateContactEvent;
use Mailtrap\DTO\Request\Contact\ContactExportFilter;
use Mailtrap\DTO\Request\Contact\UpdateContact;
use Mailtrap\DTO\Request\Contact\ImportContact;
use Mailtrap\Exception\HttpClientException;
Expand Down Expand Up @@ -925,6 +926,139 @@ public function testCreateContactEventRateLimitExceeded(): void
$this->contact->createContactEvent($contactIdentifier, $eventData);
}

/**
* =============================
* Contact Exports
* =============================
*/
public function testCreateContactExport(): void
{
$filters = [
new ContactExportFilter('list_id', 'equal', [101, 102]),
new ContactExportFilter('subscription_status', 'equal', 'subscribed'),
];
$expectedResponse = [
'id' => 1,
'status' => 'started',
'created_at' => '2025-01-01T00:00:00Z',
'updated_at' => '2025-05-01T00:00:00Z',
'url' => null,
];
$this->contact->expects($this->once())
->method('httpPost')
->with(
AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/exports',
[],
['filters' => array_map(fn(ContactExportFilter $f) => $f->toArray(), $filters)]
)
->willReturn(new Response(201, ['Content-Type' => 'application/json'], json_encode($expectedResponse)));
$response = $this->contact->createContactExport($filters);
$responseData = ResponseHelper::toArray($response);
$this->assertArrayHasKey('id', $responseData);
}

public function testCreateContactExportUnauthorized(): void
{
$this->contact->expects($this->once())
->method('httpPost')
->with(
AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/exports',
[],
['filters' => []]
)
->willReturn(new Response(401, ['Content-Type' => 'application/json'], json_encode(['error' => 'Incorrect API token'])));
$this->expectException(HttpClientException::class);
$this->expectExceptionMessage('Errors: Incorrect API token.');
$this->contact->createContactExport();
}

public function testCreateContactExportForbidden(): void
{
$this->contact->expects($this->once())
->method('httpPost')
->with(
AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/exports',
[],
['filters' => []]
)
->willReturn(new Response(403, ['Content-Type' => 'application/json'], json_encode(['errors' => 'Access forbidden'])));
$this->expectException(HttpClientException::class);
$this->expectExceptionMessage('Errors: Access forbidden.');
$this->contact->createContactExport();
}

public function testCreateContactExportValidationError(): void
{
$filters = [new ContactExportFilter('list_id', 'equal', [1])];
$errors = [
'errors' => [
'filters' => 'invalid',
'base' => [
'There is a previous export initiated. You will be notified by email once it is completed.'
],
],
];

$this->contact->expects($this->once())
->method('httpPost')
->with(
AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/exports',
[],
['filters' => array_map(fn(ContactExportFilter $f) => $f->toArray(), $filters)]
)
->willReturn(new Response(422, ['Content-Type' => 'application/json'], json_encode($errors)));

$this->expectException(HttpClientException::class);
$this->expectExceptionMessage('Errors: filters -> invalid. base -> There is a previous export initiated. You will be notified by email once it is completed.');

$this->contact->createContactExport($filters);
}

public function testCreateContactExportRateLimitExceeded(): void
{
$this->contact->expects($this->once())
->method('httpPost')
->with(
AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/exports',
[],
['filters' => []]
)
->willReturn(new Response(429, ['Content-Type' => 'application/json'], json_encode(['errors' => 'Rate limit exceeded'])));
$this->expectException(HttpClientException::class);
$this->expectExceptionMessage('Errors: Rate limit exceeded.');
$this->contact->createContactExport();
}

public function testGetContactExport(): void
{
$exportId = 1;
$expectedResponse = [
'id' => $exportId,
'status' => 'started',
'created_at' => '2021-01-01T00:00:00Z',
'updated_at' => '2021-01-01T00:00:00Z',
'url' => null,
];
$this->contact->expects($this->once())
->method('httpGet')
->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/exports/' . $exportId)
->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse)));
$response = $this->contact->getContactExport($exportId);
$responseData = ResponseHelper::toArray($response);
$this->assertArrayHasKey('id', $responseData);
}

public function testGetContactExportNotFound(): void
{
$exportId = 9999;
$this->contact->expects($this->once())
->method('httpGet')
->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/exports/' . $exportId)
->willReturn(new Response(404, ['Content-Type' => 'application/json'], json_encode(['error' => 'Not Found'])));
$this->expectException(HttpClientException::class);
$this->expectExceptionMessage('Errors: Not Found.');
$this->contact->getContactExport($exportId);
}
private function getExpectedContactFields(): array
{
return [
Expand Down