Skip to content

Commit 5288218

Browse files
committed
Add support for Email Logs API
1 parent 0b8a145 commit 5288218

10 files changed

Lines changed: 700 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [Unreleased]
2+
3+
- Add support for Email Logs API
4+
15
## [4.5.0] - 2026-03-10
26

37
- Add StatsApi with get, byDomain, byCategory, byEmailServiceProvider, byDate endpoints

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ Email API:
244244

245245
- Sending domain management CRUD – [`sending-domains/everything.ts`](examples/sending-domains/everything.ts)
246246

247+
- Email logs (list with filters, get by message ID) – [`email-logs/everything.ts`](examples/email-logs/everything.ts)
248+
247249
Email Sandbox (Testing):
248250

249251
- Send an email (Sandbox) – [`testing/send-mail.ts`](examples/testing/send-mail.ts)

examples/email-logs/everything.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 emailLogsFlow() {
12+
try {
13+
// List email logs (paginated)
14+
const list = await client.emailLogs.getList();
15+
console.log("Email logs:", list.messages.length, "messages, total:", list.total_count);
16+
if (list.messages.length > 0) {
17+
console.log("First message:", list.messages[0].message_id, list.messages[0].subject);
18+
}
19+
20+
// List with filters (date range, category, status). Filter values can be single or array.
21+
const filtered = await client.emailLogs.getList({
22+
filters: {
23+
sent_after: "2025-01-01T00:00:00Z",
24+
sent_before: "2025-01-31T23:59:59Z",
25+
category: { operator: "equal", value: ["Welcome Email", "Forget Password"] },
26+
status: { operator: "equal", value: "delivered" },
27+
},
28+
});
29+
console.log("Filtered logs:", filtered.messages.length);
30+
31+
// Next page (use search_after from previous response next_page_cursor)
32+
if (list.next_page_cursor) {
33+
const nextPage = await client.emailLogs.getList({
34+
search_after: list.next_page_cursor,
35+
});
36+
console.log("Next page:", nextPage.messages.length, "messages");
37+
}
38+
39+
// Get a single message by ID
40+
if (list.messages.length > 0) {
41+
const messageId = list.messages[0].message_id;
42+
const message = await client.emailLogs.get(messageId);
43+
console.log("Single message:", message.subject, "events:", message.events?.length ?? 0);
44+
}
45+
} catch (error) {
46+
console.error("Error in emailLogsFlow:", error instanceof Error ? error.message : String(error));
47+
}
48+
}
49+
50+
emailLogsFlow();
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import axios from "axios";
2+
import AxiosMockAdapter from "axios-mock-adapter";
3+
4+
import EmailLogsApi from "../../../../lib/api/resources/EmailLogs";
5+
import handleSendingError from "../../../../lib/axios-logger";
6+
import MailtrapError from "../../../../lib/MailtrapError";
7+
import {
8+
EmailLogMessage,
9+
EmailLogsList,
10+
EmailLogMessageDetails,
11+
} from "../../../../types/api/email-logs";
12+
13+
import CONFIG from "../../../../config";
14+
15+
const { CLIENT_SETTINGS } = CONFIG;
16+
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;
17+
18+
describe("lib/api/resources/EmailLogs: ", () => {
19+
let mock: AxiosMockAdapter;
20+
const accountId = 100;
21+
const emailLogsAPI = new EmailLogsApi(axios, accountId);
22+
23+
const mockMessage: EmailLogMessage = {
24+
message_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
25+
status: "delivered",
26+
subject: "Welcome",
27+
from: "sender@example.com",
28+
to: "recipient@example.com",
29+
sent_at: "2025-01-15T10:30:00Z",
30+
client_ip: "203.0.113.42",
31+
category: "Welcome Email",
32+
custom_variables: {},
33+
sending_stream: "transactional",
34+
sending_domain_id: 3938,
35+
template_id: 100,
36+
template_variables: {},
37+
opens_count: 2,
38+
clicks_count: 1,
39+
};
40+
41+
const mockListResponse: EmailLogsList = {
42+
messages: [mockMessage],
43+
total_count: 1,
44+
next_page_cursor: null,
45+
};
46+
47+
const mockMessageDetails: EmailLogMessageDetails = {
48+
...mockMessage,
49+
raw_message_url: "https://storage.example.com/signed/eml/abc?token=...",
50+
events: [
51+
{
52+
event_type: "click",
53+
created_at: "2025-01-15T10:35:00Z",
54+
details: {
55+
click_url: "https://example.com/track/click/abc123",
56+
web_ip_address: "198.51.100.50",
57+
},
58+
},
59+
],
60+
};
61+
62+
describe("class EmailLogsApi(): ", () => {
63+
describe("init: ", () => {
64+
it("initializes with all necessary params.", () => {
65+
expect(emailLogsAPI).toHaveProperty("getList");
66+
expect(emailLogsAPI).toHaveProperty("get");
67+
});
68+
});
69+
});
70+
71+
beforeAll(() => {
72+
axios.interceptors.response.use(
73+
(response) => response.data,
74+
handleSendingError
75+
);
76+
mock = new AxiosMockAdapter(axios);
77+
});
78+
79+
afterEach(() => {
80+
mock.reset();
81+
});
82+
83+
describe("getList(): ", () => {
84+
it("successfully gets email logs with no params.", async () => {
85+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`;
86+
87+
expect.assertions(2);
88+
89+
mock.onGet(endpoint).reply(200, mockListResponse);
90+
const result = await emailLogsAPI.getList();
91+
92+
expect(mock.history.get[0].url).toEqual(endpoint);
93+
expect(result).toEqual(mockListResponse);
94+
});
95+
96+
it("successfully gets email logs with search_after.", async () => {
97+
const searchAfter = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
98+
const baseUrl = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`;
99+
const expectedUrl = `${baseUrl}?search_after=a1b2c3d4-e5f6-7890-abcd-ef1234567890`;
100+
101+
expect.assertions(2);
102+
103+
mock.onGet(expectedUrl).reply(200, mockListResponse);
104+
const result = await emailLogsAPI.getList({ search_after: searchAfter });
105+
106+
expect(mock.history.get[0].url).toEqual(expectedUrl);
107+
expect(result).toEqual(mockListResponse);
108+
});
109+
110+
it("successfully gets email logs with filters (deepObject style).", async () => {
111+
const sentAfter = "2025-01-01T00:00:00Z";
112+
const sentBefore = "2025-01-31T23:59:59Z";
113+
const baseUrl = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`;
114+
const expectedQuery =
115+
"filters[sent_after]=2025-01-01T00%3A00%3A00Z" +
116+
"&filters[sent_before]=2025-01-31T23%3A59%3A59Z" +
117+
"&filters[to][operator]=ci_equal" +
118+
"&filters[to][value]=recipient%40example.com";
119+
const expectedUrl = `${baseUrl}?${expectedQuery}`;
120+
121+
expect.assertions(2);
122+
123+
mock.onGet(expectedUrl).reply(200, mockListResponse);
124+
const result = await emailLogsAPI.getList({
125+
filters: {
126+
sent_after: sentAfter,
127+
sent_before: sentBefore,
128+
to: { operator: "ci_equal", value: "recipient@example.com" },
129+
},
130+
});
131+
132+
expect(mock.history.get[0].url).toEqual(expectedUrl);
133+
expect(result).toEqual(mockListResponse);
134+
});
135+
136+
it("successfully gets email logs with filter value as array (e.g. category).", async () => {
137+
const baseUrl = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`;
138+
const expectedQuery =
139+
"filters[category][operator]=equal" +
140+
"&filters[category][value]=Welcome%20Email" +
141+
"&filters[category][value]=Forget%20Password";
142+
const expectedUrl = `${baseUrl}?${expectedQuery}`;
143+
144+
expect.assertions(2);
145+
146+
mock.onGet(expectedUrl).reply(200, mockListResponse);
147+
const result = await emailLogsAPI.getList({
148+
filters: {
149+
category: {
150+
operator: "equal",
151+
value: ["Welcome Email", "Forget Password"],
152+
},
153+
},
154+
});
155+
156+
expect(mock.history.get[0].url).toEqual(expectedUrl);
157+
expect(result).toEqual(mockListResponse);
158+
});
159+
160+
it("fails with unauthorized error (401).", async () => {
161+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`;
162+
const expectedErrorMessage = "Incorrect API token";
163+
164+
expect.assertions(2);
165+
166+
mock.onGet(endpoint).reply(401, { error: expectedErrorMessage });
167+
168+
try {
169+
await emailLogsAPI.getList();
170+
} catch (error) {
171+
expect(error).toBeInstanceOf(MailtrapError);
172+
if (error instanceof MailtrapError) {
173+
expect(error.message).toEqual(expectedErrorMessage);
174+
}
175+
}
176+
});
177+
178+
it("fails with bad request (400).", async () => {
179+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`;
180+
const expectedErrorMessage = "Invalid request parameters";
181+
182+
expect.assertions(2);
183+
184+
mock.onGet(endpoint).reply(400, { errors: [expectedErrorMessage] });
185+
186+
try {
187+
await emailLogsAPI.getList();
188+
} catch (error) {
189+
expect(error).toBeInstanceOf(MailtrapError);
190+
if (error instanceof MailtrapError) {
191+
expect(error.message).toEqual(expectedErrorMessage);
192+
}
193+
}
194+
});
195+
196+
it("fails with rate limit exceeded (429).", async () => {
197+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`;
198+
const expectedErrorMessage = "Rate limit exceeded";
199+
200+
expect.assertions(2);
201+
202+
mock.onGet(endpoint).reply(429, { errors: [expectedErrorMessage] });
203+
204+
try {
205+
await emailLogsAPI.getList();
206+
} catch (error) {
207+
expect(error).toBeInstanceOf(MailtrapError);
208+
if (error instanceof MailtrapError) {
209+
expect(error.message).toEqual(expectedErrorMessage);
210+
}
211+
}
212+
});
213+
});
214+
215+
describe("get(): ", () => {
216+
const messageId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
217+
218+
it("successfully gets a single email log message by ID.", async () => {
219+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs/${messageId}`;
220+
221+
expect.assertions(4);
222+
223+
mock.onGet(endpoint).reply(200, mockMessageDetails);
224+
const result = await emailLogsAPI.get(messageId);
225+
226+
expect(mock.history.get[0].url).toEqual(endpoint);
227+
expect(result).toEqual(mockMessageDetails);
228+
expect(result.raw_message_url).toBeDefined();
229+
expect(result.events).toHaveLength(1);
230+
});
231+
232+
it("fails with not found (404).", async () => {
233+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs/${messageId}`;
234+
const expectedErrorMessage = "Resource not found";
235+
236+
expect.assertions(2);
237+
238+
mock.onGet(endpoint).reply(404, { error: expectedErrorMessage });
239+
240+
try {
241+
await emailLogsAPI.get(messageId);
242+
} catch (error) {
243+
expect(error).toBeInstanceOf(MailtrapError);
244+
if (error instanceof MailtrapError) {
245+
expect(error.message).toEqual(expectedErrorMessage);
246+
}
247+
}
248+
});
249+
250+
it("fails with rate limit exceeded (429).", async () => {
251+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs/${messageId}`;
252+
const expectedErrorMessage = "Rate limit exceeded";
253+
254+
expect.assertions(2);
255+
256+
mock.onGet(endpoint).reply(429, { errors: [expectedErrorMessage] });
257+
258+
try {
259+
await emailLogsAPI.get(messageId);
260+
} catch (error) {
261+
expect(error).toBeInstanceOf(MailtrapError);
262+
if (error instanceof MailtrapError) {
263+
expect(error.message).toEqual(expectedErrorMessage);
264+
}
265+
}
266+
});
267+
});
268+
});

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ContactExportsBaseAPI from "../../lib/api/ContactExports";
1515
import TemplatesBaseAPI from "../../lib/api/Templates";
1616
import SuppressionsBaseAPI from "../../lib/api/Suppressions";
1717
import SendingDomainsBaseAPI from "../../lib/api/SendingDomains";
18+
import EmailLogsBaseAPI from "../../lib/api/EmailLogs";
1819
import ContactEventsBaseAPI from "../../lib/api/ContactEvents";
1920

