Skip to content

Suppressions api #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
66ef0e4
docs: add Suppressions example to README for enhanced API usage clarity
narekhovhannisyan Jun 21, 2025
3171823
examples: add suppressions example to demonstrate MailtrapClient func…
narekhovhannisyan Jun 21, 2025
4c8580b
test: add unit tests for MailtrapClient suppressions getter to valida…
narekhovhannisyan Jun 21, 2025
f0e3907
test: add unit tests for SuppressionsBaseAPI to verify initialization…
narekhovhannisyan Jun 21, 2025
5f88ec2
test: add comprehensive unit tests for SuppressionsApi to validate ge…
narekhovhannisyan Jun 21, 2025
4912a5b
feat: add suppressions getter to MailtrapClient for managing suppress…
narekhovhannisyan Jun 21, 2025
f576198
feat: implement SuppressionsBaseAPI for managing suppressions with Ax…
narekhovhannisyan Jun 21, 2025
43a42bd
feat: create SuppressionsApi class for managing suppressions with Axi…
narekhovhannisyan Jun 21, 2025
1520fdb
feat: add Suppression type definition for managing suppression data s…
narekhovhannisyan Jun 21, 2025
8e47f33
Merge branch 'template-api-integration' of github.com:railsware/mailt…
narekhovhannisyan Jun 29, 2025
13b8ad9
test: rename supression test to supressions
narekhovhannisyan Jul 1, 2025
a2c3e6e
refactor: update getList method in SuppressionsApi to accept ListOpti…
narekhovhannisyan Jul 1, 2025
a8ec09e
feat: add ListOptions type to enhance flexibility in suppression queries
narekhovhannisyan Jul 1, 2025
bdc8e9e
Merge branch 'main' of github.com:railsware/mailtrap-nodejs into supr…
narekhovhannisyan Jul 2, 2025
88bff56
test: update Suppressions API test to pass email as an object
narekhovhannisyan Jul 2, 2025
70541b5
refactor: update suppressions flow to improve clarity and use object …
narekhovhannisyan Jul 3, 2025
8c28b49
test: enhance Suppressions API tests to specify error types and impro…
narekhovhannisyan Jul 3, 2025
9312aa2
refactor: streamline suppressions getter by introducing account ID va…
narekhovhannisyan Jul 4, 2025
3ba3365
fix: correct spelling of "initializes" in multiple test files
narekhovhannisyan Jul 4, 2025
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Refer to the [`examples`](examples) folder for the source code of this and other
- [Advanced](examples/sending/everything.ts)
- [Minimal](examples/sending/minimal.ts)
- [Send email using template](examples/sending/template.ts)
- [Suppressions](examples/sending/suppressions.ts)
- [Nodemailer transport](examples/sending/transport.ts)

### Batch Sending API
Expand Down
30 changes: 30 additions & 0 deletions examples/sending/suppressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { MailtrapClient } from "mailtrap";

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

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

async function suppressionsFlow() {
// Get all suppressions
const allSuppressions = await client.suppressions.getList();
console.log("All suppressions:", allSuppressions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saying "all" in this case is unfortunate, because it doesn't actually return all suppressions. As the docs say, the endpoint returns up to 1000 suppressions per request (hard limit, and there's no pagination - to find something specific, a query filter should be used).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@narekhovhannisyan this was not corrected yet


// Get suppressions filtered by email
const filteredSuppressions = await client.suppressions.getList("test@example.com");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make this a named parameter, so that it's easier to add more named filters in the future.

Suggested change
const filteredSuppressions = await client.suppressions.getList("test@example.com");
const filteredSuppressions = await client.suppressions.getList({email: "test@example.com"});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@narekhovhannisyan this example should be updated after we changed the arguments

console.log("Filtered suppressions:", filteredSuppressions);

// Delete a suppression by ID (if any exist)
if (allSuppressions.length > 0) {
const suppressionToDelete = allSuppressions[0];
await client.suppressions.delete(suppressionToDelete.id);
console.log(`Suppression ${suppressionToDelete.id} deleted successfully`);
} else {
console.log("No suppressions found to delete");
}
}

suppressionsFlow().catch(console.error);
17 changes: 17 additions & 0 deletions src/__tests__/lib/api/Suppressions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import axios from "axios";

import SuppressionsBaseAPI from "../../../lib/api/Suppressions";

