From 7b7feb874ad7fb2f8adb1f1edabee390bc22735f Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Thu, 30 Jul 2026 13:35:33 +0200 Subject: [PATCH 1/2] MT-22401: Add email campaigns tools Decisions: - 11 tools mirror src/tools/contactLists/ layout (one file per tool, schemas/, __tests__, barrel index.ts), registered after the contacts group in src/server.ts - Handlers target the future mailtrap npm emailCampaigns surface via a local EmailCampaignsClient interface in src/types/mailtrap.ts with a single TODO(MT-22401) to drop the casts after the SDK release - Single-object and stats responses unwrap the {data} envelope; list keeps {data, pagination} so page tokens stay visible; delete returns a {email_campaign_id, deleted: true} confirmation - Zod validation layer added for list/create/update/schedule per repo CLAUDE.md recommendation - readOnlyHint for list/get/stats; destructiveHint: true for create/update/delete and all five lifecycle tools per the MT-22401 plan --- CLAUDE.md | 15 ++ README.md | 107 +++++++++ src/server.ts | 133 +++++++++++ .../__tests__/cancelEmailCampaign.test.ts | 45 ++++ .../__tests__/createEmailCampaign.test.ts | 90 ++++++++ .../__tests__/deleteEmailCampaign.test.ts | 42 ++++ .../__tests__/getEmailCampaign.test.ts | 45 ++++ .../__tests__/getEmailCampaignStats.test.ts | 90 ++++++++ .../__tests__/listEmailCampaigns.test.ts | 107 +++++++++ .../__tests__/resetEmailCampaign.test.ts | 45 ++++ .../__tests__/scheduleEmailCampaign.test.ts | 125 +++++++++++ .../__tests__/startEmailCampaign.test.ts | 45 ++++ .../__tests__/terminateEmailCampaign.test.ts | 45 ++++ .../__tests__/updateEmailCampaign.test.ts | 96 ++++++++ .../emailCampaigns/cancelEmailCampaign.ts | 28 +++ .../emailCampaigns/createEmailCampaign.ts | 35 +++ .../emailCampaigns/deleteEmailCampaign.ts | 30 +++ src/tools/emailCampaigns/getEmailCampaign.ts | 28 +++ .../emailCampaigns/getEmailCampaignStats.ts | 49 +++++ src/tools/emailCampaigns/index.ts | 47 ++++ .../emailCampaigns/listEmailCampaigns.ts | 50 +++++ .../emailCampaigns/resetEmailCampaign.ts | 28 +++ .../emailCampaigns/scheduleEmailCampaign.ts | 39 ++++ .../schemas/cancelEmailCampaign.ts | 14 ++ .../schemas/createEmailCampaign.ts | 143 ++++++++++++ .../schemas/deleteEmailCampaign.ts | 14 ++ .../schemas/getEmailCampaign.ts | 14 ++ .../schemas/getEmailCampaignStats.ts | 44 ++++ .../schemas/listEmailCampaigns.ts | 36 +++ .../schemas/resetEmailCampaign.ts | 14 ++ .../schemas/scheduleEmailCampaign.ts | 42 ++++ .../schemas/startEmailCampaign.ts | 14 ++ .../schemas/terminateEmailCampaign.ts | 14 ++ .../schemas/updateEmailCampaign.ts | 155 +++++++++++++ .../emailCampaigns/startEmailCampaign.ts | 28 +++ .../emailCampaigns/terminateEmailCampaign.ts | 28 +++ .../emailCampaigns/updateEmailCampaign.ts | 40 ++++ src/types/mailtrap.ts | 208 ++++++++++++++++++ 38 files changed, 2172 insertions(+) create mode 100644 src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts create mode 100644 src/tools/emailCampaigns/cancelEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/createEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/deleteEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/getEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/getEmailCampaignStats.ts create mode 100644 src/tools/emailCampaigns/index.ts create mode 100644 src/tools/emailCampaigns/listEmailCampaigns.ts create mode 100644 src/tools/emailCampaigns/resetEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/scheduleEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/cancelEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/createEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/deleteEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/getEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/getEmailCampaignStats.ts create mode 100644 src/tools/emailCampaigns/schemas/listEmailCampaigns.ts create mode 100644 src/tools/emailCampaigns/schemas/resetEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/scheduleEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/startEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/terminateEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/schemas/updateEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/startEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/terminateEmailCampaign.ts create mode 100644 src/tools/emailCampaigns/updateEmailCampaign.ts diff --git a/CLAUDE.md b/CLAUDE.md index c582051..fbb7cde 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -180,6 +180,21 @@ Folders contain inboxes; inboxes receive messages, grouped into threads. - **get-contact-export**: Get the status of a contact export job. `url` is populated when `status: finished`. +#### Email Campaigns + +- **list-email-campaigns**: List the account's email campaigns, newest first, with page-token pagination (`token`, `per_page`) and optional `search` filter by name. +- **get-email-campaign**: Get an email campaign by ID. +- **create-email-campaign**: Create a `draft` campaign. Requires `name`, `domain_id` (sending domain ID), `from_local_part`, and `template_attributes.subject`. +- **update-email-campaign**: Update a `draft` campaign (partial; template edited in place). Only `draft` campaigns can be updated. +- **delete-email-campaign**: Delete a campaign by ID (must not be in a sending state). +- **start-email-campaign**: Start sending a `draft` campaign immediately. +- **schedule-email-campaign**: Schedule a `draft` campaign; `datetime` (ISO 8601) must be in the future and no more than 1 month ahead. +- **cancel-email-campaign**: Cancel a `scheduled` campaign, returning it to `draft`. +- **terminate-email-campaign**: Terminate a sending campaign (`started`/`queued`/`paused`), aborting the in-flight send. +- **reset-email-campaign**: Reset a `scheduled` campaign back to `draft`. +- **get-email-campaign-stats**: Aggregated campaign stats (counts + rates), optional `start_date`/`end_date` window (`YYYY-MM-DD`). + + #### General / Account-admin - **list-accounts**: List Mailtrap accounts the API token can access (with each account's `access_levels`). diff --git a/README.md b/README.md index 133ac0b..3b61350 100644 --- a/README.md +++ b/README.md @@ -857,6 +857,113 @@ Get the status of a contact export job. Once `status` is `finished`, the `url` f - `export_id` (required): ID of the contact export job +### list-email-campaigns + +List the account's email campaigns, newest first, with page-token pagination. Optionally filter by name with `search`. + +**Parameters:** + +- `token` (optional): Page number to retrieve (page-token pagination). Defaults to `1` +- `per_page` (optional): Number of campaigns per page. Defaults to `50`, maximum `100` +- `search` (optional): Filter campaigns by name (case-insensitive partial match) + +### get-email-campaign + +Get an email campaign by ID. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign + +### create-email-campaign + +Create a new email campaign. The campaign is always created in the `draft` state; scheduling and starting are separate tools (`schedule-email-campaign`, `start-email-campaign`). + +**Parameters:** + +- `name` (required): Campaign name +- `domain_id` (required): ID of the verified sending domain used for the campaign, as returned by the Sending Domains endpoints +- `from_local_part` (required): Local part (before the @) of the From address +- `template_attributes` (required): Inline email template. Has: + - `subject` (required): Email subject line (max 255 chars). Supports merge tags, e.g. `Hi {{first_name}}` + - `body_html` (optional): HTML body (the design). Required before the campaign can be scheduled or started. Include an unsubscribe link via an anchor whose `href` contains the `__unsubscribe_url__` placeholder + - `body_text` (optional): Plain-text alternative of the email body + - `merge_tags` (optional): Bare names of the merge tags referenced in the subject/body, e.g. `["first_name"]` +- `from_display_name` (optional): Display name shown in the From header +- `reply_to` (optional): Reply-To address parts (`display_name`, `local_part`, `domain`) +- `delivery_mode` (optional): `rapid` (send as fast as possible) or `gradual` (throttle to `delivery_options.emails_per_hour`) +- `delivery_options` (optional): Delivery throttling options (`emails_per_hour`) +- `contact_list_ids` (optional): IDs of contact lists to send to (treated as the full set of included lists) +- `contact_segment_ids` (optional): IDs of contact segments to send to (treated as the full set of included segments) + +### update-email-campaign + +Update a `draft` email campaign. Only the provided fields change; the template is edited in place. Campaigns in any other state cannot be updated. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign to update +- All other parameters are optional and identical to `create-email-campaign` (`name`, `domain_id`, `from_local_part`, `from_display_name`, `reply_to`, `template_attributes`, `delivery_mode`, `delivery_options`, `contact_list_ids`, `contact_segment_ids`) + +### delete-email-campaign + +Delete an email campaign by ID. The campaign must not be in a sending state. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign to delete + +### start-email-campaign + +Start sending a `draft` email campaign immediately. Only `draft` campaigns can be started; the template must have a `body_html` design and the audience and verified sending domain must be set. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign to start + +### schedule-email-campaign + +Schedule a `draft` email campaign to start sending at a future time. Only `draft` campaigns can be scheduled. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign to schedule +- `datetime` (required): When to send the campaign (ISO 8601). Must be in the future and no more than 1 month ahead + +### cancel-email-campaign + +Cancel a `scheduled` email campaign, returning it to `draft`. Only `scheduled` campaigns can be cancelled. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign to cancel + +### terminate-email-campaign + +Terminate an email campaign that is currently sending (`started`, `queued`, or `paused`), aborting the in-flight send. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign to terminate + +### reset-email-campaign + +Reset a `scheduled` email campaign back to `draft`. Only `scheduled` campaigns can be reset. + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign to reset + +### get-email-campaign-stats + +Get aggregated performance statistics for an email campaign (counts and rates for deliveries, opens, clicks, bounces, spam complaints, and unsubscriptions). + +**Parameters:** + +- `email_campaign_id` (required): ID of the email campaign +- `start_date` (optional): Start of the aggregation window (inclusive), `YYYY-MM-DD`. Defaults to the day the campaign was last started +- `end_date` (optional): End of the aggregation window (inclusive), `YYYY-MM-DD`. Defaults to the current date + ### list-accounts List Mailtrap accounts the current API token can access, with each account's access levels. diff --git a/src/server.ts b/src/server.ts index 14582c0..397d961 100644 --- a/src/server.ts +++ b/src/server.ts @@ -174,6 +174,30 @@ import { getContactExport, getContactExportSchema, } from "./tools/contactExports"; +import { + listEmailCampaigns, + listEmailCampaignsSchema, + getEmailCampaign, + getEmailCampaignSchema, + createEmailCampaign, + createEmailCampaignSchema, + updateEmailCampaign, + updateEmailCampaignSchema, + deleteEmailCampaign, + deleteEmailCampaignSchema, + startEmailCampaign, + startEmailCampaignSchema, + scheduleEmailCampaign, + scheduleEmailCampaignSchema, + cancelEmailCampaign, + cancelEmailCampaignSchema, + terminateEmailCampaign, + terminateEmailCampaignSchema, + resetEmailCampaign, + resetEmailCampaignSchema, + getEmailCampaignStats, + getEmailCampaignStatsSchema, +} from "./tools/emailCampaigns"; import { listAccounts, listAccountsSchema } from "./tools/accounts"; import { getBillingUsage, getBillingUsageSchema } from "./tools/billing"; import { @@ -952,6 +976,115 @@ const tools = [ readOnlyHint: true, }, }, + { + name: "list-email-campaigns", + description: + "List the account's email campaigns, newest first, with page-token pagination (`token`, `per_page`). Optionally filter by name with `search`.", + inputSchema: listEmailCampaignsSchema, + handler: listEmailCampaigns, + annotations: { + readOnlyHint: true, + }, + }, + { + name: "get-email-campaign", + description: "Get an email campaign by ID.", + inputSchema: getEmailCampaignSchema, + handler: getEmailCampaign, + annotations: { + readOnlyHint: true, + }, + }, + { + name: "create-email-campaign", + description: + "Create a new email campaign in the `draft` state; requires `name`, `domain_id`, `from_local_part`, and a `template_attributes.subject`.", + inputSchema: createEmailCampaignSchema, + handler: createEmailCampaign, + annotations: { + destructiveHint: false, + }, + }, + { + name: "update-email-campaign", + description: + "Update a `draft` email campaign; only the provided fields change, and campaigns in any other state cannot be updated.", + inputSchema: updateEmailCampaignSchema, + handler: updateEmailCampaign, + annotations: { + destructiveHint: true, + }, + }, + { + name: "delete-email-campaign", + description: + "Delete an email campaign by ID; the campaign must not be in a sending state.", + inputSchema: deleteEmailCampaignSchema, + handler: deleteEmailCampaign, + annotations: { + destructiveHint: true, + }, + }, + { + name: "start-email-campaign", + description: + "Start sending a `draft` email campaign immediately; only `draft` campaigns can be started.", + inputSchema: startEmailCampaignSchema, + handler: startEmailCampaign, + annotations: { + destructiveHint: true, + }, + }, + { + name: "schedule-email-campaign", + description: + "Schedule a `draft` email campaign to start sending at `datetime` (ISO 8601, must be in the future and no more than 1 month ahead); only `draft` campaigns can be scheduled.", + inputSchema: scheduleEmailCampaignSchema, + handler: scheduleEmailCampaign, + annotations: { + destructiveHint: true, + }, + }, + { + name: "cancel-email-campaign", + description: + "Cancel a `scheduled` email campaign, returning it to `draft`; only `scheduled` campaigns can be cancelled.", + inputSchema: cancelEmailCampaignSchema, + handler: cancelEmailCampaign, + annotations: { + destructiveHint: true, + }, + }, + { + name: "terminate-email-campaign", + description: + "Terminate an email campaign that is currently sending (`started`, `queued`, or `paused`), aborting the in-flight send.", + inputSchema: terminateEmailCampaignSchema, + handler: terminateEmailCampaign, + annotations: { + destructiveHint: true, + }, + }, + { + name: "reset-email-campaign", + description: + "Reset a `scheduled` email campaign back to `draft`; only `scheduled` campaigns can be reset.", + inputSchema: resetEmailCampaignSchema, + handler: resetEmailCampaign, + annotations: { + destructiveHint: true, + }, + }, + { + name: "get-email-campaign-stats", + description: + "Get aggregated performance statistics for an email campaign; optionally narrow the window with `start_date`/`end_date` (`YYYY-MM-DD`).", + inputSchema: getEmailCampaignStatsSchema, + handler: getEmailCampaignStats, + annotations: { + readOnlyHint: true, + }, + }, { name: "list-accounts", description: diff --git a/src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts new file mode 100644 index 0000000..6ed37b5 --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts @@ -0,0 +1,45 @@ +import cancelEmailCampaign from "../cancelEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + cancel: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("cancelEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("cancels the campaign and returns the unwrapped campaign", async () => { + mockClient.emailCampaigns.cancel.mockResolvedValue({ + data: { id: 4567, current_state: "draft" }, + }); + + const result = await cancelEmailCampaign({ email_campaign_id: 4567 }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.cancel).toHaveBeenCalledWith(4567); + expect(result.content[0].text).toContain('"current_state": "draft"'); + expect(result.isError).toBeUndefined(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.cancel.mockRejectedValue( + new Error("Campaign is not scheduled") + ); + + const result = await cancelEmailCampaign({ email_campaign_id: 4567 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to cancel email campaign: Campaign is not scheduled" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts new file mode 100644 index 0000000..9232999 --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts @@ -0,0 +1,90 @@ +import createEmailCampaign from "../createEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + create: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +const validParams = { + name: "Spring Sale", + domain_id: 4321, + from_local_part: "news", + template_attributes: { subject: "Spring is here — 30% off" }, +}; + +describe("createEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("creates a campaign with a flat body and returns the unwrapped campaign", async () => { + mockClient.emailCampaigns.create.mockResolvedValue({ + data: { id: 4567, name: "Spring Sale", current_state: "draft" }, + }); + + const result = await createEmailCampaign(validParams); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.create).toHaveBeenCalledWith(validParams); + expect(result.content[0].text).toContain('"id": 4567'); + expect(result.content[0].text).toContain('"current_state": "draft"'); + expect(result.isError).toBeUndefined(); + }); + + it("passes optional audience and delivery fields through", async () => { + mockClient.emailCampaigns.create.mockResolvedValue({ + data: { id: 4568, name: "Spring Sale" }, + }); + + const params = { + ...validParams, + from_display_name: "Acme Marketing", + reply_to: { + display_name: "Acme Support", + local_part: "support", + domain: "acme.com", + }, + delivery_mode: "gradual", + delivery_options: { emails_per_hour: 1000 }, + contact_list_ids: [55, 56], + contact_segment_ids: [12], + }; + + const result = await createEmailCampaign(params); + + expect(mockClient.emailCampaigns.create).toHaveBeenCalledWith(params); + expect(result.isError).toBeUndefined(); + }); + + it("rejects input without a template subject", async () => { + const result = await createEmailCampaign({ + ...validParams, + template_attributes: {}, + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid input"); + expect(result.content[0].text).toContain("template_attributes.subject"); + expect(mockClient.emailCampaigns.create).not.toHaveBeenCalled(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.create.mockRejectedValue( + new Error("name can't be blank") + ); + + const result = await createEmailCampaign(validParams); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to create email campaign: name can't be blank" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts new file mode 100644 index 0000000..d784bec --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts @@ -0,0 +1,42 @@ +import deleteEmailCampaign from "../deleteEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + delete: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("deleteEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("deletes the campaign and returns a confirmation payload", async () => { + mockClient.emailCampaigns.delete.mockResolvedValue(undefined); + + const result = await deleteEmailCampaign({ email_campaign_id: 4567 }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.delete).toHaveBeenCalledWith(4567); + expect(result.content[0].text).toContain('"email_campaign_id": 4567'); + expect(result.content[0].text).toContain('"deleted": true'); + expect(result.isError).toBeUndefined(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.delete.mockRejectedValue(new Error("not found")); + + const result = await deleteEmailCampaign({ email_campaign_id: 99 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to delete email campaign: not found" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts new file mode 100644 index 0000000..01cb326 --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts @@ -0,0 +1,45 @@ +import getEmailCampaign from "../getEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + get: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("getEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("unwraps the data envelope and returns the campaign as JSON", async () => { + mockClient.emailCampaigns.get.mockResolvedValue({ + data: { id: 4567, name: "Spring Sale", current_state: "draft" }, + }); + + const result = await getEmailCampaign({ email_campaign_id: 4567 }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.get).toHaveBeenCalledWith(4567); + expect(result.content[0].text).toContain('"id": 4567'); + expect(result.content[0].text).toContain('"name": "Spring Sale"'); + expect(result.content[0].text).not.toContain('"data"'); + expect(result.isError).toBeUndefined(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.get.mockRejectedValue(new Error("not found")); + + const result = await getEmailCampaign({ email_campaign_id: 99 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to get email campaign: not found" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts b/src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts new file mode 100644 index 0000000..0d647e3 --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts @@ -0,0 +1,90 @@ +import getEmailCampaignStats from "../getEmailCampaignStats"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + getStats: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("getEmailCampaignStats", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("unwraps the data envelope and returns the stats as JSON", async () => { + mockClient.emailCampaigns.getStats.mockResolvedValue({ + data: { sent_count: 1500, delivery_count: 1450, open_rate: 0.5655 }, + }); + + const result = await getEmailCampaignStats({ email_campaign_id: 4567 }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.getStats).toHaveBeenCalledWith( + 4567, + undefined + ); + expect(result.content[0].text).toContain('"sent_count": 1500'); + expect(result.content[0].text).toContain('"open_rate": 0.5655'); + expect(result.isError).toBeUndefined(); + }); + + it("passes the date window to the client when provided", async () => { + mockClient.emailCampaigns.getStats.mockResolvedValue({ + data: { sent_count: 100 }, + }); + + const result = await getEmailCampaignStats({ + email_campaign_id: 4567, + start_date: "2026-05-01", + end_date: "2026-05-31", + }); + + expect(mockClient.emailCampaigns.getStats).toHaveBeenCalledWith(4567, { + start_date: "2026-05-01", + end_date: "2026-05-31", + }); + expect(result.isError).toBeUndefined(); + }); + + it("rejects dates that are not in YYYY-MM-DD format", async () => { + const result = await getEmailCampaignStats({ + email_campaign_id: 4567, + start_date: "invalid", + end_date: "2026-05-31T00:00:00Z", + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid input"); + expect(result.content[0].text).toContain("start_date"); + expect(result.content[0].text).toContain("end_date"); + expect(mockClient.emailCampaigns.getStats).not.toHaveBeenCalled(); + }); + + it("rejects an invalid campaign ID", async () => { + const result = await getEmailCampaignStats({ email_campaign_id: 0 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid input"); + expect(result.content[0].text).toContain("email_campaign_id"); + expect(mockClient.emailCampaigns.getStats).not.toHaveBeenCalled(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.getStats.mockRejectedValue( + new Error("not found") + ); + + const result = await getEmailCampaignStats({ email_campaign_id: 99 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to get email campaign stats: not found" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts b/src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts new file mode 100644 index 0000000..1c927a8 --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts @@ -0,0 +1,107 @@ +import listEmailCampaigns from "../listEmailCampaigns"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + getList: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("listEmailCampaigns", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("returns the campaigns with pagination as JSON", async () => { + mockClient.emailCampaigns.getList.mockResolvedValue({ + data: [ + { id: 4567, name: "Spring Sale", current_state: "draft" }, + { id: 4568, name: "Summer Sale", current_state: "scheduled" }, + ], + pagination: { token: 1, next_token: 2 }, + }); + + const result = await listEmailCampaigns({}); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.getList).toHaveBeenCalledWith(undefined); + expect(result.content[0].text).toContain('"id": 4567'); + expect(result.content[0].text).toContain('"name": "Spring Sale"'); + expect(result.content[0].text).toContain('"next_token": 2'); + expect(result.isError).toBeUndefined(); + }); + + it("passes pagination and search params to the client", async () => { + mockClient.emailCampaigns.getList.mockResolvedValue({ + data: [{ id: 4567, name: "Spring Sale" }], + pagination: { token: 2 }, + }); + + const result = await listEmailCampaigns({ + token: 2, + per_page: 10, + search: "spring", + }); + + expect(mockClient.emailCampaigns.getList).toHaveBeenCalledWith({ + token: 2, + per_page: 10, + search: "spring", + }); + expect(result.content[0].text).toContain('"name": "Spring Sale"'); + expect(result.isError).toBeUndefined(); + }); + + it("calls the client without options when no params are given", async () => { + mockClient.emailCampaigns.getList.mockResolvedValue({ data: [] }); + + await listEmailCampaigns(undefined); + + expect(mockClient.emailCampaigns.getList).toHaveBeenCalledWith(undefined); + }); + + it("rejects invalid input", async () => { + const result = await listEmailCampaigns({ per_page: 500 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid input"); + expect(mockClient.emailCampaigns.getList).not.toHaveBeenCalled(); + }); + + it("returns the empty message when no campaigns exist", async () => { + mockClient.emailCampaigns.getList.mockResolvedValue({ + data: [], + pagination: { token: 1 }, + }); + + const result = await listEmailCampaigns({}); + + expect(result.content[0].text).toBe( + "No email campaigns in your Mailtrap account." + ); + }); + + it("handles a null response", async () => { + mockClient.emailCampaigns.getList.mockResolvedValue(null); + + const result = await listEmailCampaigns({}); + + expect(result.content[0].text).toBe( + "No email campaigns in your Mailtrap account." + ); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.getList.mockRejectedValue(new Error("boom")); + + const result = await listEmailCampaigns({}); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe("Failed to list email campaigns: boom"); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts new file mode 100644 index 0000000..38df317 --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts @@ -0,0 +1,45 @@ +import resetEmailCampaign from "../resetEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + reset: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("resetEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("resets the campaign and returns the unwrapped campaign", async () => { + mockClient.emailCampaigns.reset.mockResolvedValue({ + data: { id: 4567, current_state: "draft" }, + }); + + const result = await resetEmailCampaign({ email_campaign_id: 4567 }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.reset).toHaveBeenCalledWith(4567); + expect(result.content[0].text).toContain('"current_state": "draft"'); + expect(result.isError).toBeUndefined(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.reset.mockRejectedValue( + new Error("Campaign is not scheduled") + ); + + const result = await resetEmailCampaign({ email_campaign_id: 4567 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to reset email campaign: Campaign is not scheduled" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts new file mode 100644 index 0000000..41f036e --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts @@ -0,0 +1,125 @@ +import scheduleEmailCampaign from "../scheduleEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + schedule: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +function isoDaysFromNow(days: number): string { + return new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString(); +} + +describe("scheduleEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("schedules the campaign and returns the unwrapped campaign", async () => { + const datetime = isoDaysFromNow(7); + mockClient.emailCampaigns.schedule.mockResolvedValue({ + data: { + id: 4567, + current_state: "scheduled", + current_state_metadata: { scheduled_at: datetime }, + }, + }); + + const result = await scheduleEmailCampaign({ + email_campaign_id: 4567, + datetime, + }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.schedule).toHaveBeenCalledWith(4567, { + datetime, + }); + expect(result.content[0].text).toContain('"current_state": "scheduled"'); + expect(result.content[0].text).toContain(`"scheduled_at": "${datetime}"`); + expect(result.isError).toBeUndefined(); + }); + + it("accepts an ISO 8601 datetime with a timezone offset", async () => { + const withOffset = isoDaysFromNow(7).replace("Z", "+02:00"); + mockClient.emailCampaigns.schedule.mockResolvedValue({ + data: { id: 4567, current_state: "scheduled" }, + }); + + const result = await scheduleEmailCampaign({ + email_campaign_id: 4567, + datetime: withOffset, + }); + + expect(mockClient.emailCampaigns.schedule).toHaveBeenCalledWith(4567, { + datetime: withOffset, + }); + expect(result.isError).toBeUndefined(); + }); + + it("rejects invalid input", async () => { + const result = await scheduleEmailCampaign({ email_campaign_id: 4567 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid input"); + expect(result.content[0].text).toContain("datetime"); + expect(mockClient.emailCampaigns.schedule).not.toHaveBeenCalled(); + }); + + it("rejects a datetime that is not ISO 8601", async () => { + const result = await scheduleEmailCampaign({ + email_campaign_id: 4567, + datetime: "tomorrow at noon", + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid input"); + expect(result.content[0].text).toContain("datetime"); + expect(mockClient.emailCampaigns.schedule).not.toHaveBeenCalled(); + }); + + it("rejects a datetime in the past", async () => { + const result = await scheduleEmailCampaign({ + email_campaign_id: 4567, + datetime: isoDaysFromNow(-1), + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("must be in the future"); + expect(mockClient.emailCampaigns.schedule).not.toHaveBeenCalled(); + }); + + it("rejects a datetime more than 1 month ahead", async () => { + const result = await scheduleEmailCampaign({ + email_campaign_id: 4567, + datetime: isoDaysFromNow(45), + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain( + "must be no more than 1 month ahead" + ); + expect(mockClient.emailCampaigns.schedule).not.toHaveBeenCalled(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.schedule.mockRejectedValue( + new Error("Cannot transition from 'started' to 'scheduled'") + ); + + const result = await scheduleEmailCampaign({ + email_campaign_id: 4567, + datetime: isoDaysFromNow(7), + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to schedule email campaign: Cannot transition from 'started' to 'scheduled'" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts new file mode 100644 index 0000000..78ddf8b --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts @@ -0,0 +1,45 @@ +import startEmailCampaign from "../startEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + start: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("startEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("starts the campaign and returns the unwrapped campaign", async () => { + mockClient.emailCampaigns.start.mockResolvedValue({ + data: { id: 4567, current_state: "started" }, + }); + + const result = await startEmailCampaign({ email_campaign_id: 4567 }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.start).toHaveBeenCalledWith(4567); + expect(result.content[0].text).toContain('"current_state": "started"'); + expect(result.isError).toBeUndefined(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.start.mockRejectedValue( + new Error("Campaign design can't be blank") + ); + + const result = await startEmailCampaign({ email_campaign_id: 4567 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to start email campaign: Campaign design can't be blank" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts new file mode 100644 index 0000000..d2dbb8a --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts @@ -0,0 +1,45 @@ +import terminateEmailCampaign from "../terminateEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + terminate: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("terminateEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("terminates the campaign and returns the unwrapped campaign", async () => { + mockClient.emailCampaigns.terminate.mockResolvedValue({ + data: { id: 4567, current_state: "terminating" }, + }); + + const result = await terminateEmailCampaign({ email_campaign_id: 4567 }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.terminate).toHaveBeenCalledWith(4567); + expect(result.content[0].text).toContain('"current_state": "terminating"'); + expect(result.isError).toBeUndefined(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.terminate.mockRejectedValue( + new Error("Cannot transition from 'draft' to 'terminating'") + ); + + const result = await terminateEmailCampaign({ email_campaign_id: 4567 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to terminate email campaign: Cannot transition from 'draft' to 'terminating'" + ); + }); +}); diff --git a/src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts new file mode 100644 index 0000000..5c6c5ee --- /dev/null +++ b/src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts @@ -0,0 +1,96 @@ +import updateEmailCampaign from "../updateEmailCampaign"; +import { requireClient } from "../../../client"; + +const mockClient = { + emailCampaigns: { + update: jest.fn(), + }, +}; + +jest.mock("../../../client", () => ({ + requireClient: jest.fn(() => mockClient), +})); + +describe("updateEmailCampaign", () => { + beforeEach(() => { + jest.clearAllMocks(); + (requireClient as jest.Mock).mockReturnValue(mockClient); + }); + + it("updates the campaign and returns the unwrapped campaign", async () => { + mockClient.emailCampaigns.update.mockResolvedValue({ + data: { id: 4567, name: "Spring Sale (updated)" }, + }); + + const result = await updateEmailCampaign({ + email_campaign_id: 4567, + name: "Spring Sale (updated)", + template_attributes: { + subject: "New subject", + body_html: "Hi", + }, + }); + + expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(mockClient.emailCampaigns.update).toHaveBeenCalledWith(4567, { + name: "Spring Sale (updated)", + template_attributes: { + subject: "New subject", + body_html: "Hi", + }, + }); + expect(result.content[0].text).toContain('"name": "Spring Sale (updated)"'); + expect(result.isError).toBeUndefined(); + }); + + it("accepts body_text: null to clear the text body", async () => { + mockClient.emailCampaigns.update.mockResolvedValue({ + data: { id: 4567, name: "Spring Sale" }, + }); + + const result = await updateEmailCampaign({ + email_campaign_id: 4567, + template_attributes: { body_text: null }, + }); + + expect(mockClient.emailCampaigns.update).toHaveBeenCalledWith(4567, { + template_attributes: { body_text: null }, + }); + expect(result.isError).toBeUndefined(); + }); + + it("rejects invalid input", async () => { + const result = await updateEmailCampaign({ name: "Missing ID" }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid input"); + expect(result.content[0].text).toContain("email_campaign_id"); + expect(mockClient.emailCampaigns.update).not.toHaveBeenCalled(); + }); + + it("rejects an update with no fields to change", async () => { + const result = await updateEmailCampaign({ email_campaign_id: 4567 }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain( + "Provide at least one field to update." + ); + expect(mockClient.emailCampaigns.update).not.toHaveBeenCalled(); + }); + + it("surfaces API errors", async () => { + mockClient.emailCampaigns.update.mockRejectedValue( + new Error("Campaign is not draft") + ); + + const result = await updateEmailCampaign({ + email_campaign_id: 4567, + name: "x", + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to update email campaign: Campaign is not draft" + ); + }); +}); diff --git a/src/tools/emailCampaigns/cancelEmailCampaign.ts b/src/tools/emailCampaigns/cancelEmailCampaign.ts new file mode 100644 index 0000000..3a041ad --- /dev/null +++ b/src/tools/emailCampaigns/cancelEmailCampaign.ts @@ -0,0 +1,28 @@ +import { requireClient } from "../../client"; +import { + CancelEmailCampaignRequest, + EmailCampaignsClient, +} from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; + +async function cancelEmailCampaign({ + email_campaign_id, +}: CancelEmailCampaignRequest): Promise { + try { + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.cancel(email_campaign_id); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("cancel email campaign", error); + } +} + +export default cancelEmailCampaign; diff --git a/src/tools/emailCampaigns/createEmailCampaign.ts b/src/tools/emailCampaigns/createEmailCampaign.ts new file mode 100644 index 0000000..a6327fb --- /dev/null +++ b/src/tools/emailCampaigns/createEmailCampaign.ts @@ -0,0 +1,35 @@ +import { requireClient } from "../../client"; +import { EmailCampaignsClient } from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; +import { createEmailCampaignZod } from "./schemas/createEmailCampaign"; + +async function createEmailCampaign(raw: unknown): Promise { + try { + const parsed = createEmailCampaignZod.safeParse(raw ?? {}); + if (!parsed.success) { + const msg = parsed.error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join("; "); + return { + content: [{ type: "text", text: `Invalid input: ${msg}` }], + isError: true, + }; + } + + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.create(parsed.data); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("create email campaign", error); + } +} + +export default createEmailCampaign; diff --git a/src/tools/emailCampaigns/deleteEmailCampaign.ts b/src/tools/emailCampaigns/deleteEmailCampaign.ts new file mode 100644 index 0000000..30cfdb2 --- /dev/null +++ b/src/tools/emailCampaigns/deleteEmailCampaign.ts @@ -0,0 +1,30 @@ +import { requireClient } from "../../client"; +import { + DeleteEmailCampaignRequest, + EmailCampaignsClient, +} from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; + +async function deleteEmailCampaign({ + email_campaign_id, +}: DeleteEmailCampaignRequest): Promise { + try { + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + await mailtrap.emailCampaigns.delete(email_campaign_id); + + return buildSuccessResponse( + JSON.stringify({ email_campaign_id, deleted: true }, null, 2) + ); + } catch (error) { + return buildErrorResponse("delete email campaign", error); + } +} + +export default deleteEmailCampaign; diff --git a/src/tools/emailCampaigns/getEmailCampaign.ts b/src/tools/emailCampaigns/getEmailCampaign.ts new file mode 100644 index 0000000..145f7d6 --- /dev/null +++ b/src/tools/emailCampaigns/getEmailCampaign.ts @@ -0,0 +1,28 @@ +import { requireClient } from "../../client"; +import { + EmailCampaignsClient, + GetEmailCampaignRequest, +} from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; + +async function getEmailCampaign({ + email_campaign_id, +}: GetEmailCampaignRequest): Promise { + try { + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.get(email_campaign_id); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("get email campaign", error); + } +} + +export default getEmailCampaign; diff --git a/src/tools/emailCampaigns/getEmailCampaignStats.ts b/src/tools/emailCampaigns/getEmailCampaignStats.ts new file mode 100644 index 0000000..46987a8 --- /dev/null +++ b/src/tools/emailCampaigns/getEmailCampaignStats.ts @@ -0,0 +1,49 @@ +import { requireClient } from "../../client"; +import { EmailCampaignsClient } from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; +import { getEmailCampaignStatsZod } from "./schemas/getEmailCampaignStats"; + +async function getEmailCampaignStats(raw: unknown): Promise { + try { +const parsed = getEmailCampaignStatsZod.safeParse(raw ?? {}); + if (!parsed.success) { + const msg = parsed.error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join("; "); + return { + content: [{ type: "text", text: `Invalid input: ${msg}` }], + isError: true, + }; + } + + const { + email_campaign_id: emailCampaignId, + start_date: startDate, + end_date: endDate, + } = parsed.data; + + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const params = { + ...(startDate !== undefined ? { start_date: startDate } : {}), + ...(endDate !== undefined ? { end_date: endDate } : {}), + }; + + const response = await mailtrap.emailCampaigns.getStats( + emailCampaignId, + Object.keys(params).length > 0 ? params : undefined + ); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("get email campaign stats", error); + } +} + +export default getEmailCampaignStats; diff --git a/src/tools/emailCampaigns/index.ts b/src/tools/emailCampaigns/index.ts new file mode 100644 index 0000000..523b4e4 --- /dev/null +++ b/src/tools/emailCampaigns/index.ts @@ -0,0 +1,47 @@ +import listEmailCampaignsSchema from "./schemas/listEmailCampaigns"; +import listEmailCampaigns from "./listEmailCampaigns"; +import getEmailCampaignSchema from "./schemas/getEmailCampaign"; +import getEmailCampaign from "./getEmailCampaign"; +import createEmailCampaignSchema from "./schemas/createEmailCampaign"; +import createEmailCampaign from "./createEmailCampaign"; +import updateEmailCampaignSchema from "./schemas/updateEmailCampaign"; +import updateEmailCampaign from "./updateEmailCampaign"; +import deleteEmailCampaignSchema from "./schemas/deleteEmailCampaign"; +import deleteEmailCampaign from "./deleteEmailCampaign"; +import startEmailCampaignSchema from "./schemas/startEmailCampaign"; +import startEmailCampaign from "./startEmailCampaign"; +import scheduleEmailCampaignSchema from "./schemas/scheduleEmailCampaign"; +import scheduleEmailCampaign from "./scheduleEmailCampaign"; +import cancelEmailCampaignSchema from "./schemas/cancelEmailCampaign"; +import cancelEmailCampaign from "./cancelEmailCampaign"; +import terminateEmailCampaignSchema from "./schemas/terminateEmailCampaign"; +import terminateEmailCampaign from "./terminateEmailCampaign"; +import resetEmailCampaignSchema from "./schemas/resetEmailCampaign"; +import resetEmailCampaign from "./resetEmailCampaign"; +import getEmailCampaignStatsSchema from "./schemas/getEmailCampaignStats"; +import getEmailCampaignStats from "./getEmailCampaignStats"; + +export { + listEmailCampaignsSchema, + listEmailCampaigns, + getEmailCampaignSchema, + getEmailCampaign, + createEmailCampaignSchema, + createEmailCampaign, + updateEmailCampaignSchema, + updateEmailCampaign, + deleteEmailCampaignSchema, + deleteEmailCampaign, + startEmailCampaignSchema, + startEmailCampaign, + scheduleEmailCampaignSchema, + scheduleEmailCampaign, + cancelEmailCampaignSchema, + cancelEmailCampaign, + terminateEmailCampaignSchema, + terminateEmailCampaign, + resetEmailCampaignSchema, + resetEmailCampaign, + getEmailCampaignStatsSchema, + getEmailCampaignStats, +}; diff --git a/src/tools/emailCampaigns/listEmailCampaigns.ts b/src/tools/emailCampaigns/listEmailCampaigns.ts new file mode 100644 index 0000000..5b2c597 --- /dev/null +++ b/src/tools/emailCampaigns/listEmailCampaigns.ts @@ -0,0 +1,50 @@ +import { requireClient } from "../../client"; +import { + EmailCampaignsClient, + EmailCampaignListResponse, +} from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; +import { listEmailCampaignsZod } from "./schemas/listEmailCampaigns"; + +async function listEmailCampaigns(raw: unknown): Promise { + try { + const parsed = listEmailCampaignsZod.safeParse(raw ?? {}); + if (!parsed.success) { + const msg = parsed.error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join("; "); + return { + content: [{ type: "text", text: `Invalid input: ${msg}` }], + isError: true, + }; + } + + const params = parsed.data; + + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = (await mailtrap.emailCampaigns.getList( + Object.keys(params).length > 0 ? params : undefined + )) as EmailCampaignListResponse | null | undefined; + + const campaigns = response?.data ?? []; + + if (campaigns.length === 0) { + return buildSuccessResponse( + "No email campaigns in your Mailtrap account." + ); + } + + return buildSuccessResponse(JSON.stringify(response, null, 2)); + } catch (error) { + return buildErrorResponse("list email campaigns", error); + } +} + +export default listEmailCampaigns; diff --git a/src/tools/emailCampaigns/resetEmailCampaign.ts b/src/tools/emailCampaigns/resetEmailCampaign.ts new file mode 100644 index 0000000..4e37d7e --- /dev/null +++ b/src/tools/emailCampaigns/resetEmailCampaign.ts @@ -0,0 +1,28 @@ +import { requireClient } from "../../client"; +import { + EmailCampaignsClient, + ResetEmailCampaignRequest, +} from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; + +async function resetEmailCampaign({ + email_campaign_id, +}: ResetEmailCampaignRequest): Promise { + try { + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.reset(email_campaign_id); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("reset email campaign", error); + } +} + +export default resetEmailCampaign; diff --git a/src/tools/emailCampaigns/scheduleEmailCampaign.ts b/src/tools/emailCampaigns/scheduleEmailCampaign.ts new file mode 100644 index 0000000..07d9108 --- /dev/null +++ b/src/tools/emailCampaigns/scheduleEmailCampaign.ts @@ -0,0 +1,39 @@ +import { requireClient } from "../../client"; +import { EmailCampaignsClient } from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; +import { scheduleEmailCampaignZod } from "./schemas/scheduleEmailCampaign"; + +async function scheduleEmailCampaign(raw: unknown): Promise { + try { + const parsed = scheduleEmailCampaignZod.safeParse(raw ?? {}); + if (!parsed.success) { + const msg = parsed.error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join("; "); + return { + content: [{ type: "text", text: `Invalid input: ${msg}` }], + isError: true, + }; + } + + const { email_campaign_id: emailCampaignId, datetime } = parsed.data; + + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.schedule(emailCampaignId, { + datetime, + }); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("schedule email campaign", error); + } +} + +export default scheduleEmailCampaign; diff --git a/src/tools/emailCampaigns/schemas/cancelEmailCampaign.ts b/src/tools/emailCampaigns/schemas/cancelEmailCampaign.ts new file mode 100644 index 0000000..c84f0be --- /dev/null +++ b/src/tools/emailCampaigns/schemas/cancelEmailCampaign.ts @@ -0,0 +1,14 @@ +const cancelEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to cancel.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, +}; + +export default cancelEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/createEmailCampaign.ts b/src/tools/emailCampaigns/schemas/createEmailCampaign.ts new file mode 100644 index 0000000..c5d9d4b --- /dev/null +++ b/src/tools/emailCampaigns/schemas/createEmailCampaign.ts @@ -0,0 +1,143 @@ +import { z } from "zod"; + +const createEmailCampaignSchema = { + type: "object", + properties: { + name: { + type: "string", + description: "Campaign name.", + }, + domain_id: { + type: "integer", + minimum: 1, + description: + "ID of the verified sending domain used for the campaign, as returned by the Sending Domains endpoints.", + }, + from_local_part: { + type: "string", + description: "Local part (before the @) of the From address.", + }, + from_display_name: { + type: "string", + description: "Display name shown in the From header.", + }, + reply_to: { + type: "object", + description: "Reply-To address parts.", + properties: { + display_name: { + type: "string", + description: "Reply-To display name.", + }, + local_part: { + type: "string", + description: "Local part (before the @) of the Reply-To address.", + }, + domain: { + type: "string", + description: "Domain of the Reply-To address.", + }, + }, + additionalProperties: false, + }, + template_attributes: { + type: "object", + description: "Inline email template — the campaign's subject and design.", + properties: { + subject: { + type: "string", + description: + "Email subject line (max 255 chars). Required when creating a campaign. Supports merge tags, e.g. `Hi {{first_name}}`.", + }, + body_html: { + type: "string", + description: + "HTML body of the email (the design). Optional for a draft; required before the campaign can be scheduled or started. Include an unsubscribe link via an anchor whose `href` contains the `__unsubscribe_url__` placeholder. Supports `{{tag_name}}` merge tags.", + }, + body_text: { + type: "string", + description: + "Optional plain-text alternative of the email body. Supports the same `__unsubscribe_url__` placeholder and `{{tag_name}}` merge tags as `body_html`.", + }, + merge_tags: { + type: "array", + items: { type: "string" }, + description: + 'Bare names of the merge tags referenced in the subject/body, without the `{{ }}` delimiters — e.g. `["first_name"]`.', + }, + }, + required: ["subject"], + additionalProperties: false, + }, + delivery_mode: { + type: "string", + enum: ["rapid", "gradual"], + description: + "How the campaign is delivered. `rapid` sends as fast as possible; `gradual` throttles sending to `delivery_options.emails_per_hour`.", + }, + delivery_options: { + type: "object", + description: + "Delivery throttling options. Applies when `delivery_mode` is `gradual`.", + properties: { + emails_per_hour: { + type: ["integer", "null"], + description: "Maximum number of emails sent per hour.", + }, + }, + additionalProperties: false, + }, + contact_list_ids: { + type: "array", + items: { type: "integer" }, + description: + "IDs of contact lists to send to. Treated as the full set of included lists. Combine with `contact_segment_ids` to target both.", + }, + contact_segment_ids: { + type: "array", + items: { type: "integer" }, + description: + "IDs of contact segments to send to. Treated as the full set of included segments.", + }, + }, + required: ["name", "domain_id", "from_local_part", "template_attributes"], + additionalProperties: false, +}; + +const replyToZod = z + .object({ + display_name: z.string().optional(), + local_part: z.string().optional(), + domain: z.string().optional(), + }) + .strict(); + +const deliveryOptionsZod = z + .object({ + emails_per_hour: z.number().int().nullable().optional(), + }) + .strict(); + +export const createEmailCampaignZod = z + .object({ + name: z.string(), + domain_id: z.number().int().min(1), + from_local_part: z.string(), + from_display_name: z.string().optional(), + reply_to: replyToZod.optional(), + template_attributes: z + .object({ + subject: z.string(), + body_html: z.string().optional(), + body_text: z.string().optional(), + merge_tags: z.array(z.string()).optional(), + }) + .strict(), + delivery_mode: z.enum(["rapid", "gradual"]).optional(), + delivery_options: deliveryOptionsZod.optional(), + contact_list_ids: z.array(z.number().int()).optional(), + contact_segment_ids: z.array(z.number().int()).optional(), + }) + .strict(); + +export default createEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/deleteEmailCampaign.ts b/src/tools/emailCampaigns/schemas/deleteEmailCampaign.ts new file mode 100644 index 0000000..78e7694 --- /dev/null +++ b/src/tools/emailCampaigns/schemas/deleteEmailCampaign.ts @@ -0,0 +1,14 @@ +const deleteEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to delete.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, +}; + +export default deleteEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/getEmailCampaign.ts b/src/tools/emailCampaigns/schemas/getEmailCampaign.ts new file mode 100644 index 0000000..fad470a --- /dev/null +++ b/src/tools/emailCampaigns/schemas/getEmailCampaign.ts @@ -0,0 +1,14 @@ +const getEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to fetch.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, +}; + +export default getEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/getEmailCampaignStats.ts b/src/tools/emailCampaigns/schemas/getEmailCampaignStats.ts new file mode 100644 index 0000000..4de574c --- /dev/null +++ b/src/tools/emailCampaigns/schemas/getEmailCampaignStats.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + +const getEmailCampaignStatsSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign.", + }, + start_date: { + type: "string", + pattern: "^\\d{4}-\\d{2}-\\d{2}$", + description: + "Start of the aggregation window (inclusive), in `YYYY-MM-DD` format. Defaults to the day the campaign was last started.", + }, + end_date: { + type: "string", + pattern: "^\\d{4}-\\d{2}-\\d{2}$", + description: + "End of the aggregation window (inclusive), in `YYYY-MM-DD` format. Defaults to the current date.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, +}; + +export const getEmailCampaignStatsZod = z + .object({ + email_campaign_id: z.number().int().min(1), + start_date: z + .string() + .regex(DATE_PATTERN, "must be in YYYY-MM-DD format") + .optional(), + end_date: z + .string() + .regex(DATE_PATTERN, "must be in YYYY-MM-DD format") + .optional(), + }) + .strict(); + +export default getEmailCampaignStatsSchema; diff --git a/src/tools/emailCampaigns/schemas/listEmailCampaigns.ts b/src/tools/emailCampaigns/schemas/listEmailCampaigns.ts new file mode 100644 index 0000000..65f7657 --- /dev/null +++ b/src/tools/emailCampaigns/schemas/listEmailCampaigns.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +const listEmailCampaignsSchema = { + type: "object", + properties: { + token: { + type: "integer", + minimum: 1, + description: + "Page number to retrieve (page-token pagination). Defaults to `1`.", + }, + per_page: { + type: "integer", + minimum: 1, + maximum: 100, + description: "Number of campaigns per page. Defaults to 50, maximum 100.", + }, + search: { + type: "string", + description: + 'Filter campaigns by name (case-insensitive partial match), e.g. "spring".', + }, + }, + required: [], + additionalProperties: false, +}; + +export const listEmailCampaignsZod = z + .object({ + token: z.number().int().min(1).optional(), + per_page: z.number().int().min(1).max(100).optional(), + search: z.string().optional(), + }) + .strict(); + +export default listEmailCampaignsSchema; diff --git a/src/tools/emailCampaigns/schemas/resetEmailCampaign.ts b/src/tools/emailCampaigns/schemas/resetEmailCampaign.ts new file mode 100644 index 0000000..58eab7b --- /dev/null +++ b/src/tools/emailCampaigns/schemas/resetEmailCampaign.ts @@ -0,0 +1,14 @@ +const resetEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to reset.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, +}; + +export default resetEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/scheduleEmailCampaign.ts b/src/tools/emailCampaigns/schemas/scheduleEmailCampaign.ts new file mode 100644 index 0000000..d85c0a1 --- /dev/null +++ b/src/tools/emailCampaigns/schemas/scheduleEmailCampaign.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +const scheduleEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to schedule.", + }, + datetime: { + type: "string", + description: + "When to send the campaign (ISO 8601, e.g. `2026-09-01T09:00:00Z` or with a UTC offset). Must be in the future and no more than 1 month ahead, otherwise the request is rejected with `422`.", + }, + }, + required: ["email_campaign_id", "datetime"], + additionalProperties: false, +}; + +function oneMonthFromNow(): Date { + const limit = new Date(); + limit.setMonth(limit.getMonth() + 1); + return limit; +} + +export const scheduleEmailCampaignZod = z + .object({ + email_campaign_id: z.number().int().min(1), + datetime: z + .string() + .datetime({ offset: true }) + .refine((value) => new Date(value) > new Date(), { + message: "must be in the future", + }) + .refine((value) => new Date(value) <= oneMonthFromNow(), { + message: "must be no more than 1 month ahead", + }), + }) + .strict(); + +export default scheduleEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/startEmailCampaign.ts b/src/tools/emailCampaigns/schemas/startEmailCampaign.ts new file mode 100644 index 0000000..e19f47a --- /dev/null +++ b/src/tools/emailCampaigns/schemas/startEmailCampaign.ts @@ -0,0 +1,14 @@ +const startEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to start.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, +}; + +export default startEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/terminateEmailCampaign.ts b/src/tools/emailCampaigns/schemas/terminateEmailCampaign.ts new file mode 100644 index 0000000..b531c82 --- /dev/null +++ b/src/tools/emailCampaigns/schemas/terminateEmailCampaign.ts @@ -0,0 +1,14 @@ +const terminateEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to terminate.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, +}; + +export default terminateEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/schemas/updateEmailCampaign.ts b/src/tools/emailCampaigns/schemas/updateEmailCampaign.ts new file mode 100644 index 0000000..46e6128 --- /dev/null +++ b/src/tools/emailCampaigns/schemas/updateEmailCampaign.ts @@ -0,0 +1,155 @@ +import { z } from "zod"; + +const updateEmailCampaignSchema = { + type: "object", + properties: { + email_campaign_id: { + type: "integer", + minimum: 1, + description: "Unique identifier of the email campaign to update.", + }, + name: { + type: "string", + description: "Campaign name.", + }, + domain_id: { + type: "integer", + minimum: 1, + description: + "ID of the verified sending domain used for the campaign, as returned by the Sending Domains endpoints.", + }, + from_local_part: { + type: "string", + description: "Local part (before the @) of the From address.", + }, + from_display_name: { + type: "string", + description: "Display name shown in the From header.", + }, + reply_to: { + type: "object", + description: "Reply-To address parts.", + properties: { + display_name: { + type: "string", + description: "Reply-To display name.", + }, + local_part: { + type: "string", + description: "Local part (before the @) of the Reply-To address.", + }, + domain: { + type: "string", + description: "Domain of the Reply-To address.", + }, + }, + additionalProperties: false, + }, + template_attributes: { + type: "object", + description: + "Inline email template — the campaign's template is edited in place. Updates are partial: only the sub-fields you provide change.", + properties: { + subject: { + type: "string", + description: + "Email subject line (max 255 chars). Supports merge tags, e.g. `Hi {{first_name}}`.", + }, + body_html: { + type: "string", + description: + "HTML body of the email (the design). Required before the campaign can be scheduled or started. Include an unsubscribe link via an anchor whose `href` contains the `__unsubscribe_url__` placeholder. Supports `{{tag_name}}` merge tags.", + }, + body_text: { + type: ["string", "null"], + description: + "Optional plain-text alternative of the email body. Pass `null` to clear it. Supports the same `__unsubscribe_url__` placeholder and `{{tag_name}}` merge tags as `body_html`.", + }, + merge_tags: { + type: "array", + items: { type: "string" }, + description: + 'Bare names of the merge tags referenced in the subject/body, without the `{{ }}` delimiters — e.g. `["first_name"]`. Replaced as a whole when provided.', + }, + }, + additionalProperties: false, + }, + delivery_mode: { + type: "string", + enum: ["rapid", "gradual"], + description: + "How the campaign is delivered. `rapid` sends as fast as possible; `gradual` throttles sending to `delivery_options.emails_per_hour`.", + }, + delivery_options: { + type: "object", + description: + "Delivery throttling options. Applies when `delivery_mode` is `gradual`.", + properties: { + emails_per_hour: { + type: ["integer", "null"], + description: "Maximum number of emails sent per hour.", + }, + }, + additionalProperties: false, + }, + contact_list_ids: { + type: "array", + items: { type: "integer" }, + description: + "IDs of contact lists to send to. Treated as the full set of included lists — lists not listed are removed.", + }, + contact_segment_ids: { + type: "array", + items: { type: "integer" }, + description: + "IDs of contact segments to send to. Treated as the full set of included segments.", + }, + }, + required: ["email_campaign_id"], + additionalProperties: false, + description: + "At least one field besides `email_campaign_id` must be provided.", +}; + +export const updateEmailCampaignZod = z + .object({ + email_campaign_id: z.number().int().min(1), + name: z.string().optional(), + domain_id: z.number().int().min(1).optional(), + from_local_part: z.string().optional(), + from_display_name: z.string().optional(), + reply_to: z + .object({ + display_name: z.string().optional(), + local_part: z.string().optional(), + domain: z.string().optional(), + }) + .strict() + .optional(), + template_attributes: z + .object({ + subject: z.string().optional(), + body_html: z.string().optional(), + body_text: z.string().nullable().optional(), + merge_tags: z.array(z.string()).optional(), + }) + .strict() + .optional(), + delivery_mode: z.enum(["rapid", "gradual"]).optional(), + delivery_options: z + .object({ + emails_per_hour: z.number().int().nullable().optional(), + }) + .strict() + .optional(), + contact_list_ids: z.array(z.number().int()).optional(), + contact_segment_ids: z.array(z.number().int()).optional(), + }) + .strict() + .refine( + ({ email_campaign_id: _emailCampaignId, ...updates }) => + Object.keys(updates).length > 0, + { message: "Provide at least one field to update." } + ); + +export default updateEmailCampaignSchema; diff --git a/src/tools/emailCampaigns/startEmailCampaign.ts b/src/tools/emailCampaigns/startEmailCampaign.ts new file mode 100644 index 0000000..d11c583 --- /dev/null +++ b/src/tools/emailCampaigns/startEmailCampaign.ts @@ -0,0 +1,28 @@ +import { requireClient } from "../../client"; +import { + EmailCampaignsClient, + StartEmailCampaignRequest, +} from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; + +async function startEmailCampaign({ + email_campaign_id, +}: StartEmailCampaignRequest): Promise { + try { + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.start(email_campaign_id); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("start email campaign", error); + } +} + +export default startEmailCampaign; diff --git a/src/tools/emailCampaigns/terminateEmailCampaign.ts b/src/tools/emailCampaigns/terminateEmailCampaign.ts new file mode 100644 index 0000000..67fa94d --- /dev/null +++ b/src/tools/emailCampaigns/terminateEmailCampaign.ts @@ -0,0 +1,28 @@ +import { requireClient } from "../../client"; +import { + EmailCampaignsClient, + TerminateEmailCampaignRequest, +} from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; + +async function terminateEmailCampaign({ + email_campaign_id, +}: TerminateEmailCampaignRequest): Promise { + try { + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.terminate(email_campaign_id); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("terminate email campaign", error); + } +} + +export default terminateEmailCampaign; diff --git a/src/tools/emailCampaigns/updateEmailCampaign.ts b/src/tools/emailCampaigns/updateEmailCampaign.ts new file mode 100644 index 0000000..672cbbe --- /dev/null +++ b/src/tools/emailCampaigns/updateEmailCampaign.ts @@ -0,0 +1,40 @@ +import { requireClient } from "../../client"; +import { EmailCampaignsClient } from "../../types/mailtrap"; +import { + buildErrorResponse, + buildSuccessResponse, + ToolResponse, +} from "../utils/responses"; +import { updateEmailCampaignZod } from "./schemas/updateEmailCampaign"; + +async function updateEmailCampaign(raw: unknown): Promise { + try { + const parsed = updateEmailCampaignZod.safeParse(raw ?? {}); + if (!parsed.success) { + const msg = parsed.error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join("; "); + return { + content: [{ type: "text", text: `Invalid input: ${msg}` }], + isError: true, + }; + } + + const { email_campaign_id: emailCampaignId, ...params } = parsed.data; + + const mailtrap = requireClient( + "email campaigns" + ) as unknown as EmailCampaignsClient; + + const response = await mailtrap.emailCampaigns.update( + emailCampaignId, + params + ); + + return buildSuccessResponse(JSON.stringify(response.data, null, 2)); + } catch (error) { + return buildErrorResponse("update email campaign", error); + } +} + +export default updateEmailCampaign; diff --git a/src/types/mailtrap.ts b/src/types/mailtrap.ts index 09a783d..f65d7be 100644 --- a/src/types/mailtrap.ts +++ b/src/types/mailtrap.ts @@ -628,6 +628,214 @@ export interface GetContactExportRequest { export_id: number; } +// --- Email campaign types --- + +export type EmailCampaignDeliveryMode = "rapid" | "gradual"; + +export type EmailCampaignState = + | "draft" + | "scheduled" + | "started" + | "queued" + | "paused" + | "terminating" + | "under_review" + | "finished" + | "failed" + | "failed_immediately"; + +export interface EmailCampaignReplyTo { + display_name?: string; + local_part?: string; + domain?: string; +} + +export interface EmailCampaignTemplateAttributes { + subject?: string; + body_html?: string; + body_text?: string | null; + merge_tags?: string[]; +} + +export interface EmailCampaignDeliveryOptions { + emails_per_hour?: number | null; +} + +export interface EmailCampaignStateError { + message?: string; + rcpt_index?: number; +} + +export interface EmailCampaignStateMetadata { + reason?: string; + error?: string; + errors?: EmailCampaignStateError[]; + scheduled_at?: string; +} + +export interface EmailCampaignTemplate { + id?: number; + subject?: string; + merge_tags?: string[]; + body_html?: string | null; + body_text?: string | null; +} + +export interface EmailCampaign { + id: number; + domain_id: number; + domain_name?: string; + name: string; + from_local_part?: string; + from_display_name?: string; + reply_to?: EmailCampaignReplyTo; + current_state: EmailCampaignState; + current_state_metadata?: EmailCampaignStateMetadata; + created_at?: string; + updated_at?: string; + last_started_at?: string | null; + last_started_at_date?: string; + recipient_total_count?: number | null; + contact_list_ids?: number[]; + contact_segment_ids?: number[]; + delivery_mode?: EmailCampaignDeliveryMode; + delivery_options?: EmailCampaignDeliveryOptions; + template?: EmailCampaignTemplate; +} + +export interface EmailCampaignPagination { + token?: number; + prev_token?: number | null; + next_token?: number | null; + first_url?: string; + prev_url?: string | null; + current_url?: string; + next_url?: string | null; +} + +export interface EmailCampaignListResponse { + data: EmailCampaign[]; + pagination?: EmailCampaignPagination; +} + +export interface EmailCampaignStats { + delivery_count: number; + open_count: number; + click_count: number; + bounce_count: number; + unsubscription_count: number; + sent_count: number; + spam_count: number; + delivery_rate: number; + open_rate: number; + click_rate: number; + bounce_rate: number; + spam_rate: number; + unsubscription_rate: number; +} + +export interface ListEmailCampaignsParams { + token?: number; + per_page?: number; + search?: string; +} + +export interface CreateEmailCampaignRequest { + name: string; + domain_id: number; + from_local_part: string; + template_attributes: EmailCampaignTemplateAttributes & { subject: string }; + from_display_name?: string; + reply_to?: EmailCampaignReplyTo; + delivery_mode?: EmailCampaignDeliveryMode; + delivery_options?: EmailCampaignDeliveryOptions; + contact_list_ids?: number[]; + contact_segment_ids?: number[]; +} + +export interface UpdateEmailCampaignParams { + name?: string; + domain_id?: number; + from_local_part?: string; + from_display_name?: string; + reply_to?: EmailCampaignReplyTo; + template_attributes?: EmailCampaignTemplateAttributes; + delivery_mode?: EmailCampaignDeliveryMode; + delivery_options?: EmailCampaignDeliveryOptions; + contact_list_ids?: number[]; + contact_segment_ids?: number[]; +} + +export interface UpdateEmailCampaignRequest extends UpdateEmailCampaignParams { + email_campaign_id: number; +} + +export interface GetEmailCampaignRequest { + email_campaign_id: number; +} + +export interface DeleteEmailCampaignRequest { + email_campaign_id: number; +} + +export interface StartEmailCampaignRequest { + email_campaign_id: number; +} + +export interface ScheduleEmailCampaignRequest { + email_campaign_id: number; + datetime: string; +} + +export interface CancelEmailCampaignRequest { + email_campaign_id: number; +} + +export interface TerminateEmailCampaignRequest { + email_campaign_id: number; +} + +export interface ResetEmailCampaignRequest { + email_campaign_id: number; +} + +export interface GetEmailCampaignStatsRequest { + email_campaign_id: number; + start_date?: string; + end_date?: string; +} + +// TODO(MT-22401): drop this interface and the per-handler casts once the +// mailtrap npm package ships the emailCampaigns resource. +export interface EmailCampaignsClient { + emailCampaigns: { + getList( + params?: ListEmailCampaignsParams + ): Promise; + get(emailCampaignId: number): Promise<{ data: EmailCampaign }>; + create( + params: CreateEmailCampaignRequest + ): Promise<{ data: EmailCampaign }>; + update( + emailCampaignId: number, + params: UpdateEmailCampaignParams + ): Promise<{ data: EmailCampaign }>; + delete(emailCampaignId: number): Promise; + start(emailCampaignId: number): Promise<{ data: EmailCampaign }>; + schedule( + emailCampaignId: number, + params: { datetime: string } + ): Promise<{ data: EmailCampaign }>; + cancel(emailCampaignId: number): Promise<{ data: EmailCampaign }>; + terminate(emailCampaignId: number): Promise<{ data: EmailCampaign }>; + reset(emailCampaignId: number): Promise<{ data: EmailCampaign }>; + getStats( + emailCampaignId: number, + params?: { start_date?: string; end_date?: string } + ): Promise<{ data: EmailCampaignStats }>; + }; +} + // --- General / account-admin types --- export interface MailtrapAccount { From fdcd44b09e65f6c489d71cb69f1e30dd0ac10479 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Fri, 31 Jul 2026 11:26:59 +0200 Subject: [PATCH 2/2] MT-22401: Drop MAILTRAP_ACCOUNT_ID requirement from campaign tools Decisions: - The email campaigns endpoints are token-scoped, so the handlers now pass requireAccountId: false (listAccounts precedent), matching the same fix in the Python and Node.js SDK PRs --- CLAUDE.md | 2 +- README.md | 2 +- .../emailCampaigns/__tests__/cancelEmailCampaign.test.ts | 4 +++- .../emailCampaigns/__tests__/createEmailCampaign.test.ts | 4 +++- .../emailCampaigns/__tests__/deleteEmailCampaign.test.ts | 4 +++- .../emailCampaigns/__tests__/getEmailCampaign.test.ts | 4 +++- .../__tests__/getEmailCampaignStats.test.ts | 4 +++- .../emailCampaigns/__tests__/listEmailCampaigns.test.ts | 4 +++- .../emailCampaigns/__tests__/resetEmailCampaign.test.ts | 4 +++- .../__tests__/scheduleEmailCampaign.test.ts | 4 +++- .../emailCampaigns/__tests__/startEmailCampaign.test.ts | 4 +++- .../__tests__/terminateEmailCampaign.test.ts | 4 +++- .../emailCampaigns/__tests__/updateEmailCampaign.test.ts | 4 +++- src/tools/emailCampaigns/cancelEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/createEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/deleteEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/getEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/getEmailCampaignStats.ts | 8 ++++---- src/tools/emailCampaigns/listEmailCampaigns.ts | 6 +++--- src/tools/emailCampaigns/resetEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/scheduleEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/startEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/terminateEmailCampaign.ts | 6 +++--- src/tools/emailCampaigns/updateEmailCampaign.ts | 6 +++--- 24 files changed, 69 insertions(+), 47 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fbb7cde..689be67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ Schema files define a JSON Schema–shaped object for MCP; optional Zod schemas ### Environment Variables Required - `MAILTRAP_API_TOKEN`: Required API token from Mailtrap -- `MAILTRAP_ACCOUNT_ID`: Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for send-email and send-sandbox-email. +- `MAILTRAP_ACCOUNT_ID`: Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for send-email, send-sandbox-email, and the email campaign tools. - `DEFAULT_FROM_EMAIL`: Optional. Default sender email when the tool does not receive a `from` parameter (send-email, send-sandbox-email). - `MAILTRAP_TEST_INBOX_ID`: Optional. Default test inbox ID for sandbox tools when the tool does not receive a `test_inbox_id` parameter. Enables switching inboxes per call via parameters. diff --git a/README.md b/README.md index 3b61350..ef83119 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Before using this MCP server, you need to: **Required Environment Variables:** - `MAILTRAP_API_TOKEN` - Required for all functionality -- `MAILTRAP_ACCOUNT_ID` - Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for the send tools (send-email, send-sandbox-email, and the batch-send-\* tools). +- `MAILTRAP_ACCOUNT_ID` - Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for the send tools (send-email, send-sandbox-email, and the batch-send-\* tools) and the email campaign tools. **Optional (can be passed as tool parameters instead):** diff --git a/src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts index 6ed37b5..3156627 100644 --- a/src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/cancelEmailCampaign.test.ts @@ -24,7 +24,9 @@ describe("cancelEmailCampaign", () => { const result = await cancelEmailCampaign({ email_campaign_id: 4567 }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.cancel).toHaveBeenCalledWith(4567); expect(result.content[0].text).toContain('"current_state": "draft"'); expect(result.isError).toBeUndefined(); diff --git a/src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts index 9232999..e3a07ba 100644 --- a/src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/createEmailCampaign.test.ts @@ -31,7 +31,9 @@ describe("createEmailCampaign", () => { const result = await createEmailCampaign(validParams); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.create).toHaveBeenCalledWith(validParams); expect(result.content[0].text).toContain('"id": 4567'); expect(result.content[0].text).toContain('"current_state": "draft"'); diff --git a/src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts index d784bec..5b210b1 100644 --- a/src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/deleteEmailCampaign.test.ts @@ -22,7 +22,9 @@ describe("deleteEmailCampaign", () => { const result = await deleteEmailCampaign({ email_campaign_id: 4567 }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.delete).toHaveBeenCalledWith(4567); expect(result.content[0].text).toContain('"email_campaign_id": 4567'); expect(result.content[0].text).toContain('"deleted": true'); diff --git a/src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts index 01cb326..846d6a0 100644 --- a/src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/getEmailCampaign.test.ts @@ -24,7 +24,9 @@ describe("getEmailCampaign", () => { const result = await getEmailCampaign({ email_campaign_id: 4567 }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.get).toHaveBeenCalledWith(4567); expect(result.content[0].text).toContain('"id": 4567'); expect(result.content[0].text).toContain('"name": "Spring Sale"'); diff --git a/src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts b/src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts index 0d647e3..ea505f3 100644 --- a/src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts +++ b/src/tools/emailCampaigns/__tests__/getEmailCampaignStats.test.ts @@ -24,7 +24,9 @@ describe("getEmailCampaignStats", () => { const result = await getEmailCampaignStats({ email_campaign_id: 4567 }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.getStats).toHaveBeenCalledWith( 4567, undefined diff --git a/src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts b/src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts index 1c927a8..1bc0e2c 100644 --- a/src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts +++ b/src/tools/emailCampaigns/__tests__/listEmailCampaigns.test.ts @@ -28,7 +28,9 @@ describe("listEmailCampaigns", () => { const result = await listEmailCampaigns({}); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.getList).toHaveBeenCalledWith(undefined); expect(result.content[0].text).toContain('"id": 4567'); expect(result.content[0].text).toContain('"name": "Spring Sale"'); diff --git a/src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts index 38df317..a5caa6e 100644 --- a/src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/resetEmailCampaign.test.ts @@ -24,7 +24,9 @@ describe("resetEmailCampaign", () => { const result = await resetEmailCampaign({ email_campaign_id: 4567 }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.reset).toHaveBeenCalledWith(4567); expect(result.content[0].text).toContain('"current_state": "draft"'); expect(result.isError).toBeUndefined(); diff --git a/src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts index 41f036e..b4607f8 100644 --- a/src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/scheduleEmailCampaign.test.ts @@ -36,7 +36,9 @@ describe("scheduleEmailCampaign", () => { datetime, }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.schedule).toHaveBeenCalledWith(4567, { datetime, }); diff --git a/src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts index 78ddf8b..bb65697 100644 --- a/src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/startEmailCampaign.test.ts @@ -24,7 +24,9 @@ describe("startEmailCampaign", () => { const result = await startEmailCampaign({ email_campaign_id: 4567 }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.start).toHaveBeenCalledWith(4567); expect(result.content[0].text).toContain('"current_state": "started"'); expect(result.isError).toBeUndefined(); diff --git a/src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts index d2dbb8a..bfb12d3 100644 --- a/src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/terminateEmailCampaign.test.ts @@ -24,7 +24,9 @@ describe("terminateEmailCampaign", () => { const result = await terminateEmailCampaign({ email_campaign_id: 4567 }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.terminate).toHaveBeenCalledWith(4567); expect(result.content[0].text).toContain('"current_state": "terminating"'); expect(result.isError).toBeUndefined(); diff --git a/src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts b/src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts index 5c6c5ee..4adbec5 100644 --- a/src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts +++ b/src/tools/emailCampaigns/__tests__/updateEmailCampaign.test.ts @@ -31,7 +31,9 @@ describe("updateEmailCampaign", () => { }, }); - expect(requireClient).toHaveBeenCalledWith("email campaigns"); + expect(requireClient).toHaveBeenCalledWith("email campaigns", { + requireAccountId: false, + }); expect(mockClient.emailCampaigns.update).toHaveBeenCalledWith(4567, { name: "Spring Sale (updated)", template_attributes: { diff --git a/src/tools/emailCampaigns/cancelEmailCampaign.ts b/src/tools/emailCampaigns/cancelEmailCampaign.ts index 3a041ad..3c54b84 100644 --- a/src/tools/emailCampaigns/cancelEmailCampaign.ts +++ b/src/tools/emailCampaigns/cancelEmailCampaign.ts @@ -13,9 +13,9 @@ async function cancelEmailCampaign({ email_campaign_id, }: CancelEmailCampaignRequest): Promise { try { - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.cancel(email_campaign_id); diff --git a/src/tools/emailCampaigns/createEmailCampaign.ts b/src/tools/emailCampaigns/createEmailCampaign.ts index a6327fb..06d9553 100644 --- a/src/tools/emailCampaigns/createEmailCampaign.ts +++ b/src/tools/emailCampaigns/createEmailCampaign.ts @@ -20,9 +20,9 @@ async function createEmailCampaign(raw: unknown): Promise { }; } - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.create(parsed.data); diff --git a/src/tools/emailCampaigns/deleteEmailCampaign.ts b/src/tools/emailCampaigns/deleteEmailCampaign.ts index 30cfdb2..d04a8d0 100644 --- a/src/tools/emailCampaigns/deleteEmailCampaign.ts +++ b/src/tools/emailCampaigns/deleteEmailCampaign.ts @@ -13,9 +13,9 @@ async function deleteEmailCampaign({ email_campaign_id, }: DeleteEmailCampaignRequest): Promise { try { - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; await mailtrap.emailCampaigns.delete(email_campaign_id); diff --git a/src/tools/emailCampaigns/getEmailCampaign.ts b/src/tools/emailCampaigns/getEmailCampaign.ts index 145f7d6..5666044 100644 --- a/src/tools/emailCampaigns/getEmailCampaign.ts +++ b/src/tools/emailCampaigns/getEmailCampaign.ts @@ -13,9 +13,9 @@ async function getEmailCampaign({ email_campaign_id, }: GetEmailCampaignRequest): Promise { try { - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.get(email_campaign_id); diff --git a/src/tools/emailCampaigns/getEmailCampaignStats.ts b/src/tools/emailCampaigns/getEmailCampaignStats.ts index 46987a8..2787880 100644 --- a/src/tools/emailCampaigns/getEmailCampaignStats.ts +++ b/src/tools/emailCampaigns/getEmailCampaignStats.ts @@ -9,7 +9,7 @@ import { getEmailCampaignStatsZod } from "./schemas/getEmailCampaignStats"; async function getEmailCampaignStats(raw: unknown): Promise { try { -const parsed = getEmailCampaignStatsZod.safeParse(raw ?? {}); + const parsed = getEmailCampaignStatsZod.safeParse(raw ?? {}); if (!parsed.success) { const msg = parsed.error.errors .map((e) => `${e.path.join(".")}: ${e.message}`) @@ -26,9 +26,9 @@ const parsed = getEmailCampaignStatsZod.safeParse(raw ?? {}); end_date: endDate, } = parsed.data; - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const params = { ...(startDate !== undefined ? { start_date: startDate } : {}), diff --git a/src/tools/emailCampaigns/listEmailCampaigns.ts b/src/tools/emailCampaigns/listEmailCampaigns.ts index 5b2c597..389e324 100644 --- a/src/tools/emailCampaigns/listEmailCampaigns.ts +++ b/src/tools/emailCampaigns/listEmailCampaigns.ts @@ -25,9 +25,9 @@ async function listEmailCampaigns(raw: unknown): Promise { const params = parsed.data; - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = (await mailtrap.emailCampaigns.getList( Object.keys(params).length > 0 ? params : undefined diff --git a/src/tools/emailCampaigns/resetEmailCampaign.ts b/src/tools/emailCampaigns/resetEmailCampaign.ts index 4e37d7e..0cd4c3f 100644 --- a/src/tools/emailCampaigns/resetEmailCampaign.ts +++ b/src/tools/emailCampaigns/resetEmailCampaign.ts @@ -13,9 +13,9 @@ async function resetEmailCampaign({ email_campaign_id, }: ResetEmailCampaignRequest): Promise { try { - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.reset(email_campaign_id); diff --git a/src/tools/emailCampaigns/scheduleEmailCampaign.ts b/src/tools/emailCampaigns/scheduleEmailCampaign.ts index 07d9108..544ac50 100644 --- a/src/tools/emailCampaigns/scheduleEmailCampaign.ts +++ b/src/tools/emailCampaigns/scheduleEmailCampaign.ts @@ -22,9 +22,9 @@ async function scheduleEmailCampaign(raw: unknown): Promise { const { email_campaign_id: emailCampaignId, datetime } = parsed.data; - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.schedule(emailCampaignId, { datetime, diff --git a/src/tools/emailCampaigns/startEmailCampaign.ts b/src/tools/emailCampaigns/startEmailCampaign.ts index d11c583..b515de3 100644 --- a/src/tools/emailCampaigns/startEmailCampaign.ts +++ b/src/tools/emailCampaigns/startEmailCampaign.ts @@ -13,9 +13,9 @@ async function startEmailCampaign({ email_campaign_id, }: StartEmailCampaignRequest): Promise { try { - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.start(email_campaign_id); diff --git a/src/tools/emailCampaigns/terminateEmailCampaign.ts b/src/tools/emailCampaigns/terminateEmailCampaign.ts index 67fa94d..568c6c3 100644 --- a/src/tools/emailCampaigns/terminateEmailCampaign.ts +++ b/src/tools/emailCampaigns/terminateEmailCampaign.ts @@ -13,9 +13,9 @@ async function terminateEmailCampaign({ email_campaign_id, }: TerminateEmailCampaignRequest): Promise { try { - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.terminate(email_campaign_id); diff --git a/src/tools/emailCampaigns/updateEmailCampaign.ts b/src/tools/emailCampaigns/updateEmailCampaign.ts index 672cbbe..580d426 100644 --- a/src/tools/emailCampaigns/updateEmailCampaign.ts +++ b/src/tools/emailCampaigns/updateEmailCampaign.ts @@ -22,9 +22,9 @@ async function updateEmailCampaign(raw: unknown): Promise { const { email_campaign_id: emailCampaignId, ...params } = parsed.data; - const mailtrap = requireClient( - "email campaigns" - ) as unknown as EmailCampaignsClient; + const mailtrap = requireClient("email campaigns", { + requireAccountId: false, + }) as unknown as EmailCampaignsClient; const response = await mailtrap.emailCampaigns.update( emailCampaignId,