Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
50 changes: 50 additions & 0 deletions examples/inbound/forward-rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { MailtrapClient } from "mailtrap";

const TOKEN = "<YOUR-TOKEN-HERE>";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be better to read from environment... as a general practice across all OSS examples. I raised it elsewhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fixed separately. 43 or 44 examples in this repo use this pattern.

const INBOX_ID = Number("<YOUR-INBOX-ID-HERE>");

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);
Comment thread
mklocek marked this conversation as resolved.
console.log("Deleted forward rule", ruleId);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
}
}

forwardRulesFlow();
2 changes: 2 additions & 0 deletions examples/inbound/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 23 additions & 3 deletions examples/inbound/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/lib/api/Inbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
205 changes: 205 additions & 0 deletions src/__tests__/lib/api/resources/inbound/ForwardRules.test.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>) => {
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));
});
});
});
40 changes: 40 additions & 0 deletions src/__tests__/lib/api/resources/inbound/Messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(): ", () => {
Expand Down
Loading
Loading