describe("lib/api/Suppressions: ", () => {
const accountId = 100;
const suppressionsAPI = new SuppressionsBaseAPI(axios, accountId);

describe("class SuppressionsBaseAPI(): ", () => {
describe("init: ", () => {
it("initalizes with all necessary params.", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small typo:

Suggested change
it("initalizes with all necessary params.", () => {
it("initializes with all necessary params.", () => {

expect(suppressionsAPI).toHaveProperty("getList");
expect(suppressionsAPI).toHaveProperty("delete");
});
});
});
});
170 changes: 170 additions & 0 deletions src/__tests__/lib/api/resources/Suppressions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import axios from "axios";
import AxiosMockAdapter from "axios-mock-adapter";

import SuppressionsApi from "../../../../lib/api/resources/Suppressions";
import handleSendingError from "../../../../lib/axios-logger";
import MailtrapError from "../../../../lib/MailtrapError";
import { Suppression } from "../../../../types/api/suppressions";

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

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

describe("lib/api/resources/Suppressions: ", () => {
let mock: AxiosMockAdapter;
const accountId = 100;
const suppressionsAPI = new SuppressionsApi(axios, accountId);

const mockSuppression: Suppression = {
id: "1",
email: "test@example.com",
type: "hard bounce",
created_at: "2023-01-01T00:00:00Z",
sending_stream: "transactional",
domain_name: "example.com",
message_bounce_category: "bad_mailbox",
message_category: "test",
message_client_ip: "192.168.1.1",
message_created_at: "2023-01-01T00:00:00Z",
message_outgoing_ip: "10.0.0.1",
message_recipient_mx_name: "mx.example.com",
message_sender_email: "sender@example.com",
message_subject: "Test Email",
};

const mockSuppressions: Suppression[] = [
{
id: "1",
email: "test1@example.com",
type: "hard bounce",
created_at: "2023-01-01T00:00:00Z",
sending_stream: "transactional",
domain_name: "example.com",
message_bounce_category: "bad_mailbox",
message_category: "test",
message_client_ip: "192.168.1.1",
message_created_at: "2023-01-01T00:00:00Z",
message_outgoing_ip: "10.0.0.1",
message_recipient_mx_name: "mx.example.com",
message_sender_email: "sender@example.com",
message_subject: "Test Email 1",
},
{
id: "2",
email: "test2@example.com",
type: "spam complaint",
created_at: "2023-01-02T00:00:00Z",
sending_stream: "bulk",
domain_name: "example.com",
message_bounce_category: null,
message_category: "promotional",
message_client_ip: "192.168.1.2",
message_created_at: "2023-01-02T00:00:00Z",
message_outgoing_ip: "10.0.0.2",
message_recipient_mx_name: "mx.example.com",
message_sender_email: "sender@example.com",
message_subject: "Test Email 2",
},
];

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

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

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

describe("getList(): ", () => {
it("successfully gets all suppressions.", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not all, it's just first up to 1000. See my other comment https://github.com/railsware/mailtrap-nodejs/pull/68/files#r2160850757

const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions`;

expect.assertions(2);

mock.onGet(endpoint).reply(200, mockSuppressions);
const result = await suppressionsAPI.getList();

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

it("successfully gets suppressions filtered by email.", async () => {
const email = "test@example.com";
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions`;

expect.assertions(3);

mock.onGet(endpoint, { params: { email } }).reply(200, [mockSuppression]);
const result = await suppressionsAPI.getList(email);

Check failure on line 112 in src/__tests__/lib/api/resources/Suppressions.test.ts

View workflow job for this annotation

GitHub Actions / lint

Type '"test@example.com"' has no properties in common with type 'ListOptions'.

expect(mock.history.get[0].url).toEqual(endpoint);
expect(mock.history.get[0].params).toEqual({ email });
expect(result).toEqual([mockSuppression]);
});

it("fails with error.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions`;
const expectedErrorMessage = "Request failed with status code 400";

expect.assertions(2);

mock.onGet(endpoint).reply(400, { error: expectedErrorMessage });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, but this test case is a bit unrealistic. There's no way we can get 400 from this endpoint. We documented 200, 401 and 403 only https://api-docs.mailtrap.io/docs/mailtrap-api-docs/f8144826d885a-list-and-search-suppressions


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

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

it("successfully deletes a suppression.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions/${suppressionId}`;

expect.assertions(1);

mock.onDelete(endpoint).reply(204);
await suppressionsAPI.delete(suppressionId);

expect(mock.history.delete[0].url).toEqual(endpoint);
});

it("fails with error.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions/${suppressionId}`;
const expectedErrorMessage = "Request failed with status code 404";

expect.assertions(2);

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

try {
await suppressionsAPI.delete(suppressionId);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(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 @@ -12,6 +12,7 @@ import TestingAPI from "../../lib/api/Testing";
import ContactLists from "../../lib/api/ContactLists";
import Contacts from "../../lib/api/Contacts";
import TemplatesBaseAPI from "../../lib/api/Templates";
import SuppressionsBaseAPI from "../../lib/api/Suppressions";

const { ERRORS, CLIENT_SETTINGS } = CONFIG;
const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT } = CLIENT_SETTINGS;
Expand Down Expand Up @@ -809,5 +810,31 @@ describe("lib/mailtrap-client: ", () => {
expect(templatesClient).toBeInstanceOf(TemplatesBaseAPI);
});
});

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

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

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

const suppressionsClient = client.suppressions;
expect(suppressionsClient).toBeInstanceOf(SuppressionsBaseAPI);
});
});
});
});
15 changes: 15 additions & 0 deletions src/lib/MailtrapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
BatchSendResponse,
BatchSendRequest,
} from "../types/mailtrap";
import SuppressionsBaseAPI from "./api/Suppressions";

