diff --git a/README.md b/README.md index e7936e2..0c5d113 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) diff --git a/examples/contact-exports/everything.ts b/examples/contact-exports/everything.ts new file mode 100644 index 0000000..8876821 --- /dev/null +++ b/examples/contact-exports/everything.ts @@ -0,0 +1,44 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +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(); + diff --git a/src/__tests__/lib/api/resources/ContactExports.test.ts b/src/__tests__/lib/api/resources/ContactExports.test.ts new file mode 100644 index 0000000..140b8c3 --- /dev/null +++ b/src/__tests__/lib/api/resources/ContactExports.test.ts @@ -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); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index c4123a4..8d67c18 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -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"; @@ -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({ diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index c562ece..39412f1 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -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"; @@ -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. */ diff --git a/src/lib/api/ContactExports.ts b/src/lib/api/ContactExports.ts new file mode 100644 index 0000000..2e41f21 --- /dev/null +++ b/src/lib/api/ContactExports.ts @@ -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); + } +} diff --git a/src/lib/api/resources/ContactExports.ts b/src/lib/api/resources/ContactExports.ts new file mode 100644 index 0000000..367b06a --- /dev/null +++ b/src/lib/api/resources/ContactExports.ts @@ -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(url); + } + + /** + * Export contacts. + */ + public async create(params: CreateContactExportParams) { + const url = `${this.contactExportsURL}`; + + return this.client.post( + url, + params + ); + } +} diff --git a/src/types/api/contact-exports.ts b/src/types/api/contact-exports.ts new file mode 100644 index 0000000..562d537 --- /dev/null +++ b/src/types/api/contact-exports.ts @@ -0,0 +1,23 @@ +export interface ContactExportResponse { + id: number; + status: "started" | "created" | "finished"; + created_at: string; + updated_at: string; + url: string | null; +} + +export interface CreateContactExportFilter { + name: string; // e.g. "list_id", "subscription_status", "email" + operator: + | "equal" + | "not_equal" + | "contains" + | "not_contains" + | "is_empty" + | "is_not_empty"; + value: string | number | boolean | string[] | number[]; +} + +export interface CreateContactExportParams { + filters: CreateContactExportFilter[]; +}