From 7e2b510c41bc243572c49d8b20079f4f6ad11027 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Fri, 2 Oct 2026 15:17:20 +0200 Subject: [PATCH] Add inbound forward rules, forward outcomes, thread delivery and search BREAKING CHANGE: ThreadMessage no longer has delivery_status, delivered_at and bounced_at; read them from delivery instead. --- README.md | 3 +- examples/inbound/forward-rules.ts | 50 +++++ examples/inbound/messages.ts | 2 + examples/inbound/threads.ts | 26 ++- src/__tests__/lib/api/Inbound.test.ts | 8 + .../resources/inbound/ForwardRules.test.ts | 205 ++++++++++++++++++ .../api/resources/inbound/Messages.test.ts | 40 ++++ .../lib/api/resources/inbound/Threads.test.ts | 78 ++++++- src/lib/api/Inbound.ts | 4 + src/lib/api/resources/inbound/ForwardRules.ts | 80 +++++++ src/lib/api/resources/inbound/Threads.ts | 22 +- src/types/api/inbound/forward-rules.ts | 68 ++++++ src/types/api/inbound/messages.ts | 3 + src/types/api/inbound/threads.ts | 23 +- 14 files changed, 591 insertions(+), 21 deletions(-) create mode 100644 examples/inbound/forward-rules.ts create mode 100644 src/__tests__/lib/api/resources/inbound/ForwardRules.test.ts create mode 100644 src/lib/api/resources/inbound/ForwardRules.ts create mode 100644 src/types/api/inbound/forward-rules.ts diff --git a/README.md b/README.md index ad0d92e..edb94c9 100644 --- a/README.md +++ b/README.md @@ -255,9 +255,10 @@ Email Sandbox (Testing): Inbound Email: - Folders CRUD – [`inbound/folders.ts`](examples/inbound/folders.ts) +- Forward rules CRUD – [`inbound/forward-rules.ts`](examples/inbound/forward-rules.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) +- Threads (list with search, get, delete) – [`inbound/threads.ts`](examples/inbound/threads.ts) Contact management: diff --git a/examples/inbound/forward-rules.ts b/examples/inbound/forward-rules.ts new file mode 100644 index 0000000..4e1655b --- /dev/null +++ b/examples/inbound/forward-rules.ts @@ -0,0 +1,50 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const INBOX_ID = Number(""); + +const client = new MailtrapClient({ token: TOKEN }); +const forwardRulesClient = client.inbound.forwardRules; + +async function forwardRulesFlow() { + try { + const created = await forwardRulesClient.create(INBOX_ID, { + name: "Copy billing mail to finance", + conditions: [ + { + match_type: "sender", + operator: "ends_with", + value: "@billing.example.com", + }, + ], + destinations: [{ email: "finance@example.com" }], + }); + console.log("Created forward rule:", created.data); + + const ruleId = created.data.id; + + console.log( + "All forward rules:", + await forwardRulesClient.getList(INBOX_ID) + ); + console.log( + "One forward rule:", + await forwardRulesClient.get(INBOX_ID, ruleId) + ); + + const updated = await forwardRulesClient.update(INBOX_ID, ruleId, { + destinations: [ + { email: "finance@example.com" }, + { email: "accounting@example.com" }, + ], + }); + console.log("Updated forward rule:", updated.data); + + await forwardRulesClient.delete(INBOX_ID, ruleId); + console.log("Deleted forward rule", ruleId); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + } +} + +forwardRulesFlow(); diff --git a/examples/inbound/messages.ts b/examples/inbound/messages.ts index ca529c1..0113ff1 100644 --- a/examples/inbound/messages.ts +++ b/examples/inbound/messages.ts @@ -25,6 +25,8 @@ async function messagesFlow() { } if (list.data.length > 0) { + console.log("Forward outcomes:", list.data[0].forwards); + console.log( "First message:", await messagesClient.get(INBOX_ID, list.data[0].id) diff --git a/examples/inbound/threads.ts b/examples/inbound/threads.ts index 7caa3c2..9946412 100644 --- a/examples/inbound/threads.ts +++ b/examples/inbound/threads.ts @@ -24,13 +24,33 @@ async function threadsFlow() { ); } - if (list.data.length > 0) { + const search = "acme"; + const filtered = await threadsClient.getList(INBOX_ID, { search }); + console.log("Threads matching search:", filtered); + + if (filtered.last_id) { console.log( - "First thread:", - await threadsClient.get(INBOX_ID, list.data[0].id) + "Next filtered page:", + await threadsClient.getList(INBOX_ID, { + search, + last_id: filtered.last_id, + }) ); } + if (list.data.length > 0) { + const thread = await threadsClient.get(INBOX_ID, list.data[0].id); + console.log("First thread:", thread); + + thread.messages.forEach((message) => { + if (message.direction === "outbound") { + console.log("Delivery:", message.delivery); + } else { + console.log("Forwards:", message.forwards); + } + }); + } + if (!THREAD_ID) { console.log("Set THREAD_ID above to try deletion."); return; diff --git a/src/__tests__/lib/api/Inbound.test.ts b/src/__tests__/lib/api/Inbound.test.ts index da31389..63fad8b 100644 --- a/src/__tests__/lib/api/Inbound.test.ts +++ b/src/__tests__/lib/api/Inbound.test.ts @@ -14,6 +14,14 @@ describe("lib/api/Inbound: ", () => { expect(inbound.folders).toHaveProperty("delete"); }); + it("exposes forward rules with CRUD methods.", () => { + expect(inbound.forwardRules).toHaveProperty("getList"); + expect(inbound.forwardRules).toHaveProperty("get"); + expect(inbound.forwardRules).toHaveProperty("create"); + expect(inbound.forwardRules).toHaveProperty("update"); + expect(inbound.forwardRules).toHaveProperty("delete"); + }); + it("exposes inboxes with CRUD methods.", () => { expect(inbound.inboxes).toHaveProperty("getList"); expect(inbound.inboxes).toHaveProperty("get"); diff --git a/src/__tests__/lib/api/resources/inbound/ForwardRules.test.ts b/src/__tests__/lib/api/resources/inbound/ForwardRules.test.ts new file mode 100644 index 0000000..4d377b6 --- /dev/null +++ b/src/__tests__/lib/api/resources/inbound/ForwardRules.test.ts @@ -0,0 +1,205 @@ +import axios, { AxiosInstance } from "axios"; +import MockAdapter from "axios-mock-adapter"; + +import ForwardRulesApi from "../../../../../lib/api/resources/inbound/ForwardRules"; +import handleSendingError from "../../../../../lib/axios-logger"; +import MailtrapError from "../../../../../lib/MailtrapError"; +import { + CreateForwardRuleParams, + ForwardRule, + ForwardRuleResponse, + ForwardRulesListResponse, + UpdateForwardRuleParams, +} from "../../../../../types/api/inbound/forward-rules"; + +describe("lib/api/resources/inbound/ForwardRules: ", () => { + const axiosInstance: AxiosInstance = axios.create(); + const mock = new MockAdapter(axiosInstance); + axiosInstance.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + + const forwardRulesAPI = new ForwardRulesApi(axiosInstance); + const forwardRulesURL = + "https://mailtrap.io/api/inbound/inboxes/42/forward_rules"; + + const rule: ForwardRule = { + id: 7, + name: "Copy billing mail to finance", + created_at: "2026-05-08T10:30:00.000Z", + updated_at: "2026-05-08T10:30:00.000Z", + conditions: [ + { + match_type: "sender", + operator: "ends_with", + value: "@billing.example.com", + header_key: null, + }, + ], + destinations: [{ email: "finance@example.com" }], + }; + const ruleResponse: ForwardRuleResponse = { data: rule }; + + const expectMailtrapError = async (request: () => Promise) => { + expect.assertions(2); + + try { + await request(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual("Not Found"); + } + } + }; + + afterEach(() => mock.reset()); + + describe("getList(): ", () => { + it("lists forward rules in an inbox.", async () => { + const listResponse: ForwardRulesListResponse = { + data: [ + rule, + { + id: 8, + name: "Forward everything to archive", + created_at: "2026-05-08T11:00:00.000Z", + updated_at: "2026-05-08T11:00:00.000Z", + conditions: [], + destinations: [{ email: "archive@example.com" }], + }, + ], + }; + + mock.onGet(forwardRulesURL).reply(200, listResponse); + + const result = await forwardRulesAPI.getList(42); + + expect(result).toEqual(listResponse); + }); + + it("fails with error.", async () => { + mock.onGet(forwardRulesURL).reply(404, { error: "Not Found" }); + + await expectMailtrapError(() => forwardRulesAPI.getList(42)); + }); + }); + + describe("get(): ", () => { + it("returns a single forward rule.", async () => { + const headerRule: ForwardRuleResponse = { + data: { + ...rule, + id: 9, + conditions: [ + { + match_type: "header", + operator: "equal", + value: "high", + header_key: "X-Priority-Level", + }, + ], + }, + }; + + mock.onGet(`${forwardRulesURL}/9`).reply(200, headerRule); + + const result = await forwardRulesAPI.get(42, 9); + + expect(result).toEqual(headerRule); + }); + + it("fails with error.", async () => { + mock.onGet(`${forwardRulesURL}/9`).reply(404, { error: "Not Found" }); + + await expectMailtrapError(() => forwardRulesAPI.get(42, 9)); + }); + }); + + describe("create(): ", () => { + const params: CreateForwardRuleParams = { + name: "Copy billing mail to finance", + conditions: [ + { + match_type: "sender", + operator: "ends_with", + value: "@billing.example.com", + }, + ], + destinations: [{ email: "finance@example.com" }], + }; + + it("creates a forward rule with an unwrapped body.", async () => { + mock.onPost(forwardRulesURL).reply(201, ruleResponse); + + const result = await forwardRulesAPI.create(42, params); + + expect(JSON.parse(mock.history.post[0].data)).toEqual(params); + expect(result).toEqual(ruleResponse); + }); + + it("fails with error.", async () => { + mock.onPost(forwardRulesURL).reply(404, { error: "Not Found" }); + + await expectMailtrapError(() => forwardRulesAPI.create(42, params)); + }); + }); + + describe("update(): ", () => { + it("sends only the fields provided.", async () => { + const params: UpdateForwardRuleParams = { + destinations: [ + { email: "finance@example.com" }, + { email: "accounting@example.com" }, + ], + }; + + mock.onPatch(`${forwardRulesURL}/7`).reply(200, ruleResponse); + + const result = await forwardRulesAPI.update(42, 7, params); + + expect(JSON.parse(mock.history.patch[0].data)).toEqual(params); + expect(result).toEqual(ruleResponse); + }); + + it("sends an empty array to clear conditions.", async () => { + const params: UpdateForwardRuleParams = { conditions: [] }; + + mock.onPatch(`${forwardRulesURL}/7`).reply(200, { + data: { ...rule, conditions: [] }, + }); + + await forwardRulesAPI.update(42, 7, params); + + expect(JSON.parse(mock.history.patch[0].data)).toEqual({ + conditions: [], + }); + }); + + it("fails with error.", async () => { + mock.onPatch(`${forwardRulesURL}/7`).reply(404, { error: "Not Found" }); + + await expectMailtrapError(() => + forwardRulesAPI.update(42, 7, { name: "Renamed" }) + ); + }); + }); + + describe("delete(): ", () => { + it("deletes a forward rule.", async () => { + mock.onDelete(`${forwardRulesURL}/7`).reply(204); + + const result = await forwardRulesAPI.delete(42, 7); + + expect(result).toBeUndefined(); + }); + + it("fails with error.", async () => { + mock.onDelete(`${forwardRulesURL}/7`).reply(404, { error: "Not Found" }); + + await expectMailtrapError(() => forwardRulesAPI.delete(42, 7)); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/inbound/Messages.test.ts b/src/__tests__/lib/api/resources/inbound/Messages.test.ts index 904eecd..7324301 100644 --- a/src/__tests__/lib/api/resources/inbound/Messages.test.ts +++ b/src/__tests__/lib/api/resources/inbound/Messages.test.ts @@ -45,6 +45,46 @@ describe("lib/api/resources/inbound/Messages: ", () => { expect(result).toEqual(listResponse); }); + + it("returns forward outcomes on each message.", async () => { + const response = { + data: [ + { + id: "1700000000000123", + forwards: [ + { + rule_id: 7, + rule_name: "Copy to support team", + destination: "team@example.com", + status: "forwarded", + reason: null, + message_id: "f47ac10b-58cc-4372-a567-0e02b2c3d479", + }, + { + rule_id: 8, + rule_name: null, + destination: "loop@example.com", + status: "rejected", + reason: "loop_prevention", + message_id: null, + }, + ], + }, + ], + total_count: 1, + last_id: null, + } as unknown as MessagesListResponse; + + mock.onGet(messagesURL).reply(200, response); + + const result = await messagesAPI.getList(42); + + expect(result).toEqual(response); + expect(result.data[0].forwards.map((f) => f.status)).toEqual([ + "forwarded", + "rejected", + ]); + }); }); describe("get(): ", () => { diff --git a/src/__tests__/lib/api/resources/inbound/Threads.test.ts b/src/__tests__/lib/api/resources/inbound/Threads.test.ts index 1436646..da0aa44 100644 --- a/src/__tests__/lib/api/resources/inbound/Threads.test.ts +++ b/src/__tests__/lib/api/resources/inbound/Threads.test.ts @@ -33,10 +33,37 @@ describe("lib/api/resources/inbound/Threads: ", () => { }); it("passes last_id for pagination.", async () => { - mock.onGet(`${threadsURL}?last_id=abc123`).reply(200, listResponse); + const lastId = "WzE3NzgyNDE5MDAwMDAsIjE3MDAwMDAwMDAwMDAxMjMiXQ=="; - const result = await threadsAPI.getList(42, { last_id: "abc123" }); + mock + .onGet(threadsURL, { params: { last_id: lastId } }) + .reply(200, listResponse); + const result = await threadsAPI.getList(42, { last_id: lastId }); + + expect(mock.history.get[0].params).toEqual({ last_id: lastId }); + expect(result).toEqual(listResponse); + }); + + it("passes search to filter threads.", async () => { + mock + .onGet(threadsURL, { params: { search: "acme" } }) + .reply(200, listResponse); + + const result = await threadsAPI.getList(42, { search: "acme" }); + + expect(mock.history.get[0].params).toEqual({ search: "acme" }); + expect(result).toEqual(listResponse); + }); + + it("combines search with last_id when paginating.", async () => { + const params = { search: "acme", last_id: "abc123" }; + + mock.onGet(threadsURL, { params }).reply(200, listResponse); + + const result = await threadsAPI.getList(42, params); + + expect(mock.history.get[0].params).toEqual(params); expect(result).toEqual(listResponse); }); }); @@ -54,6 +81,53 @@ describe("lib/api/resources/inbound/Threads: ", () => { expect(result).toEqual(thread); }); + + it("returns forwards on inbound and delivery on outbound messages.", async () => { + const thread = { + id: "1700000000000124", + messages: [ + { + visibility_status: "available", + direction: "inbound", + id: "1700000000000123", + forwards: [ + { + rule_id: 7, + rule_name: "Copy to support team", + destination: "team@example.com", + status: "forwarded", + reason: null, + message_id: "f47ac10b-58cc-4372-a567-0e02b2c3d479", + }, + ], + }, + { + visibility_status: "available", + direction: "outbound", + id: "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + delivery: { + to: "customer@example.com", + status: "delivered", + delivered_at: "2026-05-08T11:40:05.000Z", + bounced_at: null, + }, + }, + { visibility_status: "placeholder", direction: "inbound" }, + ], + } as unknown as Thread; + + mock.onGet(`${threadsURL}/1700000000000124`).reply(200, thread); + + const result = await threadsAPI.get(42, "1700000000000124"); + const [received, sent, placeholder] = result.messages; + + expect(result).toEqual(thread); + expect(received.forwards?.[0].status).toEqual("forwarded"); + expect(sent.delivery?.status).toEqual("delivered"); + expect(sent.delivery?.bounced_at).toBeNull(); + expect(placeholder.forwards).toBeUndefined(); + expect(placeholder.delivery).toBeUndefined(); + }); }); describe("delete(): ", () => { diff --git a/src/lib/api/Inbound.ts b/src/lib/api/Inbound.ts index 94999af..b74328d 100644 --- a/src/lib/api/Inbound.ts +++ b/src/lib/api/Inbound.ts @@ -1,6 +1,7 @@ import { AxiosInstance } from "axios"; import FoldersApi from "./resources/inbound/Folders"; +import ForwardRulesApi from "./resources/inbound/ForwardRules"; import InboxesApi from "./resources/inbound/Inboxes"; import MessagesApi from "./resources/inbound/Messages"; import ThreadsApi from "./resources/inbound/Threads"; @@ -8,6 +9,8 @@ import ThreadsApi from "./resources/inbound/Threads"; export default class InboundAPI { public folders: FoldersApi; + public forwardRules: ForwardRulesApi; + public inboxes: InboxesApi; public messages: MessagesApi; @@ -16,6 +19,7 @@ export default class InboundAPI { constructor(client: AxiosInstance) { this.folders = new FoldersApi(client); + this.forwardRules = new ForwardRulesApi(client); this.inboxes = new InboxesApi(client); this.messages = new MessagesApi(client); this.threads = new ThreadsApi(client); diff --git a/src/lib/api/resources/inbound/ForwardRules.ts b/src/lib/api/resources/inbound/ForwardRules.ts new file mode 100644 index 0000000..cb85fd9 --- /dev/null +++ b/src/lib/api/resources/inbound/ForwardRules.ts @@ -0,0 +1,80 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../../config"; +import { + CreateForwardRuleParams, + ForwardRuleResponse, + ForwardRulesListResponse, + UpdateForwardRuleParams, +} from "../../../../types/api/inbound/forward-rules"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ForwardRulesApi { + private client: AxiosInstance; + + private inboxesURL: string; + + constructor(client: AxiosInstance) { + this.client = client; + this.inboxesURL = `${GENERAL_ENDPOINT}/api/inbound/inboxes`; + } + + private forwardRulesURL(inboxId: number) { + return `${this.inboxesURL}/${inboxId}/forward_rules`; + } + + /** + * Get all forward rules in an inbox. + */ + public async getList(inboxId: number) { + return this.client.get( + this.forwardRulesURL(inboxId) + ); + } + + /** + * Get a single forward rule by ID. + */ + public async get(inboxId: number, ruleId: number) { + const url = `${this.forwardRulesURL(inboxId)}/${ruleId}`; + + return this.client.get(url); + } + + /** + * Create a new forward rule. + */ + public async create(inboxId: number, params: CreateForwardRuleParams) { + return this.client.post( + this.forwardRulesURL(inboxId), + params + ); + } + + /** + * Update a forward rule by ID. + */ + public async update( + inboxId: number, + ruleId: number, + params: UpdateForwardRuleParams + ) { + const url = `${this.forwardRulesURL(inboxId)}/${ruleId}`; + + return this.client.patch( + url, + params + ); + } + + /** + * Delete a forward rule by ID. + */ + public async delete(inboxId: number, ruleId: number) { + const url = `${this.forwardRulesURL(inboxId)}/${ruleId}`; + + return this.client.delete(url); + } +} diff --git a/src/lib/api/resources/inbound/Threads.ts b/src/lib/api/resources/inbound/Threads.ts index 7e12130..dbf3abe 100644 --- a/src/lib/api/resources/inbound/Threads.ts +++ b/src/lib/api/resources/inbound/Threads.ts @@ -25,17 +25,19 @@ export default class ThreadsApi { } /** - * List threads in an inbox (paginated). Pass `last_id` from the previous - * response to fetch the next page. + * List threads in an inbox (paginated), optionally filtered by `search`. + * 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); + public async getList(inboxId: number, options?: ThreadsListParams) { + const params = { + ...(options?.last_id && { last_id: options.last_id }), + ...(options?.search && { search: options.search }), + }; + + return this.client.get( + this.threadsURL(inboxId), + { params } + ); } /** diff --git a/src/types/api/inbound/forward-rules.ts b/src/types/api/inbound/forward-rules.ts new file mode 100644 index 0000000..13e029b --- /dev/null +++ b/src/types/api/inbound/forward-rules.ts @@ -0,0 +1,68 @@ +export type ForwardRuleMatchType = "sender" | "recipient" | "header"; + +export type ForwardRuleOperator = + | "equal" + | "not_equal" + | "contains" + | "starts_with" + | "ends_with" + | "empty" + | "not_empty"; + +export interface ForwardRuleCondition { + match_type: ForwardRuleMatchType; + operator: ForwardRuleOperator; + value: string | null; + header_key: string | null; +} + +export interface ForwardRuleDestination { + email: string; +} + +export interface ForwardRule { + id: number; + name: string; + created_at: string; + updated_at: string; + conditions: ForwardRuleCondition[]; + destinations: ForwardRuleDestination[]; +} + +export interface ForwardRuleConditionParams { + match_type: ForwardRuleMatchType; + operator: ForwardRuleOperator; + value?: string; + header_key?: string; +} + +export interface CreateForwardRuleParams { + name: string; + conditions?: ForwardRuleConditionParams[]; + destinations?: ForwardRuleDestination[]; +} + +export interface UpdateForwardRuleParams { + name?: string; + conditions?: ForwardRuleConditionParams[]; + destinations?: ForwardRuleDestination[]; +} + +export interface ForwardRuleResponse { + data: ForwardRule; +} + +export interface ForwardRulesListResponse { + data: ForwardRule[]; +} + +export type ForwardOutcomeStatus = "forwarded" | "rejected"; + +export interface ForwardOutcome { + rule_id: number; + rule_name: string | null; + destination: string; + status: ForwardOutcomeStatus; + reason: string | null; + message_id: string | null; +} diff --git a/src/types/api/inbound/messages.ts b/src/types/api/inbound/messages.ts index 438b89a..0765e43 100644 --- a/src/types/api/inbound/messages.ts +++ b/src/types/api/inbound/messages.ts @@ -1,3 +1,5 @@ +import { ForwardOutcome } from "./forward-rules"; + export type ContentDisposition = "attachment" | "inline"; export interface Attachment { @@ -36,6 +38,7 @@ export interface Message { /** ID of the thread this message belongs to, if any. */ thread_id: string | null; attachments: Attachment[]; + forwards: ForwardOutcome[]; } export interface MessageDetails extends Omit { diff --git a/src/types/api/inbound/threads.ts b/src/types/api/inbound/threads.ts index 558831e..32b1163 100644 --- a/src/types/api/inbound/threads.ts +++ b/src/types/api/inbound/threads.ts @@ -1,8 +1,22 @@ +import { ForwardOutcome } from "./forward-rules"; import { AttachmentWithDownloadUrl } from "./messages"; export type MessageVisibilityStatus = "available" | "placeholder"; export type MessageDirection = "inbound" | "outbound"; +export type ThreadMessageDeliveryStatus = + | "delivered" + | "not_delivered" + | "enqueued" + | "opted_out"; + +export interface ThreadMessageDelivery { + to: string; + status: ThreadMessageDeliveryStatus; + delivered_at: string | null; + bounced_at: string | null; +} + export interface ThreadSummary { id: string; subject: string | null; @@ -20,8 +34,7 @@ export interface ThreadSummary { /** * 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. + * guaranteed; `placeholder` entries omit the rest. */ export interface ThreadMessage { visibility_status: MessageVisibilityStatus; @@ -42,9 +55,8 @@ export interface ThreadMessage { text_body?: string | null; html_body?: string | null; attachments?: AttachmentWithDownloadUrl[]; - delivery_status?: string | null; - delivered_at?: string | null; - bounced_at?: string | null; + delivery?: ThreadMessageDelivery; + forwards?: ForwardOutcome[]; } export interface Thread extends ThreadSummary { @@ -59,4 +71,5 @@ export interface ThreadsListResponse { export interface ThreadsListParams { last_id?: string; + search?: string; }