const { CLIENT_SETTINGS, ERRORS } = CONFIG;
const {
Expand Down Expand Up @@ -132,12 +133,26 @@ export default class MailtrapClient {
return new ContactListsBaseAPI(this.axios, this.accountId);
}

/**
* Getter for Templates API.
*/
get templates() {
this.validateAccountIdPresence();

return new TemplatesBaseAPI(this.axios, this.accountId);
}

/**
* Getter for Suppressions API.
*/
get suppressions() {
if (!this.accountId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please rebase main and use the validation function for the account ID presence for consistency?

throw new MailtrapError(ACCOUNT_ID_MISSING);
}

return new SuppressionsBaseAPI(this.axios, this.accountId);
}

/**
* Returns configured host. Checks if `bulk` and `sandbox` modes are activated simultaneously,
* then reject with Mailtrap Error.
Expand Down
21 changes: 21 additions & 0 deletions src/lib/api/Suppressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AxiosInstance } from "axios";

import SuppressionsApi from "./resources/Suppressions";

export default class SuppressionsBaseAPI {
private client: AxiosInstance;

private accountId?: number;

public getList: SuppressionsApi["getList"];

public delete: SuppressionsApi["delete"];

constructor(client: AxiosInstance, accountId?: number) {
this.client = client;
this.accountId = accountId;
const suppressions = new SuppressionsApi(this.client, this.accountId);
this.getList = suppressions.getList.bind(suppressions);
this.delete = suppressions.delete.bind(suppressions);
}
}
44 changes: 44 additions & 0 deletions src/lib/api/resources/Suppressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AxiosInstance } from "axios";

import CONFIG from "../../../config";
import { ListOptions, Suppression } from "../../../types/api/suppressions";

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

export default class SuppressionsApi {
private client: AxiosInstance;

private accountId?: number;

private suppressionsURL: string;

constructor(client: AxiosInstance, accountId?: number) {
this.client = client;
this.accountId = accountId;
this.suppressionsURL = `${GENERAL_ENDPOINT}/api/accounts/${this.accountId}/suppressions`;
}

/**
* List and search suppressions by email. The endpoint returns up to 1000 suppressions per request.
*/
public async getList(options?: ListOptions) {
const params = {
...(options?.email && { email: options.email }),
};

return this.client.get<Suppression[], Suppression[]>(this.suppressionsURL, {
params,
});
}

/**
* Delete a suppression by ID.
* Mailtrap will no longer prevent sending to this email unless it's recorded in suppressions again.
*/
public async delete(id: string) {
return this.client.delete<Suppression, Suppression>(
`${this.suppressionsURL}/${id}`
);
}
}
20 changes: 20 additions & 0 deletions src/types/api/suppressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type Suppression = {
id: string;
type: "hard bounce" | "spam complaint" | "unsubscription" | "manual import";
created_at: string;
email: string;
sending_stream: "transactional" | "bulk";
domain_name: string | null;
message_bounce_category: string | null;
message_category: string | null;
message_client_ip: string | null;
message_created_at: string | null;
message_outgoing_ip: string | null;
message_recipient_mx_name: string | null;
message_sender_email: string | null;
message_subject: string | null;
};

export type ListOptions = {
email?: string;
};
Loading