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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ Email Marketing:
General API:

- Templates CRUD – [`templates/everything.ts`](examples/templates/everything.ts)
- Suppressions (find & delete) – [`sending/suppressions.ts`](examples/sending/suppressions.ts)
- Suppressions (create, find & delete) – [`sending/suppressions.ts`](examples/sending/suppressions.ts)
- Tracking Opt-outs (list, create & delete) – [`sending/tracking-opt-outs.ts`](examples/sending/tracking-opt-outs.ts)
- Billing info – [`general/billing.ts`](examples/general/billing.ts)
- Accounts info – [`general/accounts.ts`](examples/general/accounts.ts)
- Permissions listing – [`general/permissions.ts`](examples/general/permissions.ts)
Expand Down
8 changes: 8 additions & 0 deletions examples/sending/suppressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ async function suppressionsFlow() {
const filteredSuppressions = await client.suppressions.getList({email: "test@example.com"});
console.log("Filtered suppressions:", filteredSuppressions);

// Add an email to the suppression list. `type` defaults to "manual import".
const created = await client.suppressions.create({
email: "suppressed@example.com",
domain_id: 12345,
sending_stream: "transactional"
});
console.log("Created suppression:", created.data);

// Delete a suppression by ID (if any exist)
if (suppressions.length > 0) {
const suppressionToDelete = suppressions[0];
Expand Down
43 changes: 43 additions & 0 deletions examples/sending/tracking-opt-outs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { MailtrapClient } from "mailtrap";

const TOKEN = "<YOUR-TOKEN-HERE>";
const DOMAIN_ID = Number("<YOUR-DOMAIN-ID-HERE>");

const client = new MailtrapClient({ token: TOKEN });

async function trackingOptOutsFlow() {
// Opt an email out of open and click tracking for a sending domain
const created = await client.trackingOptOuts.create({
email: "tracked@example.com",
domain_id: DOMAIN_ID
});
console.log("Created tracking opt-out:", created.data);

// Get tracking opt-outs (up to 1000 per request)
const page = await client.trackingOptOuts.getList();
console.log("Tracking opt-outs:", page.data, "next cursor:", page.last_id);

// Filter by email and creation time
const filtered = await client.trackingOptOuts.getList({
email: "tracked@example.com",
start_time: "2025-01-01T00:00:00Z",
end_time: "2025-12-31T23:59:59Z"
});
console.log("Filtered tracking opt-outs:", filtered.data);

// Page through the full list, following the cursor
const all = [...page.data];
let cursor = page.last_id;
while (cursor) {
const next = await client.trackingOptOuts.getList({ last_id: cursor });
all.push(...next.data);
cursor = next.last_id;
}
console.log(`Fetched ${all.length} tracking opt-outs in total`);

// Remove an email from the tracking opt-out list. Returns the deleted record.
const deleted = await client.trackingOptOuts.delete(created.data.id);
console.log("Deleted tracking opt-out:", deleted);
}

trackingOptOutsFlow().catch(console.error);
65 changes: 65 additions & 0 deletions src/__tests__/lib/api/resources/Suppressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ describe("lib/api/resources/Suppressions: ", () => {
message_category: "test",
message_client_ip: "192.168.1.1",
message_created_at: "2023-01-01T00:00:00Z",
message_esp_response: null,
message_esp_server_type: null,
message_outgoing_ip: "10.0.0.1",
message_recipient_mx_name: "mx.example.com",
message_sender_email: "sender@example.com",
Expand All @@ -45,6 +47,8 @@ describe("lib/api/resources/Suppressions: ", () => {
message_category: "test",
message_client_ip: "192.168.1.1",
message_created_at: "2023-01-01T00:00:00Z",
message_esp_response: null,
message_esp_server_type: null,
message_outgoing_ip: "10.0.0.1",
message_recipient_mx_name: "mx.example.com",
message_sender_email: "sender@example.com",
Expand All @@ -61,6 +65,8 @@ describe("lib/api/resources/Suppressions: ", () => {
message_category: "promotional",
message_client_ip: "192.168.1.2",
message_created_at: "2023-01-02T00:00:00Z",
message_esp_response: null,
message_esp_server_type: null,
message_outgoing_ip: "10.0.0.2",
message_recipient_mx_name: "mx.example.com",
message_sender_email: "sender@example.com",
Expand All @@ -72,6 +78,7 @@ describe("lib/api/resources/Suppressions: ", () => {
describe("init: ", () => {
it("initializes with all necessary params.", () => {
expect(suppressionsAPI).toHaveProperty("getList");
expect(suppressionsAPI).toHaveProperty("create");
expect(suppressionsAPI).toHaveProperty("delete");
});
});
Expand Down Expand Up @@ -153,6 +160,64 @@ describe("lib/api/resources/Suppressions: ", () => {
});
});

describe("create(): ", () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions`;

it("sends a flat body and returns the wrapped suppression.", async () => {
const params = {
email: "test@example.com",
domain_id: 12345,
sending_stream: "transactional" as const,
};
const expectedResponse = { data: mockSuppression };

expect.assertions(2);

mock.onPost(endpoint).reply(201, expectedResponse);
const result = await suppressionsAPI.create(params);

expect(JSON.parse(mock.history.post[0].data)).toEqual(params);
expect(result).toEqual(expectedResponse);
});

it("sends the optional type when provided.", async () => {
const params = {
email: "test@example.com",
domain_id: 12345,
sending_stream: "bulk" as const,
type: "spam complaint" as const,
};

expect.assertions(1);

mock.onPost(endpoint).reply(201, { data: mockSuppression });
await suppressionsAPI.create(params);

expect(JSON.parse(mock.history.post[0].data)).toEqual(params);
});

it("fails with unauthorized error (401).", async () => {
const expectedErrorMessage = "Incorrect API token";

expect.assertions(2);

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

try {
await suppressionsAPI.create({
email: "test@example.com",
domain_id: 12345,
sending_stream: "transactional",
});
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});
});

describe("delete(): ", () => {
const suppressionId = "1";

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

import TrackingOptOutsApi from "../../../../lib/api/resources/TrackingOptOuts";
import handleSendingError from "../../../../lib/axios-logger";
import MailtrapError from "../../../../lib/MailtrapError";
import { TrackingOptOut } from "../../../../types/api/tracking-opt-outs";

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

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

describe("lib/api/resources/TrackingOptOuts: ", () => {
let mock: AxiosMockAdapter;
const trackingOptOutsAPI = new TrackingOptOutsApi(axios);
const endpoint = `${GENERAL_ENDPOINT}/api/tracking_opt_outs`;

const mockTrackingOptOut: TrackingOptOut = {
id: "64d71bf3-1276-417b-86e1-8e66f138acfe",
email: "tracked@example.com",
created_at: "2025-01-15T10:30:00Z",
domain_name: "example.com",
};

beforeAll(() => {
axios.interceptors.response.use(
(response) => response.data,
handleSendingError
);
mock = new AxiosMockAdapter(axios);
});

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

describe("class TrackingOptOutsApi(): ", () => {
describe("init: ", () => {
it("initializes with all necessary params.", () => {
expect(trackingOptOutsAPI).toHaveProperty("getList");
expect(trackingOptOutsAPI).toHaveProperty("create");
expect(trackingOptOutsAPI).toHaveProperty("delete");
});
});
});

describe("getList(): ", () => {
it("returns the page and the cursor.", async () => {
const expectedResponse = {
data: [mockTrackingOptOut],
last_id: mockTrackingOptOut.id,
};

expect.assertions(2);

mock.onGet(endpoint).reply(200, expectedResponse);
const result = await trackingOptOutsAPI.getList();

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

it("returns a null cursor on the last page.", async () => {
expect.assertions(1);

mock.onGet(endpoint).reply(200, { data: [], last_id: null });
const result = await trackingOptOutsAPI.getList();

expect(result.last_id).toBeNull();
});

it("passes the filters as query params.", async () => {
const params = {
email: "tracked@example.com",
start_time: "2025-01-01T00:00:00Z",
end_time: "2025-12-31T23:59:59Z",
last_id: mockTrackingOptOut.id,
};

expect.assertions(1);

mock.onGet(endpoint).reply(200, { data: [], last_id: null });
await trackingOptOutsAPI.getList(params);

expect(mock.history.get[0].params).toEqual(params);
});

it("omits unset filters.", async () => {
expect.assertions(1);

mock.onGet(endpoint).reply(200, { data: [], last_id: null });
await trackingOptOutsAPI.getList({ email: "tracked@example.com" });

expect(mock.history.get[0].params).toEqual({
email: "tracked@example.com",
});
});

it("fails with unauthorized error (401).", async () => {
const expectedErrorMessage = "Incorrect API token";

expect.assertions(2);

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

try {
await trackingOptOutsAPI.getList();
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});
});

describe("create(): ", () => {
const params = { email: "tracked@example.com", domain_id: 12345 };

it("sends a flat body and returns the wrapped opt-out.", async () => {
const expectedResponse = { data: mockTrackingOptOut };

expect.assertions(2);

mock.onPost(endpoint).reply(201, expectedResponse);
const result = await trackingOptOutsAPI.create(params);

expect(JSON.parse(mock.history.post[0].data)).toEqual(params);
expect(result).toEqual(expectedResponse);
});

it("fails with forbidden error (403).", async () => {
const expectedErrorMessage = "Access forbidden";

expect.assertions(2);

mock.onPost(endpoint).reply(403, { errors: expectedErrorMessage });

try {
await trackingOptOutsAPI.create(params);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});
});

describe("delete(): ", () => {
const deleteEndpoint = `${endpoint}/${mockTrackingOptOut.id}`;

it("returns the deleted opt-out from the unwrapped response.", async () => {
expect.assertions(2);

mock.onDelete(deleteEndpoint).reply(200, mockTrackingOptOut);
const result = await trackingOptOutsAPI.delete(mockTrackingOptOut.id);

expect(mock.history.delete[0].url).toEqual(deleteEndpoint);
expect(result).toEqual(mockTrackingOptOut);
});

it("fails with not found error (404).", async () => {
const expectedErrorMessage = "Tracking opt-out not found";

expect.assertions(2);

mock
.onDelete(deleteEndpoint)
.reply(404, { errors: expectedErrorMessage });

try {
await trackingOptOutsAPI.delete(mockTrackingOptOut.id);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});
});
});
8 changes: 8 additions & 0 deletions src/lib/MailtrapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import InboundAPI from "./api/Inbound";
import SendingDomainsBaseAPI from "./api/SendingDomains";
import StatsBaseAPI from "./api/Stats";
import SuppressionsBaseAPI from "./api/Suppressions";
import TrackingOptOutsBaseAPI from "./api/TrackingOptOuts";
import OrganizationsBaseAPI from "./api/Organizations";
import TemplatesBaseAPI from "./api/Templates";
import TestingAPI from "./api/Testing";
Expand Down Expand Up @@ -260,6 +261,13 @@ export default class MailtrapClient {
return new EmailCampaignsBaseAPI(this.axios);
}

/**
* Getter for Tracking Opt-outs API.
*/
get trackingOptOuts() {
return new TrackingOptOutsBaseAPI(this.axios);
}

/**
* Getter for Company Info API.
*/
Expand Down
3 changes: 3 additions & 0 deletions src/lib/api/Suppressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import SuppressionsApi from "./resources/Suppressions";
export default class SuppressionsBaseAPI {
public getList: SuppressionsApi["getList"];

public create: SuppressionsApi["create"];

public delete: SuppressionsApi["delete"];

constructor(client: AxiosInstance, accountId: number) {
const suppressions = new SuppressionsApi(client, accountId);
this.getList = suppressions.getList.bind(suppressions);
this.create = suppressions.create.bind(suppressions);
this.delete = suppressions.delete.bind(suppressions);
}
}
Loading
Loading