diff --git a/README.md b/README.md index d41a415..44e9d97 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/sending/suppressions.ts b/examples/sending/suppressions.ts new file mode 100644 index 0000000..6b6ded6 --- /dev/null +++ b/examples/sending/suppressions.ts @@ -0,0 +1,30 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID +}); + +async function suppressionsFlow() { + // Get suppressions (up to 1000 per request) + const suppressions = await client.suppressions.getList(); + console.log("Suppressions (up to 1000):", suppressions); + + // Get suppressions filtered by email + const filteredSuppressions = await client.suppressions.getList({email: "test@example.com"}); + console.log("Filtered suppressions:", filteredSuppressions); + + // Delete a suppression by ID (if any exist) + if (suppressions.length > 0) { + const suppressionToDelete = suppressions[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); diff --git a/src/__tests__/lib/api/ContactLists.test.ts b/src/__tests__/lib/api/ContactLists.test.ts index 3cd33e3..2831c86 100644 --- a/src/__tests__/lib/api/ContactLists.test.ts +++ b/src/__tests__/lib/api/ContactLists.test.ts @@ -8,7 +8,7 @@ describe("lib/api/ContactLists: ", () => { describe("class ContactLists(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(contactListsAPI).toHaveProperty("create"); expect(contactListsAPI).toHaveProperty("getList"); expect(contactListsAPI).toHaveProperty("get"); diff --git a/src/__tests__/lib/api/Contacts.test.ts b/src/__tests__/lib/api/Contacts.test.ts index 0640293..03d151a 100644 --- a/src/__tests__/lib/api/Contacts.test.ts +++ b/src/__tests__/lib/api/Contacts.test.ts @@ -8,7 +8,7 @@ describe("lib/api/Contacts: ", () => { describe("class Contacts(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(contactsAPI).toHaveProperty("create"); expect(contactsAPI).toHaveProperty("get"); expect(contactsAPI).toHaveProperty("update"); diff --git a/src/__tests__/lib/api/General.test.ts b/src/__tests__/lib/api/General.test.ts index 224fec1..2053c45 100644 --- a/src/__tests__/lib/api/General.test.ts +++ b/src/__tests__/lib/api/General.test.ts @@ -8,7 +8,7 @@ describe("lib/api/General: ", () => { describe("class General(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(generalAPI).toHaveProperty("accountAccesses"); expect(generalAPI).toHaveProperty("accounts"); expect(generalAPI).toHaveProperty("permissions"); diff --git a/src/__tests__/lib/api/Suppressions.test.ts b/src/__tests__/lib/api/Suppressions.test.ts new file mode 100644 index 0000000..1745862 --- /dev/null +++ b/src/__tests__/lib/api/Suppressions.test.ts @@ -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("initializes with all necessary params.", () => { + expect(suppressionsAPI).toHaveProperty("getList"); + expect(suppressionsAPI).toHaveProperty("delete"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/Templates.test.ts b/src/__tests__/lib/api/Templates.test.ts index 8b490e8..3c36140 100644 --- a/src/__tests__/lib/api/Templates.test.ts +++ b/src/__tests__/lib/api/Templates.test.ts @@ -8,7 +8,7 @@ describe("lib/api/Templates: ", () => { describe("class TemplatesBaseAPI(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(templatesAPI).toHaveProperty("create"); expect(templatesAPI).toHaveProperty("getList"); expect(templatesAPI).toHaveProperty("get"); diff --git a/src/__tests__/lib/api/Testing.test.ts b/src/__tests__/lib/api/Testing.test.ts index 0ecf24d..b77b35d 100644 --- a/src/__tests__/lib/api/Testing.test.ts +++ b/src/__tests__/lib/api/Testing.test.ts @@ -8,7 +8,7 @@ describe("lib/api/Testing: ", () => { describe("class Testing(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(testingAPI).toHaveProperty("projects"); expect(testingAPI).toHaveProperty("inboxes"); expect(testingAPI).toHaveProperty("messages"); diff --git a/src/__tests__/lib/api/resources/AccountAccesses.test.ts b/src/__tests__/lib/api/resources/AccountAccesses.test.ts index 156e58b..2e39390 100644 --- a/src/__tests__/lib/api/resources/AccountAccesses.test.ts +++ b/src/__tests__/lib/api/resources/AccountAccesses.test.ts @@ -42,7 +42,7 @@ describe("lib/api/resources/AccountAccesses: ", () => { describe("class AccountAccesses(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(accountAccessesAPI).toHaveProperty("listAccountAccesses"); expect(accountAccessesAPI).toHaveProperty("removeAccountAccess"); }); diff --git a/src/__tests__/lib/api/resources/Accounts.test.ts b/src/__tests__/lib/api/resources/Accounts.test.ts index 3a67ac5..0ed3920 100644 --- a/src/__tests__/lib/api/resources/Accounts.test.ts +++ b/src/__tests__/lib/api/resources/Accounts.test.ts @@ -22,7 +22,7 @@ describe("lib/api/resources/Accounts: ", () => { describe("class Accounts(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(accountsAPI).toHaveProperty("getAllAccounts"); }); }); diff --git a/src/__tests__/lib/api/resources/Attachments.test.ts b/src/__tests__/lib/api/resources/Attachments.test.ts index 508b8e9..21a696e 100644 --- a/src/__tests__/lib/api/resources/Attachments.test.ts +++ b/src/__tests__/lib/api/resources/Attachments.test.ts @@ -34,7 +34,7 @@ describe("lib/api/resources/Attachments: ", () => { describe("class Attachments(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(attachmentsAPI).toHaveProperty("get"); expect(attachmentsAPI).toHaveProperty("getList"); }); diff --git a/src/__tests__/lib/api/resources/Inboxes.test.ts b/src/__tests__/lib/api/resources/Inboxes.test.ts index cc19859..cc86e6a 100644 --- a/src/__tests__/lib/api/resources/Inboxes.test.ts +++ b/src/__tests__/lib/api/resources/Inboxes.test.ts @@ -42,7 +42,7 @@ describe("lib/api/resources/Inboxes: ", () => { describe("class Inboxes(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(inboxesAPI).toHaveProperty("cleanInbox"); expect(inboxesAPI).toHaveProperty("create"); expect(inboxesAPI).toHaveProperty("delete"); diff --git a/src/__tests__/lib/api/resources/Messages.test.ts b/src/__tests__/lib/api/resources/Messages.test.ts index a630f2a..8b7da8c 100644 --- a/src/__tests__/lib/api/resources/Messages.test.ts +++ b/src/__tests__/lib/api/resources/Messages.test.ts @@ -45,7 +45,7 @@ describe("lib/api/resources/Messages: ", () => { describe("class Messages(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(messagesAPI).toHaveProperty("deleteMessage"); expect(messagesAPI).toHaveProperty("forward"); expect(messagesAPI).toHaveProperty("get"); diff --git a/src/__tests__/lib/api/resources/Permissions.test.ts b/src/__tests__/lib/api/resources/Permissions.test.ts index 30344a8..3b50779 100644 --- a/src/__tests__/lib/api/resources/Permissions.test.ts +++ b/src/__tests__/lib/api/resources/Permissions.test.ts @@ -28,7 +28,7 @@ describe("lib/api/resources/Permissions: ", () => { describe("class Permissions(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(permissionsAPI).toHaveProperty("getResources"); expect(permissionsAPI).toHaveProperty("bulkPermissionsUpdate"); }); diff --git a/src/__tests__/lib/api/resources/Projects.test.ts b/src/__tests__/lib/api/resources/Projects.test.ts index cd4f554..fc96103 100644 --- a/src/__tests__/lib/api/resources/Projects.test.ts +++ b/src/__tests__/lib/api/resources/Projects.test.ts @@ -24,7 +24,7 @@ describe("lib/api/resources/Projects: ", () => { describe("class Projects(): ", () => { describe("init: ", () => { - it("initalizes with all necessary params.", () => { + it("initializes with all necessary params.", () => { expect(projectsAPI).toHaveProperty("create"); expect(projectsAPI).toHaveProperty("delete"); expect(projectsAPI).toHaveProperty("getById"); diff --git a/src/__tests__/lib/api/resources/Suppressions.test.ts b/src/__tests__/lib/api/resources/Suppressions.test.ts new file mode 100644 index 0000000..2767133 --- /dev/null +++ b/src/__tests__/lib/api/resources/Suppressions.test.ts @@ -0,0 +1,206 @@ +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 suppressions (up to 1000 per request).", async () => { + 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 }); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(mock.history.get[0].params).toEqual({ email }); + expect(result).toEqual([mockSuppression]); + }); + + it("fails with unauthorized error (401).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions`; + const expectedErrorMessage = "Incorrect API token"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(401, { error: expectedErrorMessage }); + + try { + await suppressionsAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + + it("fails with forbidden error (403).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions`; + const expectedErrorMessage = "Access forbidden"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(403, { errors: expectedErrorMessage }); + + 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 unauthorized error (401).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions/${suppressionId}`; + const expectedErrorMessage = "Incorrect API token"; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(401, { error: expectedErrorMessage }); + + try { + await suppressionsAPI.delete(suppressionId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + + it("fails with forbidden error (403).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/suppressions/${suppressionId}`; + const expectedErrorMessage = "Access forbidden"; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(403, { errors: expectedErrorMessage }); + + try { + await suppressionsAPI.delete(suppressionId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index 7a976e6..65b5890 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -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; @@ -846,5 +847,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); + }); + }); }); }); diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 8b17e5a..a41060e 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -22,6 +22,7 @@ import { BatchSendResponse, BatchSendRequest, } from "../types/mailtrap"; +import SuppressionsBaseAPI from "./api/Suppressions"; const { CLIENT_SETTINGS, ERRORS } = CONFIG; const { @@ -146,6 +147,15 @@ export default class MailtrapClient { return new TemplatesBaseAPI(this.axios, this.accountId); } + /** + * Getter for Suppressions API. + */ + get suppressions() { + this.validateAccountIdPresence(); + + return new SuppressionsBaseAPI(this.axios, this.accountId); + } + /** * Returns configured host. Checks if `bulk` and `sandbox` modes are activated simultaneously, * then reject with Mailtrap Error. diff --git a/src/lib/api/Suppressions.ts b/src/lib/api/Suppressions.ts new file mode 100644 index 0000000..c5da482 --- /dev/null +++ b/src/lib/api/Suppressions.ts @@ -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); + } +} diff --git a/src/lib/api/resources/Suppressions.ts b/src/lib/api/resources/Suppressions.ts new file mode 100644 index 0000000..2c10529 --- /dev/null +++ b/src/lib/api/resources/Suppressions.ts @@ -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(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( + `${this.suppressionsURL}/${id}` + ); + } +} diff --git a/src/types/api/suppressions.ts b/src/types/api/suppressions.ts new file mode 100644 index 0000000..e707739 --- /dev/null +++ b/src/types/api/suppressions.ts @@ -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; +};