diff --git a/README.md b/README.md index 50b54e8..c2d4d85 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,13 @@ Email Sandbox (Testing): - Project management CRUD – [`testing/projects.ts`](examples/testing/projects.ts) - Attachments operations – [`testing/attachments.ts`](examples/testing/attachments.ts) +Inbound Email: + +- Folders CRUD – [`inbound/folders.ts`](examples/inbound/folders.ts) +- Inboxes CRUD (hosted & custom-domain) – [`inbound/inboxes.ts`](examples/inbound/inboxes.ts) +- Messages (list, get, delete, reply, reply-all, forward) – [`inbound/messages.ts`](examples/inbound/messages.ts) +- Threads (list, get, delete) – [`inbound/threads.ts`](examples/inbound/threads.ts) + Contact management: - Contacts CRUD & listing – [`contacts/everything.ts`](examples/contacts/everything.ts) diff --git a/examples/inbound/folders.ts b/examples/inbound/folders.ts new file mode 100644 index 0000000..54626d9 --- /dev/null +++ b/examples/inbound/folders.ts @@ -0,0 +1,29 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; + +// Inbound is scoped to the token's account, so no accountId is required. +const client = new MailtrapClient({ token: TOKEN }); +const foldersClient = client.inbound.folders; + +async function foldersFlow() { + try { + const created = await foldersClient.create({ name: "Support" }); + console.log("Created folder:", created); + + console.log("All folders:", await foldersClient.getList()); + console.log("One folder:", await foldersClient.get(created.id)); + + const updated = await foldersClient.update(created.id, { + name: "Customer Success", + }); + console.log("Updated folder:", updated); + + await foldersClient.delete(created.id); + console.log("Deleted folder", created.id); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + } +} + +foldersFlow(); diff --git a/examples/inbound/inboxes.ts b/examples/inbound/inboxes.ts new file mode 100644 index 0000000..24baef1 --- /dev/null +++ b/examples/inbound/inboxes.ts @@ -0,0 +1,32 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const FOLDER_ID = Number(""); + +// Inbound is scoped to the token's account, so no accountId is required. +const client = new MailtrapClient({ token: TOKEN }); +const inboxesClient = client.inbound.inboxes; + +async function inboxesFlow() { + try { + // Omit domain_id for a Mailtrap-hosted inbox; pass it to create a + // custom-domain (catch-all) inbox. + const created = await inboxesClient.create(FOLDER_ID, { name: "Tickets" }); + console.log("Created inbox:", created); + + console.log("All inboxes:", await inboxesClient.getList(FOLDER_ID)); + console.log("One inbox:", await inboxesClient.get(FOLDER_ID, created.id)); + + const updated = await inboxesClient.update(FOLDER_ID, created.id, { + name: "Tickets (renamed)", + }); + console.log("Updated inbox:", updated); + + await inboxesClient.delete(FOLDER_ID, created.id); + console.log("Deleted inbox", created.id); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + } +} + +inboxesFlow(); diff --git a/examples/inbound/messages.ts b/examples/inbound/messages.ts new file mode 100644 index 0000000..ca529c1 --- /dev/null +++ b/examples/inbound/messages.ts @@ -0,0 +1,66 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const INBOX_ID = Number(""); + +// Set to a real message id to try the mutating actions (reply / reply-all / +// forward / delete). Left empty, the example is read-only (list + get). +const MESSAGE_ID = ""; + +// Inbound is scoped to the token's account, so no accountId is required. +const client = new MailtrapClient({ token: TOKEN }); +const messagesClient = client.inbound.messages; + +async function messagesFlow() { + try { + const list = await messagesClient.getList(INBOX_ID); + console.log("Messages:", list); + + // Fetch the next page with the returned cursor. + if (list.last_id) { + console.log( + "Next page:", + await messagesClient.getList(INBOX_ID, { last_id: list.last_id }) + ); + } + + if (list.data.length > 0) { + console.log( + "First message:", + await messagesClient.get(INBOX_ID, list.data[0].id) + ); + } + + if (!MESSAGE_ID) { + console.log( + "Set MESSAGE_ID above to try reply / reply-all / forward / delete." + ); + return; + } + + // These act on a real message: reply/reply_all/forward SEND email, and + // delete removes the message. Only run against a message you own. + console.log( + "Reply:", + await messagesClient.reply(INBOX_ID, MESSAGE_ID, { + text: "Thanks for reaching out — we are looking into it.", + }) + ); + + await messagesClient.replyAll(INBOX_ID, MESSAGE_ID, { + text: "Looping everyone in.", + }); + + await messagesClient.forward(INBOX_ID, MESSAGE_ID, { + to: [{ email: "colleague@example.com" }], + text: "Please take a look.", + }); + + await messagesClient.delete(INBOX_ID, MESSAGE_ID); + console.log("Deleted message", MESSAGE_ID); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + } +} + +messagesFlow(); diff --git a/examples/inbound/threads.ts b/examples/inbound/threads.ts new file mode 100644 index 0000000..7caa3c2 --- /dev/null +++ b/examples/inbound/threads.ts @@ -0,0 +1,47 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const INBOX_ID = Number(""); + +// Set to a real thread id to try deletion below. Left empty, the example is +// read-only (list + get). +const THREAD_ID = ""; + +// Inbound is scoped to the token's account, so no accountId is required. +const client = new MailtrapClient({ token: TOKEN }); +const threadsClient = client.inbound.threads; + +async function threadsFlow() { + try { + const list = await threadsClient.getList(INBOX_ID); + console.log("Threads:", list); + + // Fetch the next page with the returned cursor. + if (list.last_id) { + console.log( + "Next page:", + await threadsClient.getList(INBOX_ID, { last_id: list.last_id }) + ); + } + + if (list.data.length > 0) { + console.log( + "First thread:", + await threadsClient.get(INBOX_ID, list.data[0].id) + ); + } + + if (!THREAD_ID) { + console.log("Set THREAD_ID above to try deletion."); + return; + } + + // Deletes a real thread — only run against one you own. + await threadsClient.delete(INBOX_ID, THREAD_ID); + console.log("Deleted thread", THREAD_ID); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + } +} + +threadsFlow(); diff --git a/examples/sending-domains/everything.ts b/examples/sending-domains/everything.ts index a2f361b..f0d56fb 100644 --- a/examples/sending-domains/everything.ts +++ b/examples/sending-domains/everything.ts @@ -1,4 +1,4 @@ -import { MailtrapClient } from "../../src/index"; +import { MailtrapClient } from "mailtrap"; const TOKEN = ""; const ACCOUNT_ID = ""; diff --git a/src/__tests__/lib/api/Inbound.test.ts b/src/__tests__/lib/api/Inbound.test.ts new file mode 100644 index 0000000..da31389 --- /dev/null +++ b/src/__tests__/lib/api/Inbound.test.ts @@ -0,0 +1,40 @@ +import axios from "axios"; + +import InboundAPI from "../../../lib/api/Inbound"; + +describe("lib/api/Inbound: ", () => { + const inbound = new InboundAPI(axios.create()); + + describe("class InboundAPI(): ", () => { + it("exposes folders with CRUD methods.", () => { + expect(inbound.folders).toHaveProperty("getList"); + expect(inbound.folders).toHaveProperty("get"); + expect(inbound.folders).toHaveProperty("create"); + expect(inbound.folders).toHaveProperty("update"); + expect(inbound.folders).toHaveProperty("delete"); + }); + + it("exposes inboxes with CRUD methods.", () => { + expect(inbound.inboxes).toHaveProperty("getList"); + expect(inbound.inboxes).toHaveProperty("get"); + expect(inbound.inboxes).toHaveProperty("create"); + expect(inbound.inboxes).toHaveProperty("update"); + expect(inbound.inboxes).toHaveProperty("delete"); + }); + + it("exposes messages with list/get/delete and send methods.", () => { + expect(inbound.messages).toHaveProperty("getList"); + expect(inbound.messages).toHaveProperty("get"); + expect(inbound.messages).toHaveProperty("delete"); + expect(inbound.messages).toHaveProperty("reply"); + expect(inbound.messages).toHaveProperty("replyAll"); + expect(inbound.messages).toHaveProperty("forward"); + }); + + it("exposes threads with list/get/delete methods.", () => { + expect(inbound.threads).toHaveProperty("getList"); + expect(inbound.threads).toHaveProperty("get"); + expect(inbound.threads).toHaveProperty("delete"); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/EmailLogs.test.ts b/src/__tests__/lib/api/resources/EmailLogs.test.ts index 8e466e8..e02abc0 100644 --- a/src/__tests__/lib/api/resources/EmailLogs.test.ts +++ b/src/__tests__/lib/api/resources/EmailLogs.test.ts @@ -24,6 +24,10 @@ describe("lib/api/resources/EmailLogs: ", () => { message_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", status: "delivered", subject: "Welcome", + rfc_message_id: "", + in_reply_to: null, + references: [], + thread_id: null, from: "sender@example.com", to: "recipient@example.com", sent_at: "2025-01-15T10:30:00Z", diff --git a/src/__tests__/lib/api/resources/SendingDomains.test.ts b/src/__tests__/lib/api/resources/SendingDomains.test.ts index 7f6193f..caa0f0f 100644 --- a/src/__tests__/lib/api/resources/SendingDomains.test.ts +++ b/src/__tests__/lib/api/resources/SendingDomains.test.ts @@ -75,6 +75,8 @@ describe("lib/api/SendingDomains: ", () => { health_alerts_enabled: true, critical_alerts_enabled: true, alert_recipient_email: "john.doe@example.com", + inbound_enabled: true, + inbound_verified: true, permissions: mockPermissions, }, ]; @@ -125,6 +127,8 @@ describe("lib/api/SendingDomains: ", () => { health_alerts_enabled: true, critical_alerts_enabled: true, alert_recipient_email: "john.doe@example.com", + inbound_enabled: true, + inbound_verified: true, permissions: mockPermissions, }; @@ -174,6 +178,8 @@ describe("lib/api/SendingDomains: ", () => { health_alerts_enabled: true, critical_alerts_enabled: true, alert_recipient_email: "admin@newdomain.com", + inbound_enabled: true, + inbound_verified: true, permissions: mockPermissions, }; diff --git a/src/__tests__/lib/api/resources/inbound/Folders.test.ts b/src/__tests__/lib/api/resources/inbound/Folders.test.ts new file mode 100644 index 0000000..912aced --- /dev/null +++ b/src/__tests__/lib/api/resources/inbound/Folders.test.ts @@ -0,0 +1,79 @@ +import axios, { AxiosInstance } from "axios"; +import MockAdapter from "axios-mock-adapter"; + +import FoldersApi from "../../../../../lib/api/resources/inbound/Folders"; +import { Folder } from "../../../../../types/api/inbound/folders"; + +describe("lib/api/resources/inbound/Folders: ", () => { + const axiosInstance: AxiosInstance = axios.create(); + const mock = new MockAdapter(axiosInstance); + axiosInstance.interceptors.response.use((response) => response.data); + + const foldersAPI = new FoldersApi(axiosInstance); + const foldersURL = "https://mailtrap.io/api/inbound/folders"; + + afterEach(() => mock.reset()); + + describe("getList(): ", () => { + it("returns all folders.", async () => { + const folders: Folder[] = [ + { id: 1, name: "Support" }, + { id: 2, name: "Sales" }, + ]; + + mock.onGet(foldersURL).reply(200, folders); + + const result = await foldersAPI.getList(); + + expect(result).toEqual(folders); + }); + }); + + describe("get(): ", () => { + it("returns a single folder by id.", async () => { + const folder: Folder = { id: 1, name: "Support" }; + + mock.onGet(`${foldersURL}/1`).reply(200, folder); + + const result = await foldersAPI.get(1); + + expect(result).toEqual(folder); + }); + }); + + describe("create(): ", () => { + it("creates a folder.", async () => { + const params = { name: "Support" }; + const folder: Folder = { id: 1, name: "Support" }; + + mock.onPost(foldersURL, params).reply(201, folder); + + const result = await foldersAPI.create(params); + + expect(result).toEqual(folder); + }); + }); + + describe("update(): ", () => { + it("updates a folder.", async () => { + const params = { name: "Customer Success" }; + const folder: Folder = { id: 1, name: "Customer Success" }; + + mock.onPatch(`${foldersURL}/1`, params).reply(200, folder); + + const result = await foldersAPI.update(1, params); + + expect(result).toEqual(folder); + }); + }); + + describe("delete(): ", () => { + it("deletes a folder.", async () => { + mock.onDelete(`${foldersURL}/1`).reply(204); + + const result = await foldersAPI.delete(1); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/inbound/Inboxes.test.ts b/src/__tests__/lib/api/resources/inbound/Inboxes.test.ts new file mode 100644 index 0000000..95b3ea1 --- /dev/null +++ b/src/__tests__/lib/api/resources/inbound/Inboxes.test.ts @@ -0,0 +1,88 @@ +import axios, { AxiosInstance } from "axios"; +import MockAdapter from "axios-mock-adapter"; + +import InboxesApi from "../../../../../lib/api/resources/inbound/Inboxes"; +import { Inbox } from "../../../../../types/api/inbound/inboxes"; + +describe("lib/api/resources/inbound/Inboxes: ", () => { + const axiosInstance: AxiosInstance = axios.create(); + const mock = new MockAdapter(axiosInstance); + axiosInstance.interceptors.response.use((response) => response.data); + + const inboxesAPI = new InboxesApi(axiosInstance); + const inboxesURL = "https://mailtrap.io/api/inbound/folders/7/inboxes"; + + const inbox: Inbox = { + id: 42, + name: "Support tickets", + address: "support-tickets-1a2b3c4d@inbound-mailtrap.io", + domain_id: 892, + }; + + afterEach(() => mock.reset()); + + describe("getList(): ", () => { + it("returns all inboxes in a folder.", async () => { + mock.onGet(inboxesURL).reply(200, [inbox]); + + const result = await inboxesAPI.getList(7); + + expect(result).toEqual([inbox]); + }); + }); + + describe("get(): ", () => { + it("returns a single inbox by id.", async () => { + mock.onGet(`${inboxesURL}/42`).reply(200, inbox); + + const result = await inboxesAPI.get(7, 42); + + expect(result).toEqual(inbox); + }); + }); + + describe("create(): ", () => { + it("creates a hosted inbox.", async () => { + const params = { name: "Support tickets" }; + + mock.onPost(inboxesURL, params).reply(201, inbox); + + const result = await inboxesAPI.create(7, params); + + expect(result).toEqual(inbox); + }); + + it("creates a custom-domain inbox with domain_id.", async () => { + const params = { name: "Support tickets", domain_id: 892 }; + + mock.onPost(inboxesURL, params).reply(201, inbox); + + const result = await inboxesAPI.create(7, params); + + expect(result).toEqual(inbox); + }); + }); + + describe("update(): ", () => { + it("updates an inbox.", async () => { + const params = { name: "Renamed" }; + const updated: Inbox = { ...inbox, name: "Renamed" }; + + mock.onPatch(`${inboxesURL}/42`, params).reply(200, updated); + + const result = await inboxesAPI.update(7, 42, params); + + expect(result).toEqual(updated); + }); + }); + + describe("delete(): ", () => { + it("deletes an inbox.", async () => { + mock.onDelete(`${inboxesURL}/42`).reply(204); + + const result = await inboxesAPI.delete(7, 42); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/inbound/Messages.test.ts b/src/__tests__/lib/api/resources/inbound/Messages.test.ts new file mode 100644 index 0000000..904eecd --- /dev/null +++ b/src/__tests__/lib/api/resources/inbound/Messages.test.ts @@ -0,0 +1,122 @@ +import axios, { AxiosInstance } from "axios"; +import MockAdapter from "axios-mock-adapter"; + +import MessagesApi from "../../../../../lib/api/resources/inbound/Messages"; +import { + MessageDetails, + MessagesListResponse, + SendMessageResult, + ForwardMessageParams, +} from "../../../../../types/api/inbound/messages"; + +describe("lib/api/resources/inbound/Messages: ", () => { + const axiosInstance: AxiosInstance = axios.create(); + const mock = new MockAdapter(axiosInstance); + axiosInstance.interceptors.response.use((response) => response.data); + + const messagesAPI = new MessagesApi(axiosInstance); + const messagesURL = "https://mailtrap.io/api/inbound/inboxes/42/messages"; + + afterEach(() => mock.reset()); + + describe("getList(): ", () => { + const listResponse: MessagesListResponse = { + data: [], + total_count: 0, + last_id: null, + }; + + it("lists messages in an inbox.", async () => { + mock.onGet(messagesURL).reply(200, listResponse); + + const result = await messagesAPI.getList(42); + + expect(result).toEqual(listResponse); + }); + + it("passes last_id for pagination.", async () => { + mock + .onGet(`${messagesURL}?last_id=1700000000000123`) + .reply(200, listResponse); + + const result = await messagesAPI.getList(42, { + last_id: "1700000000000123", + }); + + expect(result).toEqual(listResponse); + }); + }); + + describe("get(): ", () => { + it("returns a single message with body and attachment URLs.", async () => { + const message = { + id: "1700000000000123", + raw_message_url: "https://storage.example.com/signed/eml/x?token=...", + } as MessageDetails; + + mock.onGet(`${messagesURL}/1700000000000123`).reply(200, message); + + const result = await messagesAPI.get(42, "1700000000000123"); + + expect(result).toEqual(message); + }); + }); + + describe("delete(): ", () => { + it("deletes a message.", async () => { + mock.onDelete(`${messagesURL}/1700000000000123`).reply(204); + + const result = await messagesAPI.delete(42, "1700000000000123"); + + expect(result).toBeUndefined(); + }); + }); + + const sendResult: SendMessageResult = { + message_ids: ["1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"], + }; + + describe("reply(): ", () => { + it("replies to a message.", async () => { + const params = { text: "Thanks for reaching out." }; + + mock + .onPost(`${messagesURL}/1700000000000123/reply`, params) + .reply(201, sendResult); + + const result = await messagesAPI.reply(42, "1700000000000123", params); + + expect(result).toEqual(sendResult); + }); + }); + + describe("replyAll(): ", () => { + it("replies to all recipients.", async () => { + const params = { text: "Thanks all." }; + + mock + .onPost(`${messagesURL}/1700000000000123/reply_all`, params) + .reply(201, sendResult); + + const result = await messagesAPI.replyAll(42, "1700000000000123", params); + + expect(result).toEqual(sendResult); + }); + }); + + describe("forward(): ", () => { + it("forwards a message.", async () => { + const params: ForwardMessageParams = { + to: [{ email: "colleague@example.com" }], + }; + + mock + .onPost(`${messagesURL}/1700000000000123/forward`, params) + .reply(201, sendResult); + + const result = await messagesAPI.forward(42, "1700000000000123", params); + + expect(result).toEqual(sendResult); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/inbound/Threads.test.ts b/src/__tests__/lib/api/resources/inbound/Threads.test.ts new file mode 100644 index 0000000..1436646 --- /dev/null +++ b/src/__tests__/lib/api/resources/inbound/Threads.test.ts @@ -0,0 +1,68 @@ +import axios, { AxiosInstance } from "axios"; +import MockAdapter from "axios-mock-adapter"; + +import ThreadsApi from "../../../../../lib/api/resources/inbound/Threads"; +import { + Thread, + ThreadsListResponse, +} from "../../../../../types/api/inbound/threads"; + +describe("lib/api/resources/inbound/Threads: ", () => { + const axiosInstance: AxiosInstance = axios.create(); + const mock = new MockAdapter(axiosInstance); + axiosInstance.interceptors.response.use((response) => response.data); + + const threadsAPI = new ThreadsApi(axiosInstance); + const threadsURL = "https://mailtrap.io/api/inbound/inboxes/42/threads"; + + afterEach(() => mock.reset()); + + describe("getList(): ", () => { + const listResponse: ThreadsListResponse = { + data: [], + total_count: 0, + last_id: null, + }; + + it("lists threads in an inbox.", async () => { + mock.onGet(threadsURL).reply(200, listResponse); + + const result = await threadsAPI.getList(42); + + expect(result).toEqual(listResponse); + }); + + it("passes last_id for pagination.", async () => { + mock.onGet(`${threadsURL}?last_id=abc123`).reply(200, listResponse); + + const result = await threadsAPI.getList(42, { last_id: "abc123" }); + + expect(result).toEqual(listResponse); + }); + }); + + describe("get(): ", () => { + it("returns a single thread with messages.", async () => { + const thread = { + id: "1700000000000124", + messages: [], + } as unknown as Thread; + + mock.onGet(`${threadsURL}/1700000000000124`).reply(200, thread); + + const result = await threadsAPI.get(42, "1700000000000124"); + + expect(result).toEqual(thread); + }); + }); + + describe("delete(): ", () => { + it("deletes a thread.", async () => { + mock.onDelete(`${threadsURL}/1700000000000124`).reply(204); + + const result = await threadsAPI.delete(42, "1700000000000124"); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 9428b5a..0d574a2 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -15,6 +15,7 @@ import ContactListsBaseAPI from "./api/ContactLists"; import ContactsBaseAPI from "./api/Contacts"; import EmailLogsBaseAPI from "./api/EmailLogs"; import GeneralAPI from "./api/General"; +import InboundAPI from "./api/Inbound"; import SendingDomainsBaseAPI from "./api/SendingDomains"; import StatsBaseAPI from "./api/Stats"; import SuppressionsBaseAPI from "./api/Suppressions"; @@ -147,6 +148,13 @@ export default class MailtrapClient { return new GeneralAPI(this.axios, accountId); } + /** + * Getter for Inbound API. Scoped to the token's account, so no accountId is required. + */ + get inbound() { + return new InboundAPI(this.axios); + } + /** * Getter for Contacts API. */ diff --git a/src/lib/api/Inbound.ts b/src/lib/api/Inbound.ts new file mode 100644 index 0000000..94999af --- /dev/null +++ b/src/lib/api/Inbound.ts @@ -0,0 +1,23 @@ +import { AxiosInstance } from "axios"; + +import FoldersApi from "./resources/inbound/Folders"; +import InboxesApi from "./resources/inbound/Inboxes"; +import MessagesApi from "./resources/inbound/Messages"; +import ThreadsApi from "./resources/inbound/Threads"; + +export default class InboundAPI { + public folders: FoldersApi; + + public inboxes: InboxesApi; + + public messages: MessagesApi; + + public threads: ThreadsApi; + + constructor(client: AxiosInstance) { + this.folders = new FoldersApi(client); + this.inboxes = new InboxesApi(client); + this.messages = new MessagesApi(client); + this.threads = new ThreadsApi(client); + } +} diff --git a/src/lib/api/resources/inbound/Folders.ts b/src/lib/api/resources/inbound/Folders.ts new file mode 100644 index 0000000..13a71cd --- /dev/null +++ b/src/lib/api/resources/inbound/Folders.ts @@ -0,0 +1,59 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../../config"; +import { Folder, FolderParams } from "../../../../types/api/inbound/folders"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class FoldersApi { + private client: AxiosInstance; + + private foldersURL: string; + + constructor(client: AxiosInstance) { + this.client = client; + this.foldersURL = `${GENERAL_ENDPOINT}/api/inbound/folders`; + } + + /** + * Get all inbound folders in the account. + */ + public async getList() { + return this.client.get(this.foldersURL); + } + + /** + * Get a single inbound folder by ID. + */ + public async get(folderId: number) { + const url = `${this.foldersURL}/${folderId}`; + + return this.client.get(url); + } + + /** + * Create a new inbound folder. + */ + public async create(params: FolderParams) { + return this.client.post(this.foldersURL, params); + } + + /** + * Update an inbound folder by ID. + */ + public async update(folderId: number, params: FolderParams) { + const url = `${this.foldersURL}/${folderId}`; + + return this.client.patch(url, params); + } + + /** + * Delete an inbound folder by ID, along with all of its inboxes. + */ + public async delete(folderId: number) { + const url = `${this.foldersURL}/${folderId}`; + + return this.client.delete(url); + } +} diff --git a/src/lib/api/resources/inbound/Inboxes.ts b/src/lib/api/resources/inbound/Inboxes.ts new file mode 100644 index 0000000..b2ccb4c --- /dev/null +++ b/src/lib/api/resources/inbound/Inboxes.ts @@ -0,0 +1,71 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../../config"; +import { + Inbox, + CreateInboxParams, + UpdateInboxParams, +} from "../../../../types/api/inbound/inboxes"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class InboxesApi { + private client: AxiosInstance; + + private foldersURL: string; + + constructor(client: AxiosInstance) { + this.client = client; + this.foldersURL = `${GENERAL_ENDPOINT}/api/inbound/folders`; + } + + private inboxesURL(folderId: number) { + return `${this.foldersURL}/${folderId}/inboxes`; + } + + /** + * Get all inboxes in a folder. + */ + public async getList(folderId: number) { + return this.client.get(this.inboxesURL(folderId)); + } + + /** + * Get a single inbox by ID. + */ + public async get(folderId: number, inboxId: number) { + const url = `${this.inboxesURL(folderId)}/${inboxId}`; + + return this.client.get(url); + } + + /** + * Create a new inbox in a folder. + */ + public async create(folderId: number, params: CreateInboxParams) { + return this.client.post(this.inboxesURL(folderId), params); + } + + /** + * Update an inbox by ID. + */ + public async update( + folderId: number, + inboxId: number, + params: UpdateInboxParams + ) { + const url = `${this.inboxesURL(folderId)}/${inboxId}`; + + return this.client.patch(url, params); + } + + /** + * Delete an inbox by ID. + */ + public async delete(folderId: number, inboxId: number) { + const url = `${this.inboxesURL(folderId)}/${inboxId}`; + + return this.client.delete(url); + } +} diff --git a/src/lib/api/resources/inbound/Messages.ts b/src/lib/api/resources/inbound/Messages.ts new file mode 100644 index 0000000..c914370 --- /dev/null +++ b/src/lib/api/resources/inbound/Messages.ts @@ -0,0 +1,100 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../../config"; +import { + MessageDetails, + MessagesListResponse, + MessagesListParams, + SendMessageParams, + ForwardMessageParams, + SendMessageResult, +} from "../../../../types/api/inbound/messages"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class MessagesApi { + private client: AxiosInstance; + + private inboxesURL: string; + + constructor(client: AxiosInstance) { + this.client = client; + this.inboxesURL = `${GENERAL_ENDPOINT}/api/inbound/inboxes`; + } + + private messagesURL(inboxId: number) { + return `${this.inboxesURL}/${inboxId}/messages`; + } + + /** + * List messages in an inbox (paginated). Pass `last_id` from the previous + * response to fetch the next page. + */ + public async getList(inboxId: number, params?: MessagesListParams) { + const url = params?.last_id + ? `${this.messagesURL(inboxId)}?last_id=${encodeURIComponent( + params.last_id + )}` + : this.messagesURL(inboxId); + + return this.client.get(url); + } + + /** + * Get a single message with its full body and attachment download URLs. + */ + public async get(inboxId: number, messageId: string) { + const url = `${this.messagesURL(inboxId)}/${messageId}`; + + return this.client.get(url); + } + + /** + * Delete a message by ID. + */ + public async delete(inboxId: number, messageId: string) { + const url = `${this.messagesURL(inboxId)}/${messageId}`; + + return this.client.delete(url); + } + + /** + * Reply to a message. Sends to the original sender. + */ + public async reply( + inboxId: number, + messageId: string, + params: SendMessageParams + ) { + const url = `${this.messagesURL(inboxId)}/${messageId}/reply`; + + return this.client.post(url, params); + } + + /** + * Reply to a message and copy the original's other recipients. + */ + public async replyAll( + inboxId: number, + messageId: string, + params: SendMessageParams + ) { + const url = `${this.messagesURL(inboxId)}/${messageId}/reply_all`; + + return this.client.post(url, params); + } + + /** + * Forward a message to new recipients (at least one `to` is required). + */ + public async forward( + inboxId: number, + messageId: string, + params: ForwardMessageParams + ) { + const url = `${this.messagesURL(inboxId)}/${messageId}/forward`; + + return this.client.post(url, params); + } +} diff --git a/src/lib/api/resources/inbound/Threads.ts b/src/lib/api/resources/inbound/Threads.ts new file mode 100644 index 0000000..7e12130 --- /dev/null +++ b/src/lib/api/resources/inbound/Threads.ts @@ -0,0 +1,58 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../../config"; +import { + Thread, + ThreadsListResponse, + ThreadsListParams, +} from "../../../../types/api/inbound/threads"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ThreadsApi { + private client: AxiosInstance; + + private inboxesURL: string; + + constructor(client: AxiosInstance) { + this.client = client; + this.inboxesURL = `${GENERAL_ENDPOINT}/api/inbound/inboxes`; + } + + private threadsURL(inboxId: number) { + return `${this.inboxesURL}/${inboxId}/threads`; + } + + /** + * List threads in an inbox (paginated). Pass `last_id` from the previous + * response to fetch the next page. + */ + public async getList(inboxId: number, params?: ThreadsListParams) { + const url = params?.last_id + ? `${this.threadsURL(inboxId)}?last_id=${encodeURIComponent( + params.last_id + )}` + : this.threadsURL(inboxId); + + return this.client.get(url); + } + + /** + * Get a single thread with its messages embedded (oldest first). + */ + public async get(inboxId: number, threadId: string) { + const url = `${this.threadsURL(inboxId)}/${threadId}`; + + return this.client.get(url); + } + + /** + * Delete a thread by ID. + */ + public async delete(inboxId: number, threadId: string) { + const url = `${this.threadsURL(inboxId)}/${threadId}`; + + return this.client.delete(url); + } +} diff --git a/src/types/api/email-logs.ts b/src/types/api/email-logs.ts index 38fbedb..8170898 100644 --- a/src/types/api/email-logs.ts +++ b/src/types/api/email-logs.ts @@ -8,6 +8,10 @@ export type EmailLogMessage = { message_id: string; status: EmailLogMessageStatus; subject: string | null; + rfc_message_id: string | null; + in_reply_to: string | null; + references: string[]; + thread_id: string | null; from: string; to: string; sent_at: string; diff --git a/src/types/api/inbound/folders.ts b/src/types/api/inbound/folders.ts new file mode 100644 index 0000000..c377896 --- /dev/null +++ b/src/types/api/inbound/folders.ts @@ -0,0 +1,8 @@ +export interface Folder { + id: number; + name: string; +} + +export interface FolderParams { + name: string; +} diff --git a/src/types/api/inbound/inboxes.ts b/src/types/api/inbound/inboxes.ts new file mode 100644 index 0000000..6ec6ac8 --- /dev/null +++ b/src/types/api/inbound/inboxes.ts @@ -0,0 +1,20 @@ +export interface Inbox { + id: number; + name: string; + address: string; + domain_id: number; +} + +/** + * Create an inbox inside a folder. Omit `domain_id` for a standard + * Mailtrap-hosted inbox; pass it to create a custom-domain (catch-all) inbox. + */ +export interface CreateInboxParams { + name: string; + domain_id?: number; +} + +/** Update an inbox. Rename only. */ +export interface UpdateInboxParams { + name: string; +} diff --git a/src/types/api/inbound/messages.ts b/src/types/api/inbound/messages.ts new file mode 100644 index 0000000..438b89a --- /dev/null +++ b/src/types/api/inbound/messages.ts @@ -0,0 +1,97 @@ +export type ContentDisposition = "attachment" | "inline"; + +export interface Attachment { + attachment_id: string; + size: number | null; + filename: string | null; + content_type: string | null; + content_disposition: ContentDisposition | null; + content_id: string | null; +} + +export interface AttachmentWithDownloadUrl extends Attachment { + download_url: string | null; + download_url_expires_at: string | null; +} + +export interface Message { + /** Mailtrap object id (not the RFC Message-ID header). */ + id: string; + inbox_id: number; + from: string | null; + to: string[]; + cc: string[]; + bcc: string[]; + reply_to: string | null; + subject: string | null; + /** Value of the RFC 5322 Message-ID header. */ + rfc_message_id: string | null; + in_reply_to: string | null; + references: string[]; + headers: Record | null; + size: number | null; + html_size: number | null; + text_size: number | null; + received_at: string; + /** ID of the thread this message belongs to, if any. */ + thread_id: string | null; + attachments: Attachment[]; +} + +export interface MessageDetails extends Omit { + attachments: AttachmentWithDownloadUrl[]; + raw_message_url: string | null; + raw_message_expires_at: string | null; + html_body: string | null; + text_body: string | null; +} + +export interface MessagesListResponse { + data: Message[]; + total_count: number; + last_id: string | null; +} + +export interface MessagesListParams { + last_id?: string; +} + +export interface Address { + email: string; + name?: string; +} + +export interface EmailAttachment { + content: string; + filename: string; + type?: string; + disposition?: ContentDisposition; + content_id?: string; +} + +/** + * Body for reply, reply-all, and forward. `from` is rejected for + * Mailtrap-hosted inboxes and required for custom-domain inboxes. + */ +export interface SendMessageParams { + from?: Address; + to?: Address[]; + cc?: Address[]; + bcc?: Address[]; + reply_to?: Address; + text?: string; + html?: string; + category?: string; + attachments?: EmailAttachment[]; + headers?: Record; + custom_variables?: Record; +} + +/** Forward requires at least one recipient in `to`. */ +export interface ForwardMessageParams extends Omit { + to: [Address, ...Address[]]; +} + +export interface SendMessageResult { + message_ids: string[]; +} diff --git a/src/types/api/inbound/threads.ts b/src/types/api/inbound/threads.ts new file mode 100644 index 0000000..558831e --- /dev/null +++ b/src/types/api/inbound/threads.ts @@ -0,0 +1,62 @@ +import { AttachmentWithDownloadUrl } from "./messages"; + +export type MessageVisibilityStatus = "available" | "placeholder"; +export type MessageDirection = "inbound" | "outbound"; + +export interface ThreadSummary { + id: string; + subject: string | null; + message_count: number; + size: number; + first_message_at: string; + last_received_at: string | null; + last_sent_at: string | null; + last_activity_at: string; + last_message_id: string | null; + senders: string[]; + recipients: string[]; + attachments: AttachmentWithDownloadUrl[]; +} + +/** + * A message inside a thread. Only `visibility_status` and `direction` are + * guaranteed; `placeholder` entries omit the rest. `available` outbound + * entries additionally carry the delivery lifecycle fields. + */ +export interface ThreadMessage { + visibility_status: MessageVisibilityStatus; + direction: MessageDirection; + id?: string; + message_group_id?: string | null; + subject?: string | null; + rfc_message_id?: string | null; + in_reply_to?: string | null; + references?: string[]; + from?: string | null; + to?: string[]; + cc?: string[]; + bcc?: string[]; + reply_to?: string | null; + created_at?: string; + email_size?: number; + text_body?: string | null; + html_body?: string | null; + attachments?: AttachmentWithDownloadUrl[]; + delivery_status?: string | null; + delivered_at?: string | null; + bounced_at?: string | null; +} + +export interface Thread extends ThreadSummary { + messages: ThreadMessage[]; +} + +export interface ThreadsListResponse { + data: ThreadSummary[]; + total_count: number; + last_id: string | null; +} + +export interface ThreadsListParams { + last_id?: string; +} diff --git a/src/types/api/sending-domains.ts b/src/types/api/sending-domains.ts index 4333867..567e435 100644 --- a/src/types/api/sending-domains.ts +++ b/src/types/api/sending-domains.ts @@ -28,6 +28,8 @@ export interface SendingDomain { health_alerts_enabled: boolean; critical_alerts_enabled: boolean; alert_recipient_email: string | null; + inbound_enabled: boolean; + inbound_verified: boolean; permissions: SendingDomainPermissions; }