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
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ Currently, with this SDK you can:
- Inbox management
- Project management
- Contact management
- Contacts CRUD
- Lists CRUD
- Contact Fields CRUD
- Contact Imports (bulk import contacts)
- Contacts
- Contact Exports
- Contact Fields
- Contact Imports
- Contact Lists
- General
- Templates CRUD
- Suppressions management (find and delete)
- Templates
- Suppressions management
- Account access management
- Permissions management
- List accounts you have access to
Expand Down Expand Up @@ -127,6 +128,9 @@ Refer to the [`examples`](examples) folder for the source code of this and other

- [Contact Fields](examples/contact-fields/everything.ts)

### Contact Exports API

- [Contact Exports](examples/contact-exports/everything.ts)
### Contact Imports API

- [Contact Imports](examples/contact-imports/everything.ts)
Expand Down
44 changes: 44 additions & 0 deletions examples/contact-exports/everything.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MailtrapClient } from "mailtrap";

const TOKEN = "<YOUR-TOKEN-HERE>";
const ACCOUNT_ID = "<YOUR-ACCOUNT-ID-HERE>";

const client = new MailtrapClient({
token: TOKEN,
accountId: Number(ACCOUNT_ID),
});

async function createContactExport() {
try {
// Get contact lists and use first one if available
const lists = await client.contactLists.getList();
const listId = Array.isArray(lists) && lists.length > 0 ? lists[0].id : undefined;

// Create filters array per API docs:
// - Use list_id filter with array of list IDs if list available
// - Add subscription_status filter to export only subscribed contacts
const filters = listId
? [
{ name: "list_id", operator: "equal" as const, value: [listId] },
{ name: "subscription_status", operator: "equal" as const, value: "subscribed" },
]
: [
{ name: "subscription_status", operator: "equal" as const, value: "subscribed" },
];

const created = await client.contactExports.create({ filters });
console.log("Export created:", JSON.stringify(created, null, 2));

// Fetch export to check status and get download URL when finished
const fetched = await client.contactExports.get(created.id);
console.log("Export fetched:", JSON.stringify(fetched, null, 2));
} catch (error) {
console.error(
"Error creating contact export:",
error instanceof Error ? error.message : String(error)
);
}
}

createContactExport();

192 changes: 192 additions & 0 deletions src/__tests__/lib/api/resources/ContactExports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import axios from "axios";
import AxiosMockAdapter from "axios-mock-adapter";

import ContactExportsApi from "../../../../lib/api/resources/ContactExports";
import handleSendingError from "../../../../lib/axios-logger";
import MailtrapError from "../../../../lib/MailtrapError";

import CONFIG from "../../../../config";

const { CLIENT_SETTINGS } = CONFIG;
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;

describe("lib/api/resources/ContactExports: ", () => {
let mock: AxiosMockAdapter;
const accountId = 100;
const contactExportsAPI = new ContactExportsApi(axios, accountId);

const createContactExportRequest = {
filters: [
{ name: "list_id", operator: "equal" as const, value: [101, 102] },
{
name: "subscription_status",
operator: "equal" as const,
value: "subscribed",
},
],
};

const createContactExportResponse: {
id: number;
status: "started" | "created" | "finished";
created_at: string;
updated_at: string;
url: string | null;
} = {
id: 69,
status: "created",
created_at: "2025-11-01T06:29:00.848Z",
updated_at: "2025-11-01T06:29:00.848Z",
url: null,
};

const getContactExportResponse: {
id: number;
status: "started" | "created" | "finished";
created_at: string;
updated_at: string;
url: string | null;
} = {
id: 69,
status: "finished",
created_at: "2025-11-01T06:29:00.848Z",
updated_at: "2025-11-01T06:29:01.053Z",
url: "https://mailsend-us-mailtrap-tmp-uploads.s3.amazonaws.com/data_exports/export.csv.gz",
};

describe("class ContactExportsApi(): ", () => {
describe("init: ", () => {
it("initializes with all necessary params.", () => {
expect(contactExportsAPI).toHaveProperty("create");
expect(contactExportsAPI).toHaveProperty("get");
});
});
});

beforeAll(() => {
/**
* Init Axios interceptors for handling response.data, errors.
*/
axios.interceptors.response.use(
(response) => response.data,
handleSendingError
);
mock = new AxiosMockAdapter(axios);
});

afterEach(() => {
mock.reset();
});

describe("create(): ", () => {
it("successfully creates a contact export.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
const expectedResponseData = createContactExportResponse;

expect.assertions(2);

mock
.onPost(endpoint, createContactExportRequest)
.reply(200, expectedResponseData);
const result = await contactExportsAPI.create(createContactExportRequest);

expect(mock.history.post[0].url).toEqual(endpoint);
expect(result).toEqual(expectedResponseData);
});

it("fails with error when filters are invalid.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
const expectedErrorMessage = {
errors: {
filters: "invalid",
},
};

expect.assertions(2);

mock.onPost(endpoint).reply(422, expectedErrorMessage);

try {
await contactExportsAPI.create(createContactExportRequest);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
// axios logger returns "[object Object]" for error objects, so we check for that
expect(error.message).toBe("[object Object]");
}
}
});

it("fails with error when accountId is invalid.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
const expectedErrorMessage = "Account not found";

expect.assertions(2);

mock.onPost(endpoint).reply(404, { error: expectedErrorMessage });

try {
await contactExportsAPI.create(createContactExportRequest);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toContain(expectedErrorMessage);
}
}
});
});

