Skip to content

Commit 626e994

Browse files
Merge pull request #98 from mailtrap/contact-exports-api
Contact exports
2 parents e4dded0 + 3e90cfa commit 626e994

File tree

8 files changed

+364
-7
lines changed

8 files changed

+364
-7
lines changed

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ Currently, with this SDK you can:
3030
- Inbox management
3131
- Project management
3232
- Contact management
33-
- Contacts CRUD
34-
- Lists CRUD
35-
- Contact Fields CRUD
36-
- Contact Imports (bulk import contacts)
33+
- Contacts
34+
- Contact Exports
35+
- Contact Fields
36+
- Contact Imports
37+
- Contact Lists
3738
- General
38-
- Templates CRUD
39-
- Suppressions management (find and delete)
39+
- Templates
40+
- Suppressions management
4041
- Account access management
4142
- Permissions management
4243
- List accounts you have access to
@@ -127,6 +128,9 @@ Refer to the [`examples`](examples) folder for the source code of this and other
127128

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

131+
### Contact Exports API
132+
133+
- [Contact Exports](examples/contact-exports/everything.ts)
130134
### Contact Imports API
131135

132136
- [Contact Imports](examples/contact-imports/everything.ts)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { MailtrapClient } from "mailtrap";
2+
3+
const TOKEN = "<YOUR-TOKEN-HERE>";
4+
const ACCOUNT_ID = "<YOUR-ACCOUNT-ID-HERE>";
5+
6+
const client = new MailtrapClient({
7+
token: TOKEN,
8+
accountId: Number(ACCOUNT_ID),
9+
});
10+
11+
async function createContactExport() {
12+
try {
13+
// Get contact lists and use first one if available
14+
const lists = await client.contactLists.getList();
15+
const listId = Array.isArray(lists) && lists.length > 0 ? lists[0].id : undefined;
16+
17+
// Create filters array per API docs:
18+
// - Use list_id filter with array of list IDs if list available
19+
// - Add subscription_status filter to export only subscribed contacts
20+
const filters = listId
21+
? [
22+
{ name: "list_id", operator: "equal" as const, value: [listId] },
23+
{ name: "subscription_status", operator: "equal" as const, value: "subscribed" },
24+
]
25+
: [
26+
{ name: "subscription_status", operator: "equal" as const, value: "subscribed" },
27+
];
28+
29+
const created = await client.contactExports.create({ filters });
30+
console.log("Export created:", JSON.stringify(created, null, 2));
31+
32+
// Fetch export to check status and get download URL when finished
33+
const fetched = await client.contactExports.get(created.id);
34+
console.log("Export fetched:", JSON.stringify(fetched, null, 2));
35+
} catch (error) {
36+
console.error(
37+
"Error creating contact export:",
38+
error instanceof Error ? error.message : String(error)
39+
);
40+
}
41+
}
42+
43+
createContactExport();
44+
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import axios from "axios";
2+
import AxiosMockAdapter from "axios-mock-adapter";
3+
4+
import ContactExportsApi from "../../../../lib/api/resources/ContactExports";
5+
import handleSendingError from "../../../../lib/axios-logger";
6+
import MailtrapError from "../../../../lib/MailtrapError";
7+
8+
import CONFIG from "../../../../config";
9+
10+
const { CLIENT_SETTINGS } = CONFIG;
11+
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;
12+
13+
describe("lib/api/resources/ContactExports: ", () => {
14+
let mock: AxiosMockAdapter;
15+
const accountId = 100;
16+
const contactExportsAPI = new ContactExportsApi(axios, accountId);
17+
18+
const createContactExportRequest = {
19+
filters: [
20+
{ name: "list_id", operator: "equal" as const, value: [101, 102] },
21+
{
22+
name: "subscription_status",
23+
operator: "equal" as const,
24+
value: "subscribed",
25+
},
26+
],
27+
};
28+
29+
const createContactExportResponse: {
30+
id: number;
31+
status: "started" | "created" | "finished";
32+
created_at: string;
33+
updated_at: string;
34+
url: string | null;
35+
} = {
36+
id: 69,
37+
status: "created",
38+
created_at: "2025-11-01T06:29:00.848Z",
39+
updated_at: "2025-11-01T06:29:00.848Z",
40+
url: null,
41+
};
42+
43+
const getContactExportResponse: {
44+
id: number;
45+
status: "started" | "created" | "finished";
46+
created_at: string;
47+
updated_at: string;
48+
url: string | null;
49+
} = {
50+
id: 69,
51+
status: "finished",
52+
created_at: "2025-11-01T06:29:00.848Z",
53+
updated_at: "2025-11-01T06:29:01.053Z",
54+
url: "https://mailsend-us-mailtrap-tmp-uploads.s3.amazonaws.com/data_exports/export.csv.gz",
55+
};
56+
57+
describe("class ContactExportsApi(): ", () => {
58+
describe("init: ", () => {
59+
it("initializes with all necessary params.", () => {
60+
expect(contactExportsAPI).toHaveProperty("create");
61+
expect(contactExportsAPI).toHaveProperty("get");
62+
});
63+
});
64+
});
65+
66+
beforeAll(() => {
67+
/**
68+
* Init Axios interceptors for handling response.data, errors.
69+
*/
70+
axios.interceptors.response.use(
71+
(response) => response.data,
72+
handleSendingError
73+
);
74+
mock = new AxiosMockAdapter(axios);
75+
});
76+
77+
afterEach(() => {
78+
mock.reset();
79+
});
80+
81+
describe("create(): ", () => {
82+
it("successfully creates a contact export.", async () => {
83+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
84+
const expectedResponseData = createContactExportResponse;
85+
86+
expect.assertions(2);
87+
88+
mock
89+
.onPost(endpoint, createContactExportRequest)
90+
.reply(200, expectedResponseData);
91+
const result = await contactExportsAPI.create(createContactExportRequest);
92+
93+
expect(mock.history.post[0].url).toEqual(endpoint);
94+
expect(result).toEqual(expectedResponseData);
95+
});
96+
97+
it("fails with error when filters are invalid.", async () => {
98+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
99+
const expectedErrorMessage = {
100+
errors: {
101+
filters: "invalid",
102+
},
103+
};
104+
105+
expect.assertions(2);
106+
107+
mock.onPost(endpoint).reply(422, expectedErrorMessage);
108+
109+
try {
110+
await contactExportsAPI.create(createContactExportRequest);
111+
} catch (error) {
112+
expect(error).toBeInstanceOf(MailtrapError);
113+
if (error instanceof MailtrapError) {
114+
// axios logger returns "[object Object]" for error objects, so we check for that
115+
expect(error.message).toBe("[object Object]");
116+
}
117+
}
118+
});
119+
120+
it("fails with error when accountId is invalid.", async () => {
121+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
122+
const expectedErrorMessage = "Account not found";
123+
124+
expect.assertions(2);
125+
126+
mock.onPost(endpoint).reply(404, { error: expectedErrorMessage });
127+
128+
try {
129+
await contactExportsAPI.create(createContactExportRequest);
130+
} catch (error) {
131+
expect(error).toBeInstanceOf(MailtrapError);
132+
if (error instanceof MailtrapError) {
133+
expect(error.message).toContain(expectedErrorMessage);
134+
}
135+
}
136+
});
137+
});
138+
139+
describe("get(): ", () => {
140+
const exportId = 69;
141+
142+
it("successfully gets a contact export by id.", async () => {
143+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${exportId}`;
144+
const expectedResponseData = getContactExportResponse;
145+
146+
expect.assertions(2);
147+
148+
mock.onGet(endpoint).reply(200, expectedResponseData);
149+
const result = await contactExportsAPI.get(exportId);
150+
151+
expect(mock.history.get[0].url).toEqual(endpoint);
152+
expect(result).toEqual(expectedResponseData);
153+
});
154+
155+
it("fails with error when export not found.", async () => {
156+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${exportId}`;
157+
const expectedErrorMessage = "Export not found";
158+
159+
expect.assertions(2);
160+
161+
mock.onGet(endpoint).reply(404, { error: expectedErrorMessage });
162+
163+
try {
164+
await contactExportsAPI.get(exportId);
165+
} catch (error) {
166+
expect(error).toBeInstanceOf(MailtrapError);
167+
if (error instanceof MailtrapError) {
168+
expect(error.message).toContain(expectedErrorMessage);
169+
}
170+
}
171+
});
172+
173+
it("fails with error when exportId is invalid.", async () => {
174+
const invalidExportId = 999;
175+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${invalidExportId}`;
176+
const expectedErrorMessage = "Export not found";
177+
178+
expect.assertions(2);
179+
180+
mock.onGet(endpoint).reply(404, { error: expectedErrorMessage });
181+
182+
try {
183+
await contactExportsAPI.get(invalidExportId);
184+
} catch (error) {
185+
expect(error).toBeInstanceOf(MailtrapError);
186+
if (error instanceof MailtrapError) {
187+
expect(error.message).toContain(expectedErrorMessage);
188+
}
189+
}
190+
});
191+
});
192+
});

src/__tests__/lib/mailtrap-client.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import GeneralAPI from "../../lib/api/General";
1111
import TestingAPI from "../../lib/api/Testing";
1212
import ContactLists from "../../lib/api/ContactLists";
1313
import Contacts from "../../lib/api/Contacts";
14+
import ContactExportsBaseAPI from "../../lib/api/ContactExports";
1415
import TemplatesBaseAPI from "../../lib/api/Templates";
1516
import SuppressionsBaseAPI from "../../lib/api/Suppressions";
1617
import SendingDomainsBaseAPI from "../../lib/api/SendingDomains";
@@ -823,6 +824,32 @@ describe("lib/mailtrap-client: ", () => {
823824
});
824825
});
825826

827+
describe("get contactExports(): ", () => {
828+
it("rejects with Mailtrap error, when `accountId` is missing.", () => {
829+
const client = new MailtrapClient({
830+
token: "MY_API_TOKEN",
831+
});
832+
expect.assertions(1);
833+
834+
try {
835+
client.contactExports;
836+
} catch (error) {
837+
expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING));
838+
}
839+
});
840+
841+
it("returns contact exports API object when accountId is provided.", () => {
842+
const client = new MailtrapClient({
843+
token: "MY_API_TOKEN",
844+
accountId: 10,
845+
});
846+
expect.assertions(1);
847+
848+
const contactExportsClient = client.contactExports;
849+
expect(contactExportsClient).toBeInstanceOf(ContactExportsBaseAPI);
850+
});
851+
});
852+
826853
describe("get templates(): ", () => {
827854
it("rejects with Mailtrap error, when `accountId` is missing.", () => {
828855
const client = new MailtrapClient({

src/lib/MailtrapClient.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import handleSendingError from "./axios-logger";
88
import MailtrapError from "./MailtrapError";
99

1010
import ContactsBaseAPI from "./api/Contacts";
11+
import ContactExportsBaseAPI from "./api/ContactExports";
1112
import ContactFieldsBaseAPI from "./api/ContactFields";
12-
import ContactListsBaseAPI from "./api/ContactLists";
1313
import ContactImportsBaseAPI from "./api/ContactImports";
14+
import ContactListsBaseAPI from "./api/ContactLists";
1415
import GeneralAPI from "./api/General";
1516
import TemplatesBaseAPI from "./api/Templates";
1617
import SuppressionsBaseAPI from "./api/Suppressions";
@@ -131,6 +132,14 @@ export default class MailtrapClient {
131132
return new ContactsBaseAPI(this.axios, accountId);
132133
}
133134

135+
/**
136+
* Getter for Contact Exports API.
137+
*/
138+
get contactExports() {
139+
const accountId = this.validateAccountIdPresence();
140+
return new ContactExportsBaseAPI(this.axios, accountId);
141+
}
142+
134143
/**
135144
* Getter for Contact Lists API.
136145
*/

src/lib/api/ContactExports.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AxiosInstance } from "axios";
2+
3+
import ContactExportsApi from "./resources/ContactExports";
4+
5+
export default class ContactExportsBaseAPI {
6+
public create: ContactExportsApi["create"];
7+
8+
public get: ContactExportsApi["get"];
9+
10+
constructor(client: AxiosInstance, accountId: number) {
11+
const contactExports = new ContactExportsApi(client, accountId);
12+
this.create = contactExports.create.bind(contactExports);
13+
this.get = contactExports.get.bind(contactExports);
14+
}
15+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { AxiosInstance } from "axios";
2+
3+
import CONFIG from "../../../config";
4+
5+
import {
6+
ContactExportResponse,
7+
CreateContactExportParams,
8+
} from "../../../types/api/contact-exports";
9+
10+
const { CLIENT_SETTINGS } = CONFIG;
11+
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;
12+
13+
export default class ContactExportsApi {
14+
private client: AxiosInstance;
15+
16+
private contactExportsURL: string;
17+
18+
constructor(client: AxiosInstance, accountId: number) {
19+
this.client = client;
20+
this.contactExportsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`;
21+
}
22+
23+
/**
24+
* Get a contact export by `exportId`.
25+
*/
26+
public async get(exportId: number) {
27+
const url = `${this.contactExportsURL}/${exportId}`;
28+
29+
return this.client.get<ContactExportResponse, ContactExportResponse>(url);
30+
}
31+
32+
/**
33+
* Export contacts.
34+
*/
35+
public async create(params: CreateContactExportParams) {
36+
const url = `${this.contactExportsURL}`;
37+
38+
return this.client.post<ContactExportResponse, ContactExportResponse>(
39+
url,
40+
params
41+
);
42+
}
43+
}

0 commit comments

Comments
 (0)