Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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.6.0] - 2025-07-15
- Add Contact Imports API functionality

## [3.5.0] - 2025-07-12
- Add Contact Fields API functionality

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Currently with this SDK you can:
- Fields CRUD
- Contacts CRUD
- Lists CRUD
- Import
- General
- Templates CRUD
- Suppressions management (find and delete)
Expand Down
49 changes: 49 additions & 0 deletions examples/general/contacts.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Mailtrap\Config;
use Mailtrap\DTO\Request\Contact\CreateContact;
use Mailtrap\DTO\Request\Contact\ImportContact;
use Mailtrap\DTO\Request\Contact\UpdateContact;
use Mailtrap\Helper\ResponseHelper;
use Mailtrap\MailtrapGeneralClient;
Expand Down Expand Up @@ -271,3 +272,51 @@
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), PHP_EOL;
}


/**
* Import contacts in bulk with support for custom fields and list management.
* Existing contacts with matching email addresses will be updated automatically.
* You can import up to 50,000 contacts per request.
* The import process runs asynchronously - use the returned import ID to check the status and results.
*
* POST https://mailtrap.io/api/accounts/{account_id}/contacts/imports
*/
try {
$contactsToImport = [
new ImportContact(
email: '[email protected]',
fields: ['first_name' => 'John', 'last_name' => 'Smith', 'zip_code' => 11111],
listIdsIncluded: [1, 2],
listIdsExcluded: [4, 5]
),
new ImportContact(
email: '[email protected]',
fields: ['first_name' => 'Joe', 'last_name' => 'Doe', 'zip_code' => 22222],
listIdsIncluded: [1],
listIdsExcluded: [4]
),
];

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


/**
* Get the status of a contact import by ID.
*
* GET https://mailtrap.io/api/accounts/{account_id}/contacts/imports/{import_id}
*/
try {
$importId = 1; // Replace 1 with the actual import ID
$response = $contacts->getContactImport($importId);

// print the response body (array)
var_dump(ResponseHelper::toArray($response));
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), PHP_EOL;
}
30 changes: 30 additions & 0 deletions src/Api/General/Contact.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Mailtrap\Api\AbstractApi;
use Mailtrap\ConfigInterface;
use Mailtrap\DTO\Request\Contact\CreateContact;
use Mailtrap\DTO\Request\Contact\ImportContact;
use Mailtrap\DTO\Request\Contact\UpdateContact;
use Psr\Http\Message\ResponseInterface;

Expand Down Expand Up @@ -246,6 +247,35 @@ public function deleteContactField(int $fieldId): ResponseInterface
);
}

/**
* Import contacts in bulk.
*
* @param ImportContact[] $contacts
* @return ResponseInterface
*/
public function importContacts(array $contacts): ResponseInterface
{
return $this->handleResponse(
$this->httpPost(
path: $this->getBasePath() . '/imports',
body: ['contacts' => array_map(fn(ImportContact $contact) => $contact->toArray(), $contacts)]
)
);
}

/**
* Get the status of a contact import by ID.
*
* @param int $importId
* @return ResponseInterface
*/
public function getContactImport(int $importId): ResponseInterface
{
return $this->handleResponse(
$this->httpGet($this->getBasePath() . '/imports/' . $importId)
);
}

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

declare(strict_types=1);

namespace Mailtrap\DTO\Request\Contact;

/**
* Class ImportContact
*/
final class ImportContact implements ContactInterface
{
public function __construct(
private string $email,
private array $fields = [],
private array $listIdsIncluded = [],
private array $listIdsExcluded = []
) {
}

public static function init(
string $email,
array $fields = [],
array $listIdsIncluded = [],
array $listIdsExcluded = []
): self {
return new self($email, $fields, $listIdsIncluded, $listIdsExcluded);
}

public function getEmail(): string
{
return $this->email;
}

public function getFields(): array
{
return $this->fields;
}

public function getListIdsIncluded(): array
{
return $this->listIdsIncluded;
}

public function getListIdsExcluded(): array
{
return $this->listIdsExcluded;
}

public function toArray(): array
{
return array_filter(
[
'email' => $this->getEmail(),
'fields' => $this->getFields(),
'list_ids_included' => $this->getListIdsIncluded(),
'list_ids_excluded' => $this->getListIdsExcluded(),
],
fn($value) => $value !== null
);
}
}
150 changes: 150 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\UpdateContact;
use Mailtrap\DTO\Request\Contact\ImportContact;
use Mailtrap\Exception\HttpClientException;
use Mailtrap\Tests\MailtrapTestCase;
use Nyholm\Psr7\Response;
Expand Down Expand Up @@ -476,6 +477,155 @@ public function testDeleteContactField(): void
$this->assertEquals(204, $response->getStatusCode());
}

