diff --git a/README.md b/README.md index 3fc9833..f497b4c 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,13 @@ Refer to the [`examples`](examples) folder for the source code of this and other - [Send email using template](examples/sending/template.ts) - [Nodemailer transport](examples/sending/transport.ts) +### Batch Sending API + + - [Send a batch of transactional emails](examples/batch/transactional.ts) + - [Send a batch of bulk emails](examples/batch/bulk.ts) + - [Send a batch of sandbox emails](examples/batch/sandbox.ts) + - [Send a batch of emails from template](examples/batch/template.ts) + ### Email testing API - [Attachments](examples/testing/attachments.ts) diff --git a/examples/batch/bulk.ts b/examples/batch/bulk.ts new file mode 100644 index 0000000..a152e8a --- /dev/null +++ b/examples/batch/bulk.ts @@ -0,0 +1,39 @@ +import { MailtrapClient } from "mailtrap"; + +/** + * For this example, you need to have ready-to-use sending domain or, + * a Demo domain that allows sending emails to your own account email. + * @see https://help.mailtrap.io/article/69-sending-domain-setup + */ + +const TOKEN = ""; +const SENDER_EMAIL = ""; +const RECIPIENT_EMAIL = ""; + +const client = new MailtrapClient({ + token: TOKEN, +}); + +client.batchSend({ + base: { + from: { name: "Mailtrap Test", email: SENDER_EMAIL }, + subject: "Sandbox Email", + text: "Welcome to Mailtrap Sandbox Batch Sending!" + }, + requests: [ + { + to: [{ email: RECIPIENT_EMAIL }], + custom_variables: { + email_number: 1 + } + }, + { + to: [{ email: RECIPIENT_EMAIL }], + custom_variables: { + email_number: 2 + } + } + ] +}) + .then(console.log) + .catch(console.error); \ No newline at end of file diff --git a/examples/batch/sandbox.ts b/examples/batch/sandbox.ts new file mode 100644 index 0000000..ac06ff7 --- /dev/null +++ b/examples/batch/sandbox.ts @@ -0,0 +1,42 @@ +import { MailtrapClient } from "mailtrap"; + +/** + * For this example, you need to have ready-to-use sending domain or, + * a Demo domain that allows sending emails to your own account email. + * @see https://help.mailtrap.io/article/69-sending-domain-setup + */ + +const TOKEN = ""; +const TEST_INBOX_ID = ""; +const SENDER_EMAIL = ""; +const RECIPIENT_EMAIL = ""; + +const client = new MailtrapClient({ + token: TOKEN, + sandbox: true, + testInboxId: TEST_INBOX_ID +}); + +client.batchSend({ + base: { + from: { name: "Mailtrap Test", email: SENDER_EMAIL }, + subject: "Sandbox Email", + text: "Welcome to Mailtrap Sandbox Batch Sending!" + }, + requests: [ + { + to: [{ email: RECIPIENT_EMAIL }], + custom_variables: { + email_number: 1 + } + }, + { + to: [{ email: RECIPIENT_EMAIL }], + custom_variables: { + email_number: 2 + } + } + ] +}) + .then(console.log) + .catch(console.error); diff --git a/examples/batch/template.ts b/examples/batch/template.ts new file mode 100644 index 0000000..39649da --- /dev/null +++ b/examples/batch/template.ts @@ -0,0 +1,39 @@ +import { MailtrapClient } from "mailtrap"; + +/** + * For this example, you need to have ready-to-use sending domain or, + * a Demo domain that allows sending emails to your own account email. + * @see https://help.mailtrap.io/article/69-sending-domain-setup + */ + +const TOKEN = ""; +const SENDER_EMAIL = ""; +const RECIPIENT_EMAIL = ""; +const TEMPLATE_UUID = ""; + +const client = new MailtrapClient({ token: TOKEN }); + +client.batchSend({ + base: { + from: { name: "Mailtrap Test", email: SENDER_EMAIL }, + template_uuid: TEMPLATE_UUID + }, + requests: [ + { + to: [{ email: RECIPIENT_EMAIL }], + template_variables: { + user_name: "John Doe", + company_name: "Example Corp" + } + }, + { + to: [{ email: RECIPIENT_EMAIL }], + template_variables: { + user_name: "Jane Smith", + company_name: "Example Corp" + } + } + ] +}) + .then(console.log) + .catch(console.error); diff --git a/examples/batch/transactional.ts b/examples/batch/transactional.ts new file mode 100644 index 0000000..7e64388 --- /dev/null +++ b/examples/batch/transactional.ts @@ -0,0 +1,37 @@ +import { MailtrapClient } from "mailtrap"; + +/** + * For this example, you need to have ready-to-use sending domain or, + * a Demo domain that allows sending emails to your own account email. + * @see https://help.mailtrap.io/article/69-sending-domain-setup + */ + +const TOKEN = ""; +const SENDER_EMAIL = ""; +const RECIPIENT_EMAIL = ""; + +const client = new MailtrapClient({ token: TOKEN }); + +client.batchSend({ + base: { + from: { name: "Mailtrap Test", email: SENDER_EMAIL }, + subject: "Transactional Email", + text: "Welcome to Mailtrap Batch Sending!" + }, + requests: [ + { + to: [{ email: RECIPIENT_EMAIL }], + custom_variables: { + email_number: 1 + } + }, + { + to: [{ email: RECIPIENT_EMAIL }], + custom_variables: { + email_number: 2 + } + } + ] +}) + .then(console.log) + .catch(console.error); diff --git a/examples/testing/send-mail.ts b/examples/testing/send-mail.ts index ca40e73..15bbde1 100644 --- a/examples/testing/send-mail.ts +++ b/examples/testing/send-mail.ts @@ -4,9 +4,9 @@ import { MailtrapClient } from "mailtrap" * For this example, you need to have ready-to-use sending domain or, * a Demo domain that allows sending emails to your own account email. * @see https://help.mailtrap.io/article/69-sending-domain-setup + * @see https://help.mailtrap.io/article/69-sending-domain-setup#Demo-Domain--oYOU5" */ -", @see https://help.mailtrap.io/article/69-sending-domain-setup#Demo-Domain--oYOU5" const TOKEN = ""; const TEST_INBOX_ID = "" diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index c510081..7de7fa7 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -327,6 +327,337 @@ describe("lib/mailtrap-client: ", () => { } } }); + + describe("batch sending:", () => { + it("rejects with Mailtrap error when bulk and sandbox modes are used together", async () => { + const batchClient = new MailtrapClient({ + token: "MY_API_TOKEN", + bulk: true, + sandbox: true, + }); + + const batchData = { + base: { + from: { + email: "sender@mailtrap.io", + name: "Mailtrap", + }, + subject: "Batch Subject", + text: "Batch Text", + }, + requests: [ + { + to: [ + { + email: "recipient1.mock@email.com", + name: "recipient1", + }, + ], + }, + ], + }; + + try { + await batchClient.batchSend(batchData); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(BULK_SANDBOX_INCOMPATIBLE); + } + } + }); + + it("successfully sends a batch of emails in bulk mode", async () => { + const batchClient = new MailtrapClient({ + token: "MY_API_TOKEN", + bulk: true, + }); + const endpoint = `${BULK_ENDPOINT}/api/batch`; + const expectedBatchResponseData = { + success: true, + responses: [ + { + success: true, + message_ids: ["57f17fe0-3935-11f0-0000-f15507b34198"], + }, + { + success: true, + message_ids: ["57f17fe0-3935-11f0-0000-f15507b34199"], + }, + ], + }; + mock.onPost(endpoint).reply(200, expectedBatchResponseData); + + const batchData = { + base: { + from: { + email: "sender@mailtrap.io", + name: "Mailtrap", + }, + subject: "Batch Subject", + text: "Batch Text", + }, + requests: [ + { + to: [ + { + email: "recipient1.mock@email.com", + name: "recipient1", + }, + ], + }, + { + to: [ + { + email: "recipient2.mock@email.com", + name: "recipient2", + }, + ], + }, + ], + }; + + const result = await batchClient.batchSend(batchData); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(mock.history.post[0].data).toEqual( + JSON.stringify({ + base: batchData.base, + requests: batchData.requests.map((req) => ({ + to: req.to, + })), + }) + ); + expect(result).toEqual(expectedBatchResponseData); + }); + + it("successfully sends a batch of emails in sandbox mode", async () => { + const batchClient = new MailtrapClient({ + token: "MY_API_TOKEN", + sandbox: true, + testInboxId: 100, + }); + const endpoint = `${TESTING_ENDPOINT}/api/batch/100`; + const expectedBatchResponseData = { + success: true, + responses: [ + { + success: true, + message_ids: ["57f17fe0-3935-11f0-0000-f15507b34198"], + }, + ], + }; + mock.onPost(endpoint).reply(200, expectedBatchResponseData); + + const batchData = { + base: { + from: { + email: "sender@mailtrap.io", + name: "Mailtrap", + }, + subject: "Batch Subject", + text: "Batch Text", + }, + requests: [ + { + to: [ + { + email: "recipient1.mock@email.com", + name: "recipient1", + }, + ], + }, + { + to: [ + { + email: "recipient2.mock@email.com", + name: "recipient2", + }, + ], + }, + ], + }; + + const result = await batchClient.batchSend(batchData); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(mock.history.post[0].data).toEqual( + JSON.stringify({ + base: batchData.base, + requests: batchData.requests.map((req) => ({ + to: req.to, + })), + }) + ); + expect(result).toEqual(expectedBatchResponseData); + }); + + it("successfully sends a batch of emails with template", async () => { + const batchClient = new MailtrapClient({ + token: "MY_API_TOKEN", + bulk: true, + }); + const endpoint = `${BULK_ENDPOINT}/api/batch`; + const expectedBatchResponseData = { + success: true, + responses: [ + { + success: true, + message_ids: ["57f17fe0-3935-11f0-0000-f15507b34198"], + }, + ], + }; + mock.onPost(endpoint).reply(200, expectedBatchResponseData); + + const batchData = { + base: { + from: { + email: "sender@mailtrap.io", + name: "Mailtrap", + }, + template_uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", + template_variables: { + company_name: "Mailtrap", + }, + }, + requests: [ + { + to: [ + { + email: "recipient1.mock@email.com", + name: "recipient1", + }, + ], + }, + { + to: [ + { + email: "recipient2.mock@email.com", + name: "recipient2", + }, + ], + }, + ], + }; + + const result = await batchClient.batchSend(batchData); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(mock.history.post[0].data).toEqual( + JSON.stringify({ + base: batchData.base, + requests: batchData.requests.map((req) => ({ + to: req.to, + })), + }) + ); + expect(result).toEqual(expectedBatchResponseData); + }); + + it("successfully sends a batch of transactional emails", async () => { + const batchClient = new MailtrapClient({ + token: "MY_API_TOKEN", + }); + const endpoint = `${SENDING_ENDPOINT}/api/batch`; + const expectedBatchResponseData = { + success: true, + responses: [ + { + success: true, + message_ids: ["1cc512a0-3936-11f0-0040-f1a6f440b26b"], // First recipient + }, + { + success: true, + message_ids: ["1cc47660-3936-11f0-0040-f1a6f440b26b"], // Second recipient + }, + ], + }; + mock.onPost(endpoint).reply(200, expectedBatchResponseData); + + const batchData = { + base: { + from: { + email: "sender@mailtrap.io", + name: "Mailtrap", + }, + subject: "Transactional Batch Subject", + text: "Transactional Batch Text", + }, + requests: [ + { + to: [ + { + email: "recipient1.mock@email.com", + name: "recipient1", + }, + ], + }, + { + to: [ + { + email: "recipient2.mock@email.com", + name: "recipient2", + }, + ], + }, + ], + }; + + const result = await batchClient.batchSend(batchData); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(mock.history.post[0].data).toEqual( + JSON.stringify({ + base: batchData.base, + requests: batchData.requests.map((req) => ({ + to: req.to, + })), + }) + ); + expect(result).toEqual(expectedBatchResponseData); + }); + + it("handles API errors for batch sending", async () => { + const batchClient = new MailtrapClient({ + token: "MY_API_TOKEN", + bulk: true, + }); + const endpoint = `${BULK_ENDPOINT}/api/batch`; + const responseData = { + success: true, + responses: [ + { + success: false, + errors: ["'from' is required", "'subject' is required"], + }, + ], + }; + mock.onPost(endpoint).reply(200, responseData); + + const batchData = { + base: { + text: "Batch Text", + }, + requests: [ + { + to: [ + { + email: "recipient1.mock@email.com", + name: "recipient1", + }, + ], + }, + ], + }; + + // @ts-ignore + const result = await batchClient.batchSend(batchData); + expect(result).toEqual(responseData); + expect(result.responses[0].success).toBe(false); + expect(result.responses[0].errors).toContain("'from' is required"); + expect(result.responses[0].errors).toContain("'subject' is required"); + }); + }); }); describe("get testing(): ", () => { diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index a0b9e81..eb4dcc6 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -11,7 +11,13 @@ import TestingAPI from "./api/Testing"; import CONFIG from "../config"; -import { Mail, SendResponse, MailtrapClientConfig } from "../types/mailtrap"; +import { + Mail, + SendResponse, + MailtrapClientConfig, + BatchSendResponse, + BatchSendRequest, +} from "../types/mailtrap"; import MailtrapError from "./MailtrapError"; const { CLIENT_SETTINGS, ERRORS } = CONFIG; @@ -129,4 +135,28 @@ export default class MailtrapClient { return this.axios.post(url, preparedMail); } + + /** + * Sends a batch of emails with the given array of mail objects. + * If there is an error, rejects with MailtrapError. + */ + public async batchSend( + request: BatchSendRequest + ): Promise { + const { requests, base } = request; + const host = this.determineHost(); + const ifSandbox = + this.sandbox && this.testInboxId ? `/${this.testInboxId}` : ""; + const url = `${host}/api/batch${ifSandbox}`; + + const preparedBase = base ? encodeMailBuffers(base) : undefined; + const preparedRequests = requests.map((singleRequest) => + encodeMailBuffers(singleRequest as Partial) + ); + + return this.axios.post(url, { + base: preparedBase, + requests: preparedRequests, + }); + } } diff --git a/src/lib/mail-buffer-encoder.ts b/src/lib/mail-buffer-encoder.ts index 1a33d06..1b4126a 100644 --- a/src/lib/mail-buffer-encoder.ts +++ b/src/lib/mail-buffer-encoder.ts @@ -5,7 +5,7 @@ import { Mail } from "../types/mailtrap"; * If `mail` has `html` and it's an instance of `Buffer`, then converts to string. * If it has attachments, then converts `Buffer` content to `base64` string if needed. */ -export default function encodeMailBuffers(mail: Mail): Mail { +export default function encodeMailBuffers(mail: Partial): Partial { const preparedMail = { ...mail }; if ("text" in preparedMail && preparedMail.text instanceof Buffer) { diff --git a/src/types/mailtrap.ts b/src/types/mailtrap.ts index 6731ebd..4e8b506 100644 --- a/src/types/mailtrap.ts +++ b/src/types/mailtrap.ts @@ -68,3 +68,58 @@ export type MailtrapClientConfig = { bulk?: boolean; sandbox?: boolean; }; + +export type BatchMail = Mail[]; + +export interface BatchSendResponse { + success: boolean; + responses: Array<{ + success: boolean; + message_ids?: string[]; + errors?: string[]; + }>; +} + +interface BaseAddress { + email: string; + name?: string; +} + +interface InlineBatchSendBase extends Omit { + from: BaseAddress; + subject: string; // Required when using inline content + text?: string | Buffer; + html?: string | Buffer; + attachments?: Attachment[]; + headers?: Record; + custom_variables?: Record; + category?: string; // Allowed for inline content + reply_to?: BaseAddress; +} + +interface TemplateBatchSendBase extends Omit { + from: BaseAddress; + template_uuid: string; // Required for template usage + template_variables?: Record; + custom_variables?: Record; + reply_to?: BaseAddress; +} + +export interface BatchSendRequest { + base?: InlineBatchSendBase | TemplateBatchSendBase; + requests: { + to: BaseAddress[]; + cc?: BaseAddress[]; + bcc?: BaseAddress[]; + reply_to?: BaseAddress[]; + subject?: string; + text?: string; + html?: string; + category?: string; // Only allowed when not using template_uuid + template_uuid?: string; + template_variables?: Record; + custom_variables?: Record; + attachments?: Attachment[]; + headers?: Record; + }[]; +}