From 6ca84110a800def7563fb7fdf742935b899dacda Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Fri, 13 Mar 2026 12:27:01 +0000 Subject: [PATCH] Add support for Email Logs API --- CHANGELOG.md | 1 + README.md | 28 +- examples/email-logs/everything.ts | 53 ++++ package.json | 6 +- .../lib/api/resources/EmailLogs.test.ts | 268 ++++++++++++++++++ src/__tests__/lib/mailtrap-client.test.ts | 29 +- src/config/index.ts | 2 +- src/lib/MailtrapClient.ts | 15 +- src/lib/api/EmailLogs.ts | 15 + src/lib/api/resources/EmailLogs.ts | 65 +++++ src/types/api/email-logs.ts | 226 +++++++++++++++ yarn.lock | 67 ++++- 12 files changed, 742 insertions(+), 33 deletions(-) create mode 100644 examples/email-logs/everything.ts create mode 100644 src/__tests__/lib/api/resources/EmailLogs.test.ts create mode 100644 src/lib/api/EmailLogs.ts create mode 100644 src/lib/api/resources/EmailLogs.ts create mode 100644 src/types/api/email-logs.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ba225..e7208c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - Add StatsApi with get, byDomain, byCategory, byEmailServiceProvider, byDate endpoints +- Add Email Logs API ## [4.4.0] - 2025-12-08 diff --git a/README.md b/README.md index 346937d..eead63b 100644 --- a/README.md +++ b/README.md @@ -227,65 +227,43 @@ See transport usage below: Email API: - Send an email (Transactional stream) – [`sending/minimal.ts`](examples/sending/minimal.ts) - - Send an email (Bulk stream) – [`bulk/send-mail.ts`](examples/bulk/send-mail.ts) - - Send an email with a Template (Transactional) – [`sending/template.ts`](examples/sending/template.ts) - - Send an email with a Template (Bulk) – [`bulk/send-mail.ts`](examples/bulk/send-mail.ts) - - Batch send (Transactional) – [`batch/transactional.ts`](examples/batch/transactional.ts) - -- Batch send (Bulk) – [`batch/bulk.ts`](examples/batch/bulk.ts) - +- Batch send (Bulk) – [`batch/bulk.ts`](examples/bulk/bulk.ts) - Batch send with Template (Transactional) – [`batch/template.ts`](examples/batch/template.ts) - - Batch send with Template (Bulk) – [`batch/template.ts`](examples/batch/template.ts) - - Sending domain management CRUD – [`sending-domains/everything.ts`](examples/sending-domains/everything.ts) +- Sending stats (aggregated and by domain, category, ESP, date) – [`stats/everything.ts`](examples/stats/everything.ts) +- Email logs (list with filters, get by message ID) – [`email-logs/everything.ts`](examples/email-logs/everything.ts) Email Sandbox (Testing): - Send an email (Sandbox) – [`testing/send-mail.ts`](examples/testing/send-mail.ts) - - Send an email with a Template (Sandbox) – [`testing/template.ts`](examples/testing/template.ts) - - Batch send (Sandbox) – [`batch/sandbox.ts`](examples/batch/sandbox.ts) - - Batch send with Template (Sandbox) – [`batch/sandbox.ts`](examples/batch/sandbox.ts) - - Message management CRUD – [`testing/messages.ts`](examples/testing/messages.ts) - - Inbox management CRUD – [`testing/inboxes.ts`](examples/testing/inboxes.ts) - - Project management CRUD – [`testing/projects.ts`](examples/testing/projects.ts) - - Attachments operations – [`testing/attachments.ts`](examples/testing/attachments.ts) Contact management: - Contacts CRUD & listing – [`contacts/everything.ts`](examples/contacts/everything.ts) - - Contact lists CRUD – [`contact-lists/everything.ts`](examples/contact-lists/everything.ts) - - Custom fields CRUD – [`contact-fields/everything.ts`](examples/contact-fields/everything.ts) - - Import/Export – [`contact-imports/everything.ts`](examples/contact-imports/everything.ts), [`contact-exports/everything.ts`](examples/contact-exports/everything.ts) - - Events – [`contact-events/everything.ts`](examples/contact-events/everything.ts) General API: - Templates CRUD – [`templates/everything.ts`](examples/templates/everything.ts) - - Suppressions (find & delete) – [`sending/suppressions.ts`](examples/sending/suppressions.ts) - - Billing info – [`general/billing.ts`](examples/general/billing.ts) - - Accounts info – [`general/accounts.ts`](examples/general/accounts.ts) - - Permissions listing – [`general/permissions.ts`](examples/general/permissions.ts) - - Users listing – [`general/account-accesses.ts`](examples/general/account-accesses.ts) ## Contributing diff --git a/examples/email-logs/everything.ts b/examples/email-logs/everything.ts new file mode 100644 index 0000000..8e36dab --- /dev/null +++ b/examples/email-logs/everything.ts @@ -0,0 +1,53 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: Number(ACCOUNT_ID), +}); + +async function emailLogsFlow() { + try { + // List email logs (paginated) + const list = await client.emailLogs.getList(); + console.log("Email logs:", list.messages.length, "messages, total:", list.total_count); + if (list.messages.length > 0) { + console.log("First message:", list.messages[0].message_id, list.messages[0].subject); + } + + // List with filters (date range, category, status). Filter values can be single or array. + const now = new Date(); + const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000); + const filtered = await client.emailLogs.getList({ + filters: { + sent_after: twoDaysAgo.toISOString(), + sent_before: now.toISOString(), + subject: { operator: "not_empty" }, + to: { operator: "ci_equal", value: "recipient@example.com" }, + category: { operator: "equal", value: ["Welcome Email", "Forget Password"] }, + }, + }); + console.log("Filtered logs:", filtered.messages.length); + + // Next page (use search_after from previous response next_page_cursor) + if (list.next_page_cursor) { + const nextPage = await client.emailLogs.getList({ + search_after: list.next_page_cursor, + }); + console.log("Next page:", nextPage.messages.length, "messages"); + } + + // Get a single message by ID + if (list.messages.length > 0) { + const messageId = list.messages[0].message_id; + const message = await client.emailLogs.get(messageId); + console.log("Single message:", message.subject, "events:", message.events?.length ?? 0); + } + } catch (error) { + console.error("Error in emailLogsFlow:", error instanceof Error ? error.message : String(error)); + } +} + +emailLogsFlow(); diff --git a/package.json b/package.json index dc204d0..d4511e6 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "version": "4.4.0", "author": "Railsware Products Studio LLC", "dependencies": { - "axios": ">=0.27" + "axios": ">=0.27", + "qs": "^6.15.0" }, "devDependencies": { - "rimraf": "^5.0.5", "@babel/core": "^7.20.5", "@babel/preset-env": "^7.20.2", "@babel/preset-typescript": "^7.18.6", @@ -15,6 +15,7 @@ "@types/jest": "^29.5.3", "@types/node": "^18.15.11", "@types/nodemailer": "^6.4.9", + "@types/qs": "^6.15.0", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "axios-mock-adapter": "^1.21.2", @@ -28,6 +29,7 @@ "jest": "^29.3.1", "nodemailer": "^7.0.7", "prettier": "^2.6.2", + "rimraf": "^5.0.5", "ts-node": "^10.2.1", "typescript": "^5.0.3" }, diff --git a/src/__tests__/lib/api/resources/EmailLogs.test.ts b/src/__tests__/lib/api/resources/EmailLogs.test.ts new file mode 100644 index 0000000..275701b --- /dev/null +++ b/src/__tests__/lib/api/resources/EmailLogs.test.ts @@ -0,0 +1,268 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import EmailLogsApi from "../../../../lib/api/resources/EmailLogs"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; +import { + EmailLogMessage, + EmailLogsList, + EmailLogMessageDetails, +} from "../../../../types/api/email-logs"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/EmailLogs: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const emailLogsAPI = new EmailLogsApi(axios, accountId); + + const mockMessage: EmailLogMessage = { + message_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + status: "delivered", + subject: "Welcome", + from: "sender@example.com", + to: "recipient@example.com", + sent_at: "2025-01-15T10:30:00Z", + client_ip: "203.0.113.42", + category: "Welcome Email", + custom_variables: {}, + sending_stream: "transactional", + sending_domain_id: 3938, + template_id: 100, + template_variables: {}, + opens_count: 2, + clicks_count: 1, + }; + + const mockListResponse: EmailLogsList = { + messages: [mockMessage], + total_count: 1, + next_page_cursor: null, + }; + + const mockMessageDetails: EmailLogMessageDetails = { + ...mockMessage, + raw_message_url: "https://storage.example.com/signed/eml/abc?token=...", + events: [ + { + event_type: "click", + created_at: "2025-01-15T10:35:00Z", + details: { + click_url: "https://example.com/track/click/abc123", + web_ip_address: "198.51.100.50", + }, + }, + ], + }; + + describe("class EmailLogsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(emailLogsAPI).toHaveProperty("getList"); + expect(emailLogsAPI).toHaveProperty("get"); + }); + }); + }); + + beforeAll(() => { + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("getList(): ", () => { + it("successfully gets email logs with no params.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, mockListResponse); + const result = await emailLogsAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(mockListResponse); + }); + + it("successfully gets email logs with search_after.", async () => { + const searchAfter = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; + const baseUrl = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + const expectedUrl = `${baseUrl}?search_after=a1b2c3d4-e5f6-7890-abcd-ef1234567890`; + + expect.assertions(2); + + mock.onGet(expectedUrl).reply(200, mockListResponse); + const result = await emailLogsAPI.getList({ search_after: searchAfter }); + + expect(mock.history.get[0].url).toEqual(expectedUrl); + expect(result).toEqual(mockListResponse); + }); + + it("successfully gets email logs with filters (deepObject style).", async () => { + const sentAfter = "2025-01-01T00:00:00Z"; + const sentBefore = "2025-01-31T23:59:59Z"; + const baseUrl = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + const expectedQuery = + "filters[sent_after]=2025-01-01T00%3A00%3A00Z" + + "&filters[sent_before]=2025-01-31T23%3A59%3A59Z" + + "&filters[to][operator]=ci_equal" + + "&filters[to][value]=recipient%40example.com"; + const expectedUrl = `${baseUrl}?${expectedQuery}`; + + expect.assertions(2); + + mock.onGet(expectedUrl).reply(200, mockListResponse); + const result = await emailLogsAPI.getList({ + filters: { + sent_after: sentAfter, + sent_before: sentBefore, + to: { operator: "ci_equal", value: "recipient@example.com" }, + }, + }); + + expect(mock.history.get[0].url).toEqual(expectedUrl); + expect(result).toEqual(mockListResponse); + }); + + it("successfully gets email logs with filter value as array (e.g. category).", async () => { + const baseUrl = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + const expectedQuery = + "filters[category][operator]=equal" + + "&filters[category][value]=Welcome%20Email" + + "&filters[category][value]=Forget%20Password"; + const expectedUrl = `${baseUrl}?${expectedQuery}`; + + expect.assertions(2); + + mock.onGet(expectedUrl).reply(200, mockListResponse); + const result = await emailLogsAPI.getList({ + filters: { + category: { + operator: "equal", + value: ["Welcome Email", "Forget Password"], + }, + }, + }); + + expect(mock.history.get[0].url).toEqual(expectedUrl); + expect(result).toEqual(mockListResponse); + }); + + it("fails with unauthorized error (401).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + const expectedErrorMessage = "Incorrect API token"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(401, { error: expectedErrorMessage }); + + try { + await emailLogsAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + + it("fails with bad request (400).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + const expectedErrorMessage = "Invalid request parameters"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(400, { errors: [expectedErrorMessage] }); + + try { + await emailLogsAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + + it("fails with rate limit exceeded (429).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + const expectedErrorMessage = "Rate limit exceeded"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(429, { errors: [expectedErrorMessage] }); + + try { + await emailLogsAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("get(): ", () => { + const messageId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; + + it("successfully gets a single email log message by ID.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs/${messageId}`; + + expect.assertions(4); + + mock.onGet(endpoint).reply(200, mockMessageDetails); + const result = await emailLogsAPI.get(messageId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(mockMessageDetails); + expect(result.raw_message_url).toBeDefined(); + expect(result.events).toHaveLength(1); + }); + + it("fails with not found (404).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs/${messageId}`; + const expectedErrorMessage = "Resource not found"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await emailLogsAPI.get(messageId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + + it("fails with rate limit exceeded (429).", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs/${messageId}`; + const expectedErrorMessage = "Rate limit exceeded"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(429, { errors: [expectedErrorMessage] }); + + try { + await emailLogsAPI.get(messageId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index e28390f..70513f1 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -15,6 +15,7 @@ import ContactExportsBaseAPI from "../../lib/api/ContactExports"; import TemplatesBaseAPI from "../../lib/api/Templates"; import SuppressionsBaseAPI from "../../lib/api/Suppressions"; import SendingDomainsBaseAPI from "../../lib/api/SendingDomains"; +import EmailLogsBaseAPI from "../../lib/api/EmailLogs"; import ContactEventsBaseAPI from "../../lib/api/ContactEvents"; const { ERRORS, CLIENT_SETTINGS } = CONFIG; @@ -912,7 +913,7 @@ describe("lib/mailtrap-client: ", () => { }); expect(() => client.sendingDomains).toThrow( - "accountId is missing, some features of testing API may not work properly." + "accountId is missing, please provide a valid accountId." ); }); @@ -929,6 +930,32 @@ describe("lib/mailtrap-client: ", () => { }); }); + describe("get emailLogs(): ", () => { + it("rejects with Mailtrap error, when `accountId` is missing.", () => { + expect.assertions(1); + + const client = new MailtrapClient({ + token: "test-token", + }); + + expect(() => client.emailLogs).toThrow( + "accountId is missing, please provide a valid accountId." + ); + }); + + it("returns email logs API object when accountId is provided.", () => { + expect.assertions(1); + + const client = new MailtrapClient({ + token: "test-token", + accountId: 123, + }); + + const emailLogsClient = client.emailLogs; + expect(emailLogsClient).toBeInstanceOf(EmailLogsBaseAPI); + }); + }); + describe("get contactEvents(): ", () => { it("rejects with Mailtrap error, when `accountId` is missing.", () => { const client = new MailtrapClient({ diff --git a/src/config/index.ts b/src/config/index.ts index f1b5ad2..e03712b 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -8,7 +8,7 @@ export default { NO_DATA_ERROR: "No Data.", TEST_INBOX_ID_MISSING: "testInboxId is missing, testing API will not work.", ACCOUNT_ID_MISSING: - "accountId is missing, some features of testing API may not work properly.", + "accountId is missing, please provide a valid accountId.", BULK_SANDBOX_INCOMPATIBLE: "Bulk mode is not applicable for sandbox API.", }, CLIENT_SETTINGS: { diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index c1544ff..e5037c1 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -7,17 +7,18 @@ import encodeMailBuffers from "./mail-buffer-encoder"; import handleSendingError from "./axios-logger"; import MailtrapError from "./MailtrapError"; -import ContactsBaseAPI from "./api/Contacts"; import ContactEventsBaseAPI from "./api/ContactEvents"; import ContactExportsBaseAPI from "./api/ContactExports"; import ContactFieldsBaseAPI from "./api/ContactFields"; import ContactImportsBaseAPI from "./api/ContactImports"; import ContactListsBaseAPI from "./api/ContactLists"; +import ContactsBaseAPI from "./api/Contacts"; +import EmailLogsBaseAPI from "./api/EmailLogs"; import GeneralAPI from "./api/General"; -import TemplatesBaseAPI from "./api/Templates"; -import SuppressionsBaseAPI from "./api/Suppressions"; import SendingDomainsBaseAPI from "./api/SendingDomains"; import StatsBaseAPI from "./api/Stats"; +import SuppressionsBaseAPI from "./api/Suppressions"; +import TemplatesBaseAPI from "./api/Templates"; import TestingAPI from "./api/Testing"; import CONFIG from "../config"; @@ -206,6 +207,14 @@ export default class MailtrapClient { return new SendingDomainsBaseAPI(this.axios, accountId); } + /** + * Getter for Email Logs API. + */ + get emailLogs() { + const accountId = this.validateAccountIdPresence(); + return new EmailLogsBaseAPI(this.axios, accountId); + } + /** * Returns configured host. Checks if `bulk` and `sandbox` modes are activated simultaneously, * then reject with Mailtrap Error. diff --git a/src/lib/api/EmailLogs.ts b/src/lib/api/EmailLogs.ts new file mode 100644 index 0000000..354c4cf --- /dev/null +++ b/src/lib/api/EmailLogs.ts @@ -0,0 +1,15 @@ +import { AxiosInstance } from "axios"; + +import EmailLogsApi from "./resources/EmailLogs"; + +export default class EmailLogsBaseAPI { + public getList: EmailLogsApi["getList"]; + + public get: EmailLogsApi["get"]; + + constructor(client: AxiosInstance, accountId: number) { + const emailLogs = new EmailLogsApi(client, accountId); + this.getList = emailLogs.getList.bind(emailLogs); + this.get = emailLogs.get.bind(emailLogs); + } +} diff --git a/src/lib/api/resources/EmailLogs.ts b/src/lib/api/resources/EmailLogs.ts new file mode 100644 index 0000000..856b964 --- /dev/null +++ b/src/lib/api/resources/EmailLogs.ts @@ -0,0 +1,65 @@ +import { AxiosInstance } from "axios"; +import qs from "qs"; + +import CONFIG from "../../../config"; +import { + EmailLogsList, + EmailLogsListParams, + EmailLogMessageDetails, +} from "../../../types/api/email-logs"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +/** + * Serialize query params for email logs list. Uses qs for deepObject-style + * bracket notation (e.g. filters[sent_after]=..., filters[to][operator]=...) + * with repeated keys for array values. + */ +function serializeEmailLogsParams(params: EmailLogsListParams): string { + const query: Record = {}; + if (params.search_after != null) { + query.search_after = params.search_after; + } + if (params.filters && typeof params.filters === "object") { + query.filters = params.filters; + } + return qs.stringify(query, { + arrayFormat: "repeat", + encode: true, + encodeValuesOnly: true, + }); +} + +export default class EmailLogsApi { + private client: AxiosInstance; + + private emailLogsURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.emailLogsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_logs`; + } + + /** + * List email logs (paginated). Results are ordered by sent_at descending. + * Use search_after with next_page_cursor from the previous response for the next page. + */ + public async getList(params?: EmailLogsListParams) { + const url = + params && (params.search_after || params.filters) + ? `${this.emailLogsURL}?${serializeEmailLogsParams(params)}` + : this.emailLogsURL; + + return this.client.get(url); + } + + /** + * Get a single email log message by message ID. + */ + public async get(sendingMessageId: string) { + const url = `${this.emailLogsURL}/${sendingMessageId}`; + + return this.client.get(url); + } +} diff --git a/src/types/api/email-logs.ts b/src/types/api/email-logs.ts new file mode 100644 index 0000000..38fbedb --- /dev/null +++ b/src/types/api/email-logs.ts @@ -0,0 +1,226 @@ +export type EmailLogMessageStatus = + | "delivered" + | "not_delivered" + | "enqueued" + | "opted_out"; + +export type EmailLogMessage = { + message_id: string; + status: EmailLogMessageStatus; + subject: string | null; + from: string; + to: string; + sent_at: string; + client_ip: string | null; + category: string | null; + custom_variables: Record; + sending_stream: "transactional" | "bulk"; + sending_domain_id: number; + template_id: number | null; + template_variables: Record; + opens_count: number; + clicks_count: number; +}; + +export type EmailLogsList = { + messages: EmailLogMessage[]; + total_count: number; + next_page_cursor: string | null; +}; + +export type EmailLogMessageDetails = EmailLogMessage & { + raw_message_url?: string; + events?: EmailLogMessageEvent[]; +}; + +/** Discriminated union: use event_type to narrow. */ +export type EmailLogMessageEvent = + | EmailLogMessageEventDelivery + | EmailLogMessageEventOpen + | EmailLogMessageEventClick + | EmailLogMessageEventBounce + | EmailLogMessageEventSpam + | EmailLogMessageEventUnsubscribe + | EmailLogMessageEventReject; + +export type EmailLogMessageEventDeliveryDetails = { + sending_ip: string | null; + recipient_mx: string | null; + email_service_provider: string | null; +}; + +export type EmailLogMessageEventDelivery = { + event_type: "delivery"; + created_at: string; + details: EmailLogMessageEventDeliveryDetails; +}; + +export type EmailLogMessageEventOpenDetails = { + web_ip_address: string | null; +}; + +export type EmailLogMessageEventOpen = { + event_type: "open"; + created_at: string; + details: EmailLogMessageEventOpenDetails; +}; + +export type EmailLogMessageEventClickDetails = { + click_url: string | null; + web_ip_address: string | null; +}; + +export type EmailLogMessageEventClick = { + event_type: "click"; + created_at: string; + details: EmailLogMessageEventClickDetails; +}; + +export type EmailLogMessageEventBounceDetails = { + sending_ip: string | null; + recipient_mx: string | null; + email_service_provider: string | null; + email_service_provider_status: string | null; + email_service_provider_response: string | null; + bounce_category: string | null; +}; + +export type EmailLogMessageEventBounce = { + event_type: "soft_bounce" | "bounce"; + created_at: string; + details: EmailLogMessageEventBounceDetails; +}; + +export type EmailLogMessageEventSpamDetails = { + spam_feedback_type: string | null; +}; + +export type EmailLogMessageEventSpam = { + event_type: "spam"; + created_at: string; + details: EmailLogMessageEventSpamDetails; +}; + +export type EmailLogMessageEventUnsubscribeDetails = { + web_ip_address: string | null; +}; + +export type EmailLogMessageEventUnsubscribe = { + event_type: "unsubscribe"; + created_at: string; + details: EmailLogMessageEventUnsubscribeDetails; +}; + +export type EmailLogMessageEventRejectDetails = { + reject_reason: string | null; +}; + +export type EmailLogMessageEventReject = { + event_type: "suspension" | "reject"; + created_at: string; + details: EmailLogMessageEventRejectDetails; +}; + +/** Filter types for list params (deepObject query). */ + +export type FilterTo = { + operator: "ci_contain" | "ci_not_contain" | "ci_equal" | "ci_not_equal"; + value: string | string[]; +}; + +export type FilterFrom = { + operator: "ci_contain" | "ci_not_contain" | "ci_equal" | "ci_not_equal"; + value: string | string[]; +}; + +export type FilterSubject = { + operator: + | "ci_contain" + | "ci_not_contain" + | "ci_equal" + | "ci_not_equal" + | "empty" + | "not_empty"; + value?: string | string[]; +}; + +export type FilterStatus = { + operator: "equal" | "not_equal"; + value: EmailLogMessageStatus | EmailLogMessageStatus[]; +}; + +export type EmailLogEventType = + | "delivery" + | "open" + | "click" + | "bounce" + | "spam" + | "unsubscribe" + | "soft_bounce" + | "reject" + | "suspension"; + +export type FilterEvents = { + operator: "include_event" | "not_include_event"; + value: EmailLogEventType | EmailLogEventType[]; +}; + +export type FilterNumber = { + operator: "equal" | "greater_than" | "less_than"; + value: number; +}; + +export type FilterClientIp = { + operator: "equal" | "not_equal" | "contain" | "not_contain"; + value: string | string[]; +}; + +export type FilterSendingIp = { + operator: "equal" | "not_equal" | "contain" | "not_contain"; + value: string | string[]; +}; + +export type FilterMandatoryText = { + operator: "ci_contain" | "ci_not_contain" | "ci_equal" | "ci_not_equal"; + value: string | string[]; +}; + +export type FilterExact = { + operator: "equal" | "not_equal"; + value: string | string[]; +}; + +export type FilterSendingDomainId = { + operator: "equal" | "not_equal"; + value: number | number[]; +}; + +export type FilterSendingStream = { + operator: "equal" | "not_equal"; + value: ("transactional" | "bulk") | ("transactional" | "bulk")[]; +}; + +export type EmailLogsListFilters = { + sent_after?: string; + sent_before?: string; + to?: FilterTo; + from?: FilterFrom; + subject?: FilterSubject; + status?: FilterStatus; + events?: FilterEvents; + clicks_count?: FilterNumber; + opens_count?: FilterNumber; + client_ip?: FilterClientIp; + sending_ip?: FilterSendingIp; + email_service_provider_response?: FilterMandatoryText; + email_service_provider?: FilterExact; + recipient_mx?: FilterMandatoryText; + category?: FilterExact; + sending_domain_id?: FilterSendingDomainId; + sending_stream?: FilterSendingStream; +}; + +export type EmailLogsListParams = { + search_after?: string; + filters?: EmailLogsListFilters; +}; diff --git a/yarn.lock b/yarn.lock index 3797e45..9c085ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1564,6 +1564,11 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== +"@types/qs@^6.15.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.15.0.tgz#963ab61779843fe910639a50661b48f162bc7f79" + integrity sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow== + "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -2021,6 +2026,14 @@ call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2926,7 +2939,7 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -4108,6 +4121,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -4357,6 +4375,13 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@^6.15.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.0.tgz#db8fd5d1b1d2d6b5b33adaf87429805f1909e7b3" + integrity sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ== + dependencies: + side-channel "^1.1.0" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -4584,6 +4609,35 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -4593,6 +4647,17 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"