public function testImportContacts(): void
{
$contacts = [
new ImportContact(
email: '[email protected]',
fields: ['first_name' => 'John', 'last_name' => 'Smith', 'zip_code' => 11111],
listIdsIncluded: [1, 2, 3],
listIdsExcluded: [4, 5, 6]
),
new ImportContact(
email: '[email protected]',
fields: ['first_name' => 'Joe', 'last_name' => 'Doe', 'zip_code' => 22222],
listIdsIncluded: [1],
listIdsExcluded: [4]
),
];

$expectedResponse = [
'id' => 1,
'status' => 'created',
];

$this->contact->expects($this->once())
->method('httpPost')
->with(
AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/imports',
[],
['contacts' => array_map(fn(ImportContact $contact) => $contact->toArray(), $contacts)]
)
->willReturn(new Response(201, ['Content-Type' => 'application/json'], json_encode($expectedResponse)));

$response = $this->contact->importContacts($contacts);
$responseData = ResponseHelper::toArray($response);

$this->assertInstanceOf(Response::class, $response);
$this->assertArrayHasKey('id', $responseData);
$this->assertEquals(1, $responseData['id']);
$this->assertEquals('created', $responseData['status']);
}

public function testGetContactImportInProgress(): void
{
$importId = 1;
$expectedResponse = [
'id' => $importId,
'status' => 'created',
];

$this->contact->expects($this->once())
->method('httpGet')
->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/imports/' . $importId)
->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse)));

$response = $this->contact->getContactImport($importId);
$responseData = ResponseHelper::toArray($response);

$this->assertInstanceOf(Response::class, $response);
$this->assertArrayHasKey('id', $responseData);
$this->assertEquals($importId, $responseData['id']);
$this->assertEquals('created', $responseData['status']);
}

public function testGetContactImportFinished(): void
{
$importId = 1;
$expectedResponse = [
'id' => $importId,
'status' => 'finished',
'created_contacts_count' => 2,
'updated_contacts_count' => 0,
'contacts_over_limit_count' => 0,
];

$this->contact->expects($this->once())
->method('httpGet')
->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/imports/' . $importId)
->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse)));

$response = $this->contact->getContactImport($importId);
$responseData = ResponseHelper::toArray($response);

$this->assertInstanceOf(Response::class, $response);
$this->assertArrayHasKey('id', $responseData);
$this->assertEquals($importId, $responseData['id']);
$this->assertEquals('finished', $responseData['status']);
$this->assertEquals(2, $responseData['created_contacts_count']);
$this->assertEquals(0, $responseData['updated_contacts_count']);
$this->assertEquals(0, $responseData['contacts_over_limit_count']);
}

public function testGetContactImportNotFound(): void
{
$importId = 999;

$this->contact->expects($this->once())
->method('httpGet')
->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/imports/' . $importId)
->willReturn(
new Response(404, ['Content-Type' => 'application/json'], json_encode(['error' => 'Not Found']))
);

$this->expectException(HttpClientException::class);
$this->expectExceptionMessage('Errors: Not Found.');

$this->contact->getContactImport($importId);
}

public function testImportContactsValidationError(): void
{
$contacts = [
new ImportContact(
email: 'invalid-email',
fields: ['first_name' => 'John'],
listIdsIncluded: [],
listIdsExcluded: []
),
];

$expectedResponse = [
'errors' => [
[
'email' => 'invalid-email',
'errors' => [
'email' => [
'is invalid',
'top level domain is too short',
],
],
],
],
];

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

$this->expectException(HttpClientException::class);
$this->expectExceptionMessage('Errors: email -> invalid-email. errors -> email -> is invalid. top level domain is too short.');

$this->contact->importContacts($contacts);
}

private function getExpectedContactFields(): array
{
return [
Expand Down