describe("get(): ", () => {
const exportId = 69;

it("successfully gets a contact export by id.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${exportId}`;
const expectedResponseData = getContactExportResponse;

expect.assertions(2);

mock.onGet(endpoint).reply(200, expectedResponseData);
const result = await contactExportsAPI.get(exportId);

expect(mock.history.get[0].url).toEqual(endpoint);
expect(result).toEqual(expectedResponseData);
});

it("fails with error when export not found.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${exportId}`;
const expectedErrorMessage = "Export not found";

expect.assertions(2);

mock.onGet(endpoint).reply(404, { error: expectedErrorMessage });

try {
await contactExportsAPI.get(exportId);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toContain(expectedErrorMessage);
}
}
});

it("fails with error when exportId is invalid.", async () => {
const invalidExportId = 999;
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${invalidExportId}`;
const expectedErrorMessage = "Export not found";

expect.assertions(2);

mock.onGet(endpoint).reply(404, { error: expectedErrorMessage });

try {
await contactExportsAPI.get(invalidExportId);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toContain(expectedErrorMessage);
}
}
});
});
});
27 changes: 27 additions & 0 deletions src/__tests__/lib/mailtrap-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import GeneralAPI from "../../lib/api/General";
import TestingAPI from "../../lib/api/Testing";
import ContactLists from "../../lib/api/ContactLists";
import Contacts from "../../lib/api/Contacts";
import ContactExportsBaseAPI from "../../lib/api/ContactExports";
import TemplatesBaseAPI from "../../lib/api/Templates";
import SuppressionsBaseAPI from "../../lib/api/Suppressions";
import SendingDomainsBaseAPI from "../../lib/api/SendingDomains";
Expand Down Expand Up @@ -823,6 +824,32 @@ describe("lib/mailtrap-client: ", () => {
});
});

describe("get contactExports(): ", () => {
it("rejects with Mailtrap error, when `accountId` is missing.", () => {
const client = new MailtrapClient({
token: "MY_API_TOKEN",
});
expect.assertions(1);

try {
client.contactExports;
} catch (error) {
expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING));
}
});

it("returns contact exports API object when accountId is provided.", () => {
const client = new MailtrapClient({
token: "MY_API_TOKEN",
accountId: 10,
});
expect.assertions(1);

const contactExportsClient = client.contactExports;
expect(contactExportsClient).toBeInstanceOf(ContactExportsBaseAPI);
});
});

describe("get templates(): ", () => {
it("rejects with Mailtrap error, when `accountId` is missing.", () => {
const client = new MailtrapClient({
Expand Down
11 changes: 10 additions & 1 deletion src/lib/MailtrapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import handleSendingError from "./axios-logger";
import MailtrapError from "./MailtrapError";

import ContactsBaseAPI from "./api/Contacts";
import ContactExportsBaseAPI from "./api/ContactExports";
import ContactFieldsBaseAPI from "./api/ContactFields";
import ContactListsBaseAPI from "./api/ContactLists";
import ContactImportsBaseAPI from "./api/ContactImports";
import ContactListsBaseAPI from "./api/ContactLists";
import GeneralAPI from "./api/General";
import TemplatesBaseAPI from "./api/Templates";
import SuppressionsBaseAPI from "./api/Suppressions";
Expand Down Expand Up @@ -131,6 +132,14 @@ export default class MailtrapClient {
return new ContactsBaseAPI(this.axios, accountId);
}

/**
* Getter for Contact Exports API.
*/
get contactExports() {
const accountId = this.validateAccountIdPresence();
return new ContactExportsBaseAPI(this.axios, accountId);
}

/**
* Getter for Contact Lists API.
*/
Expand Down
15 changes: 15 additions & 0 deletions src/lib/api/ContactExports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AxiosInstance } from "axios";

import ContactExportsApi from "./resources/ContactExports";

export default class ContactExportsBaseAPI {
public create: ContactExportsApi["create"];

public get: ContactExportsApi["get"];

constructor(client: AxiosInstance, accountId: number) {
const contactExports = new ContactExportsApi(client, accountId);
this.create = contactExports.create.bind(contactExports);
this.get = contactExports.get.bind(contactExports);
}
}
43 changes: 43 additions & 0 deletions src/lib/api/resources/ContactExports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AxiosInstance } from "axios";

import CONFIG from "../../../config";

import {
ContactExportResponse,
CreateContactExportParams,
} from "../../../types/api/contact-exports";

const { CLIENT_SETTINGS } = CONFIG;
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;

export default class ContactExportsApi {
private client: AxiosInstance;

private contactExportsURL: string;

constructor(client: AxiosInstance, accountId: number) {
this.client = client;
this.contactExportsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
}

/**
* Get a contact export by `exportId`.
*/
public async get(exportId: number) {
const url = `${this.contactExportsURL}/${exportId}`;

return this.client.get<ContactExportResponse, ContactExportResponse>(url);
}

/**
* Export contacts.
*/
public async create(params: CreateContactExportParams) {
const url = `${this.contactExportsURL}`;

return this.client.post<ContactExportResponse, ContactExportResponse>(
url,
params
);
}
}
Loading