2021
const { ERRORS, CLIENT_SETTINGS } = CONFIG;
@@ -912,7 +913,7 @@ describe("lib/mailtrap-client: ", () => {
912913
});
913914

914915
expect(() => client.sendingDomains).toThrow(
915-
"accountId is missing, some features of testing API may not work properly."
916+
"accountId is missing, please provide a valid accountId."
916917
);
917918
});
918919

@@ -929,6 +930,32 @@ describe("lib/mailtrap-client: ", () => {
929930
});
930931
});
931932

933+
describe("get emailLogs(): ", () => {
934+
it("rejects with Mailtrap error, when `accountId` is missing.", () => {
935+
expect.assertions(1);
936+
937+
const client = new MailtrapClient({
938+
token: "test-token",
939+
});
940+
941+
expect(() => client.emailLogs).toThrow(
942+
"accountId is missing, please provide a valid accountId."
943+
);
944+
});
945+
946+
it("returns email logs API object when accountId is provided.", () => {
947+
expect.assertions(1);
948+
949+
const client = new MailtrapClient({
950+
token: "test-token",
951+
accountId: 123,
952+
});
953+
954+
const emailLogsClient = client.emailLogs;
955+
expect(emailLogsClient).toBeInstanceOf(EmailLogsBaseAPI);
956+
});
957+
});
958+
932959
describe("get contactEvents(): ", () => {
933960
it("rejects with Mailtrap error, when `accountId` is missing.", () => {
934961
const client = new MailtrapClient({

src/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default {
88
NO_DATA_ERROR: "No Data.",
99
TEST_INBOX_ID_MISSING: "testInboxId is missing, testing API will not work.",
1010
ACCOUNT_ID_MISSING:
11-
"accountId is missing, some features of testing API may not work properly.",
11+
"accountId is missing, please provide a valid accountId.",
1212
BULK_SANDBOX_INCOMPATIBLE: "Bulk mode is not applicable for sandbox API.",
1313
},
1414
CLIENT_SETTINGS: {

0 commit comments

Comments
 (0)