From 15e5327024ef5251ad8d9f3b9bf2b6b71419f32a Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sun, 25 May 2025 20:44:13 +1000 Subject: [PATCH] feat: add In-Reply-To option (#165) --- apps/docs/api-reference/openapi.json | 8 + apps/web/package.json | 2 +- .../migration.sql | 2 + apps/web/prisma/schema.prisma | 1 + apps/web/src/server/aws/ses.ts | 147 +-- .../public-api/api/emails/send-email.ts | 1 - apps/web/src/server/public-api/hono.ts | 1 - .../server/public-api/schemas/email-schema.ts | 1 + .../src/server/service/email-queue-service.ts | 59 +- apps/web/src/server/service/email-service.ts | 18 + apps/web/src/types/index.ts | 3 +- packages/sdk/package.json | 2 +- packages/sdk/types/schema.d.ts | 1085 ++++++++++------- pnpm-lock.yaml | 16 +- 14 files changed, 754 insertions(+), 592 deletions(-) create mode 100644 apps/web/prisma/migrations/20250523232535_add_in_reply_to/migration.sql diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 2828c42..3fa3cfc 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -605,6 +605,10 @@ "scheduledAt": { "type": "string", "format": "date-time" + }, + "inReplyToId": { + "type": "string", + "nullable": true } }, "required": [ @@ -758,6 +762,10 @@ "scheduledAt": { "type": "string", "format": "date-time" + }, + "inReplyToId": { + "type": "string", + "nullable": true } }, "required": [ diff --git a/apps/web/package.json b/apps/web/package.json index d5d3497..89409f4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,7 +23,7 @@ "@aws-sdk/client-sns": "^3.797.0", "@aws-sdk/s3-request-presigner": "^3.797.0", "@hono/swagger-ui": "^0.5.1", - "@hono/zod-openapi": "^0.19.5", + "@hono/zod-openapi": "^0.10.0", "@hookform/resolvers": "^5.0.1", "@isaacs/ttlcache": "^1.4.1", "@prisma/client": "^6.6.0", diff --git a/apps/web/prisma/migrations/20250523232535_add_in_reply_to/migration.sql b/apps/web/prisma/migrations/20250523232535_add_in_reply_to/migration.sql new file mode 100644 index 0000000..da3d102 --- /dev/null +++ b/apps/web/prisma/migrations/20250523232535_add_in_reply_to/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Email" ADD COLUMN "inReplyToId" TEXT; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index f53f6e1..269882e 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -245,6 +245,7 @@ model Email { attachments String? campaignId String? contactId String? + inReplyToId String? team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) emailEvents EmailEvent[] diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 4328be7..ab76b9d 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -103,96 +103,7 @@ export async function getDomainIdentity(domain: string, region: string) { return response; } -export async function sendEmailThroughSes({ - to, - from, - subject, - cc, - bcc, - text, - html, - replyTo, - region, - configurationSetName, - unsubUrl, - isBulk, -}: Partial & { - region: string; - configurationSetName: string; - cc?: string[]; - bcc?: string[]; - replyTo?: string[]; - to?: string[]; - isBulk?: boolean; -}) { - const sesClient = getSesClient(region); - const command = new SendEmailCommand({ - FromEmailAddress: from, - ReplyToAddresses: replyTo ? replyTo : undefined, - Destination: { - ToAddresses: to, - CcAddresses: cc, - BccAddresses: bcc, - }, - Content: { - // EmailContent - Simple: { - // Message - Subject: { - // Content - Data: subject, // required - Charset: "UTF-8", - }, - Body: { - // Body - Text: text - ? { - Data: text, // required - Charset: "UTF-8", - } - : undefined, - Html: html - ? { - Data: html, // required - Charset: "UTF-8", - } - : undefined, - }, - Headers: [ - // Spread in any unsubscribe headers if unsubUrl is defined - ...(unsubUrl - ? [ - { Name: "List-Unsubscribe", Value: `<${unsubUrl}>` }, - { - Name: "List-Unsubscribe-Post", - Value: "List-Unsubscribe=One-Click", - }, - ] - : []), - // Spread in the precedence header if present - ...(isBulk ? [{ Name: "Precedence", Value: "bulk" }] : []), - { - Name: "X-Entity-Ref-ID", - Value: nanoid(), - }, - ], - }, - }, - ConfigurationSetName: configurationSetName, - }); - - try { - const response = await sesClient.send(command); - console.log("Email sent! Message ID:", response.MessageId); - return response.MessageId; - } catch (error) { - console.error("Failed to send email", error); - throw error; - } -} - -// Need to improve this. Use some kinda library to do this -export async function sendEmailWithAttachments({ +export async function sendRawEmail({ to, from, subject, @@ -200,22 +111,28 @@ export async function sendEmailWithAttachments({ cc, bcc, // eslint-disable-next-line no-unused-vars - text, + text, // text is not used directly in raw email but kept for interface consistency html, attachments, region, configurationSetName, + unsubUrl, + isBulk, + inReplyToMessageId, }: Partial & { region: string; configurationSetName: string; - attachments: { filename: string; content: string }[]; + attachments?: { filename: string; content: string }[]; // Made attachments optional cc?: string[]; bcc?: string[]; replyTo?: string[]; to?: string[]; + unsubUrl?: string; + isBulk?: boolean; + inReplyToMessageId?: string; }) { const sesClient = getSesClient(region); - const boundary = "NextPart"; + const boundary = `NextPart`; let rawEmail = `From: ${from}\n`; rawEmail += `To: ${Array.isArray(to) ? to.join(", ") : to}\n`; rawEmail += cc && cc.length ? `Cc: ${cc.join(", ")}\n` : ""; @@ -224,19 +141,37 @@ export async function sendEmailWithAttachments({ replyTo && replyTo.length ? `Reply-To: ${replyTo.join(", ")}\n` : ""; rawEmail += `Subject: ${subject}\n`; rawEmail += `MIME-Version: 1.0\n`; + + // Add headers + if (unsubUrl) { + rawEmail += `List-Unsubscribe: <${unsubUrl}>\n`; + rawEmail += `List-Unsubscribe-Post: List-Unsubscribe=One-Click\n`; + } + if (isBulk) { + rawEmail += `Precedence: bulk\n`; + } + if (inReplyToMessageId) { + rawEmail += `In-Reply-To: <${inReplyToMessageId}@email.amazonses.com>\n`; + rawEmail += `References: <${inReplyToMessageId}@email.amazonses.com>\n`; + } + rawEmail += `X-Entity-Ref-ID: ${nanoid()}\n`; + rawEmail += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; rawEmail += `--${boundary}\n`; rawEmail += `Content-Type: text/html; charset="UTF-8"\n\n`; rawEmail += `${html}\n\n`; - for (const attachment of attachments) { - const content = attachment.content; // Convert buffer to base64 - const mimeType = - mime.lookup(attachment.filename) || "application/octet-stream"; - rawEmail += `--${boundary}\n`; - rawEmail += `Content-Type: ${mimeType}; name="${attachment.filename}"\n`; - rawEmail += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; - rawEmail += `Content-Transfer-Encoding: base64\n\n`; - rawEmail += `${content}\n\n`; + + if (attachments && attachments.length > 0) { + for (const attachment of attachments) { + const content = attachment.content; // Assumes content is base64 + const mimeType = + mime.lookup(attachment.filename) || "application/octet-stream"; + rawEmail += `--${boundary}\n`; + rawEmail += `Content-Type: ${mimeType}; name="${attachment.filename}"\n`; + rawEmail += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; + rawEmail += `Content-Transfer-Encoding: base64\n\n`; + rawEmail += `${content}\n\n`; + } } rawEmail += `--${boundary}--`; @@ -252,11 +187,13 @@ export async function sendEmailWithAttachments({ try { const response = await sesClient.send(command); - console.log("Email with attachments sent! Message ID:", response.MessageId); + console.log("Email sent! Message ID:", response.MessageId); return response.MessageId; } catch (error) { - console.error("Failed to send email with attachments", error); - throw new Error("Failed to send email with attachments"); + console.error("Failed to send email", error); + // It's better to throw the original error or a new error with more context + // throw new Error("Failed to send email"); + throw error; } } diff --git a/apps/web/src/server/public-api/api/emails/send-email.ts b/apps/web/src/server/public-api/api/emails/send-email.ts index d4952e0..5a6164c 100644 --- a/apps/web/src/server/public-api/api/emails/send-email.ts +++ b/apps/web/src/server/public-api/api/emails/send-email.ts @@ -1,6 +1,5 @@ import { createRoute, z } from "@hono/zod-openapi"; import { PublicAPIApp } from "~/server/public-api/hono"; -import { getTeamFromToken } from "~/server/public-api/auth"; import { sendEmail } from "~/server/service/email-service"; import { emailSchema } from "../../schemas/email-schema"; diff --git a/apps/web/src/server/public-api/hono.ts b/apps/web/src/server/public-api/hono.ts index 5a6186b..153a9d7 100644 --- a/apps/web/src/server/public-api/hono.ts +++ b/apps/web/src/server/public-api/hono.ts @@ -70,7 +70,6 @@ export function getApp() { let currentRequests: number; let ttl: number; - let isNewKey = false; try { // Increment the key. If the key does not exist, it is created and set to 1. diff --git a/apps/web/src/server/public-api/schemas/email-schema.ts b/apps/web/src/server/public-api/schemas/email-schema.ts index 0629fe2..6d66df5 100644 --- a/apps/web/src/server/public-api/schemas/email-schema.ts +++ b/apps/web/src/server/public-api/schemas/email-schema.ts @@ -29,6 +29,7 @@ export const emailSchema = z .max(10) // Limit attachments array size if desired .optional(), scheduledAt: z.string().datetime({ offset: true }).optional(), // Ensure ISO 8601 format with offset + inReplyToId: z.string().optional().nullable(), }) .refine( (data) => !!data.subject || !!data.templateId, diff --git a/apps/web/src/server/service/email-queue-service.ts b/apps/web/src/server/service/email-queue-service.ts index b94c6d9..40edc34 100644 --- a/apps/web/src/server/service/email-queue-service.ts +++ b/apps/web/src/server/service/email-queue-service.ts @@ -4,7 +4,7 @@ import { EmailAttachment } from "~/types"; import { convert as htmlToText } from "html-to-text"; import { getConfigurationSetName } from "~/utils/ses-utils"; import { db } from "../db"; -import { sendEmailThroughSes, sendEmailWithAttachments } from "../aws/ses"; +import { sendRawEmail } from "../aws/ses"; import { getRedis } from "../redis"; import { DEFAULT_QUEUE_OPTIONS } from "../queue/queue-constants"; import { Prisma } from "@prisma/client"; @@ -331,34 +331,37 @@ async function executeEmail( ? htmlToText(email.html) : undefined; + let inReplyToMessageId: string | undefined = undefined; + + if (email.inReplyToId) { + const replyEmail = await db.email.findUnique({ + where: { + id: email.inReplyToId, + }, + }); + + if (replyEmail && replyEmail.sesEmailId) { + inReplyToMessageId = replyEmail.sesEmailId; + } + } + try { - const messageId = attachments.length - ? await sendEmailWithAttachments({ - to: email.to, - from: email.from, - subject: email.subject, - replyTo: email.replyTo ?? undefined, - bcc: email.bcc, - cc: email.cc, - text, - html: email.html ?? undefined, - region: domain?.region ?? env.AWS_DEFAULT_REGION, - configurationSetName, - attachments, - }) - : await sendEmailThroughSes({ - to: email.to, - from: email.from, - subject: email.subject, - replyTo: email.replyTo ?? undefined, - text, - html: email.html ?? undefined, - region: domain?.region ?? env.AWS_DEFAULT_REGION, - configurationSetName, - attachments, - unsubUrl, - isBulk, - }); + const messageId = await sendRawEmail({ + to: email.to, + from: email.from, + subject: email.subject, + replyTo: email.replyTo ?? undefined, + bcc: email.bcc, + cc: email.cc, + text, + html: email.html ?? undefined, + region: domain?.region ?? env.AWS_DEFAULT_REGION, + configurationSetName, + attachments: attachments.length > 0 ? attachments : undefined, + unsubUrl, + isBulk, + inReplyToMessageId, + }); // Delete attachments after sending the email await db.email.update({ diff --git a/apps/web/src/server/service/email-service.ts b/apps/web/src/server/service/email-service.ts index 8cb66c1..b8e74ec 100644 --- a/apps/web/src/server/service/email-service.ts +++ b/apps/web/src/server/service/email-service.ts @@ -63,6 +63,7 @@ export async function sendEmail( bcc, scheduledAt, apiKeyId, + inReplyToId, } = emailContent; let subject = subjectFromApiCall; let html = htmlFromApiCall; @@ -99,6 +100,22 @@ export async function sendEmail( } } + if (inReplyToId) { + const email = await db.email.findUnique({ + where: { + id: inReplyToId, + teamId, + }, + }); + + if (!email) { + throw new UnsendApiError({ + code: "BAD_REQUEST", + message: '"inReplyTo" is invalid', + }); + } + } + if (!text && !html) { throw new UnsendApiError({ code: "BAD_REQUEST", @@ -131,6 +148,7 @@ export async function sendEmail( scheduledAt: scheduledAtDate, latestStatus: scheduledAtDate ? "SCHEDULED" : "QUEUED", apiId: apiKeyId, + inReplyToId, }, }); diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index f9921f6..bbfb753 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -3,7 +3,7 @@ export type EmailContent = { from: string; subject?: string; templateId?: string; - variables?: Record, + variables?: Record; text?: string; html?: string; replyTo?: string | string[]; @@ -12,6 +12,7 @@ export type EmailContent = { attachments?: Array; unsubUrl?: string; scheduledAt?: string; + inReplyToId?: string | null; }; export type EmailAttachment = { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 59604c9..e6ff157 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "unsend", - "version": "1.5.0", + "version": "1.5.1", "description": "", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/sdk/types/schema.d.ts b/packages/sdk/types/schema.d.ts index fd5cfa0..314c82d 100644 --- a/packages/sdk/types/schema.d.ts +++ b/packages/sdk/types/schema.d.ts @@ -3,484 +3,677 @@ * Do not make direct changes to the file. */ - export interface paths { - "/v1/domains": { - get: { - responses: { - /** @description Retrieve the user */ - 200: { - content: { - "application/json": ({ - /** - * @description The ID of the domain - * @example 1 - */ - id: number; - /** - * @description The name of the domain - * @example example.com - */ - name: string; - /** - * @description The ID of the team - * @example 1 - */ - teamId: number; - /** @enum {string} */ - status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE"; - /** @default us-east-1 */ - region?: string; - /** @default false */ - clickTracking?: boolean; - /** @default false */ - openTracking?: boolean; - publicKey: string; - dkimStatus?: string | null; - spfDetails?: string | null; - createdAt: string; - updatedAt: string; - /** @default false */ - dmarcAdded?: boolean; - /** @default false */ - isVerifying?: boolean; - errorMessage?: string | null; - subdomain?: string | null; - })[]; - }; + "/v1/domains": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - }; - post: { - requestBody: { - content: { - "application/json": { - name: string; - region: string; - }; - }; - }; - responses: { - /** @description Create a new domain */ - 200: { - content: { - "application/json": { - /** - * @description The ID of the domain - * @example 1 - */ - id: number; - /** - * @description The name of the domain - * @example example.com - */ - name: string; - /** - * @description The ID of the team - * @example 1 - */ - teamId: number; - /** @enum {string} */ - status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE"; - /** @default us-east-1 */ - region?: string; - /** @default false */ - clickTracking?: boolean; - /** @default false */ - openTracking?: boolean; - publicKey: string; - dkimStatus?: string | null; - spfDetails?: string | null; - createdAt: string; - updatedAt: string; - /** @default false */ - dmarcAdded?: boolean; - /** @default false */ - isVerifying?: boolean; - errorMessage?: string | null; - subdomain?: string | null; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - }; - }; - }; - }; - "/v1/domains/{id}/verify": { - put: { - parameters: { - path: { - id: number | null; - }; - }; - responses: { - /** @description Create a new domain */ - 200: { - content: { - "application/json": { - message: string; - }; - }; - }; - }; - }; - }; - "/v1/emails/{emailId}": { - get: { - parameters: { - path: { - emailId: string; - }; - }; - responses: { - /** @description Retrieve the email */ - 200: { - content: { - "application/json": { - id: string; - teamId: number; - to: string | string[]; - replyTo?: string | string[]; - cc?: string | string[]; - bcc?: string | string[]; - from: string; - subject: string; - html: string | null; - text: string | null; - createdAt: string; - updatedAt: string; - emailEvents: ({ - emailId: string; - /** @enum {string} */ - status: "SCHEDULED" | "QUEUED" | "SENT" | "DELIVERY_DELAYED" | "BOUNCED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERED" | "OPENED" | "CLICKED" | "COMPLAINED" | "FAILED" | "CANCELLED"; - createdAt: string; - data?: unknown; - })[]; - }; - }; - }; - }; - }; - patch: { - parameters: { - path: { - emailId: string; - }; - }; - requestBody: { - content: { - "application/json": { - /** Format: date-time */ - scheduledAt: string; - }; - }; - }; - responses: { - /** @description Retrieve the user */ - 200: { - content: { - "application/json": { - emailId?: string; - }; - }; - }; - }; - }; - }; - "/v1/emails": { - post: { - requestBody: { - content: { - "application/json": { - to: string | string[]; - /** Format: email */ - from: string; - /** @description Optional when templateId is provided */ - subject?: string; - /** @description ID of a template from the dashboard */ - templateId?: string; - variables?: { - [key: string]: string; - }; - replyTo?: string | string[]; - cc?: string | string[]; - bcc?: string | string[]; - text?: string | null; - html?: string | null; - attachments?: { - filename: string; - content: string; - }[]; - /** Format: date-time */ - scheduledAt?: string; - }; - }; - }; - responses: { - /** @description Retrieve the user */ - 200: { - content: { - "application/json": { - emailId?: string; - }; - }; - }; - }; - }; - }; - "/v1/emails/batch": { - post: { - requestBody: { - content: { - "application/json": ({ - to: string | string[]; - /** Format: email */ - from: string; - /** @description Optional when templateId is provided */ - subject?: string; - /** @description ID of a template from the dashboard */ - templateId?: string; - variables?: { - [key: string]: string; - }; - replyTo?: string | string[]; - cc?: string | string[]; - bcc?: string | string[]; - text?: string | null; - html?: string | null; - attachments?: { - filename: string; - content: string; - }[]; - /** Format: date-time */ - scheduledAt?: string; - })[]; - }; - }; - responses: { - /** @description List of successfully created email IDs */ - 200: { - content: { - "application/json": { - data: { - emailId: string; - }[]; - }; - }; - }; - }; - }; - }; - "/v1/emails/{emailId}/cancel": { - post: { - parameters: { - path: { - emailId: string; - }; - }; - responses: { - /** @description Retrieve the user */ - 200: { - content: { - "application/json": { - emailId?: string; - }; - }; - }; - }; - }; - }; - "/v1/contactBooks/{contactBookId}/contacts": { - get: { - parameters: { - query?: { - emails?: string; - page?: number; - limit?: number; - ids?: string; - }; - path: { - contactBookId: string; - }; - }; - responses: { - /** @description Retrieve multiple contacts */ - 200: { - content: { - "application/json": ({ - id: string; - firstName?: string | null; - lastName?: string | null; - email: string; - /** @default true */ - subscribed?: boolean; - properties: { - [key: string]: string; + requestBody?: never; + responses: { + /** @description Retrieve the user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** + * @description The ID of the domain + * @example 1 + */ + id: number; + /** + * @description The name of the domain + * @example example.com + */ + name: string; + /** + * @description The ID of the team + * @example 1 + */ + teamId: number; + /** @enum {string} */ + status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE"; + /** @default us-east-1 */ + region: string; + /** @default false */ + clickTracking: boolean; + /** @default false */ + openTracking: boolean; + publicKey: string; + dkimStatus?: string | null; + spfDetails?: string | null; + createdAt: string; + updatedAt: string; + /** @default false */ + dmarcAdded: boolean; + /** @default false */ + isVerifying: boolean; + errorMessage?: string | null; + subdomain?: string | null; + }[]; + }; }; - contactBookId: string; - createdAt: string; - updatedAt: string; - })[]; - }; + }; }; - }; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + region: string; + }; + }; + }; + responses: { + /** @description Create a new domain */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** + * @description The ID of the domain + * @example 1 + */ + id: number; + /** + * @description The name of the domain + * @example example.com + */ + name: string; + /** + * @description The ID of the team + * @example 1 + */ + teamId: number; + /** @enum {string} */ + status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE"; + /** @default us-east-1 */ + region: string; + /** @default false */ + clickTracking: boolean; + /** @default false */ + openTracking: boolean; + publicKey: string; + dkimStatus?: string | null; + spfDetails?: string | null; + createdAt: string; + updatedAt: string; + /** @default false */ + dmarcAdded: boolean; + /** @default false */ + isVerifying: boolean; + errorMessage?: string | null; + subdomain?: string | null; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - post: { - parameters: { - path: { - contactBookId: string; + "/v1/domains/{id}/verify": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - requestBody: { - content: { - "application/json": { - email: string; - firstName?: string; - lastName?: string; - properties?: { - [key: string]: string; + get?: never; + put: { + parameters: { + query?: never; + header?: never; + path: { + id: number | null; + }; + cookie?: never; }; - subscribed?: boolean; - }; - }; - }; - responses: { - /** @description Retrieve the user */ - 200: { - content: { - "application/json": { - contactId?: string; + requestBody?: never; + responses: { + /** @description Create a new domain */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message: string; + }; + }; + }; }; - }; }; - }; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - }; - "/v1/contactBooks/{contactBookId}/contacts/{contactId}": { - get: { - parameters: { - path: { - contactBookId: string; - contactId: string; + "/v1/emails/{emailId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - responses: { - /** @description Retrieve the contact */ - 200: { - content: { - "application/json": { - id: string; - firstName?: string | null; - lastName?: string | null; - email: string; - /** @default true */ - subscribed?: boolean; - properties: { - [key: string]: string; - }; - contactBookId: string; - createdAt: string; - updatedAt: string; + get: { + parameters: { + query?: never; + header?: never; + path: { + emailId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retrieve the email */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + teamId: number; + to: string | string[]; + replyTo?: string | string[]; + cc?: string | string[]; + bcc?: string | string[]; + from: string; + subject: string; + html: string | null; + text: string | null; + createdAt: string; + updatedAt: string; + emailEvents: { + emailId: string; + /** @enum {string} */ + status: "SCHEDULED" | "QUEUED" | "SENT" | "DELIVERY_DELAYED" | "BOUNCED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERED" | "OPENED" | "CLICKED" | "COMPLAINED" | "FAILED" | "CANCELLED"; + createdAt: string; + data?: unknown; + }[]; + }; + }; + }; }; - }; }; - }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: { + parameters: { + query?: never; + header?: never; + path: { + emailId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: date-time */ + scheduledAt: string; + }; + }; + }; + responses: { + /** @description Retrieve the user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + emailId?: string; + }; + }; + }; + }; + }; + trace?: never; }; - put: { - parameters: { - path: { - contactBookId: string; + "/v1/emails": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - requestBody: { - content: { - "application/json": { - email: string; - firstName?: string; - lastName?: string; - properties?: { - [key: string]: string; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - subscribed?: boolean; - }; - }; - }; - responses: { - /** @description Contact upserted successfully */ - 200: { - content: { - "application/json": { - contactId: string; + requestBody: { + content: { + "application/json": { + to: string | string[]; + /** Format: email */ + from: string; + /** @description Optional when templateId is provided */ + subject?: string; + /** @description ID of a template from the dashboard */ + templateId?: string; + variables?: { + [key: string]: string; + }; + replyTo?: string | string[]; + cc?: string | string[]; + bcc?: string | string[]; + text?: string | null; + html?: string | null; + attachments?: { + filename: string; + content: string; + }[]; + /** Format: date-time */ + scheduledAt?: string; + inReplyToId?: string | null; + }; + }; + }; + responses: { + /** @description Retrieve the user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + emailId?: string; + }; + }; + }; }; - }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - delete: { - parameters: { - path: { - contactBookId: string; - contactId: string; + "/v1/emails/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - responses: { - /** @description Contact deleted successfully */ - 200: { - content: { - "application/json": { - success: boolean; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + to: string | string[]; + /** Format: email */ + from: string; + /** @description Optional when templateId is provided */ + subject?: string; + /** @description ID of a template from the dashboard */ + templateId?: string; + variables?: { + [key: string]: string; + }; + replyTo?: string | string[]; + cc?: string | string[]; + bcc?: string | string[]; + text?: string | null; + html?: string | null; + attachments?: { + filename: string; + content: string; + }[]; + /** Format: date-time */ + scheduledAt?: string; + inReplyToId?: string | null; + }[]; + }; + }; + responses: { + /** @description List of successfully created email IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data: { + emailId: string; + }[]; + }; + }; + }; }; - }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - patch: { - parameters: { - path: { - contactBookId: string; - contactId: string; + "/v1/emails/{emailId}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - requestBody: { - content: { - "application/json": { - firstName?: string; - lastName?: string; - properties?: { - [key: string]: string; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path: { + emailId: string; + }; + cookie?: never; }; - subscribed?: boolean; - }; - }; - }; - responses: { - /** @description Retrieve the user */ - 200: { - content: { - "application/json": { - contactId?: string; + requestBody?: never; + responses: { + /** @description Retrieve the user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + emailId?: string; + }; + }; + }; }; - }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/contactBooks/{contactBookId}/contacts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + emails?: string; + page?: number; + limit?: number; + ids?: string; + }; + header?: never; + path: { + contactBookId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retrieve multiple contacts */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + firstName?: string | null; + lastName?: string | null; + email: string; + /** @default true */ + subscribed: boolean; + properties: { + [key: string]: string; + }; + contactBookId: string; + createdAt: string; + updatedAt: string; + }[]; + }; + }; + }; + }; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + email: string; + firstName?: string; + lastName?: string; + properties?: { + [key: string]: string; + }; + subscribed?: boolean; + }; + }; + }; + responses: { + /** @description Retrieve the user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + contactId?: string; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/contactBooks/{contactBookId}/contacts/{contactId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + contactId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retrieve the contact */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + firstName?: string | null; + lastName?: string | null; + email: string; + /** @default true */ + subscribed: boolean; + properties: { + [key: string]: string; + }; + contactBookId: string; + createdAt: string; + updatedAt: string; + }; + }; + }; + }; + }; + put: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + email: string; + firstName?: string; + lastName?: string; + properties?: { + [key: string]: string; + }; + subscribed?: boolean; + }; + }; + }; + responses: { + /** @description Contact upserted successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + contactId: string; + }; + }; + }; + }; + }; + post?: never; + delete: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + contactId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Contact deleted successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + success: boolean; + }; + }; + }; + }; + }; + options?: never; + head?: never; + patch: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + contactId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + firstName?: string; + lastName?: string; + properties?: { + [key: string]: string; + }; + subscribed?: boolean; + }; + }; + }; + responses: { + /** @description Retrieve the user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + contactId?: string; + }; + }; + }; + }; + }; + trace?: never; }; - }; } - export type webhooks = Record; - export interface components { - schemas: { - }; - responses: never; - parameters: { - }; - requestBodies: never; - headers: never; - pathItems: never; + schemas: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } - export type $defs = Record; - -export type external = Record; - export type operations = Record; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf7cc9e..e3d8b94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,8 +147,8 @@ importers: specifier: ^0.5.1 version: 0.5.1(hono@4.7.7) '@hono/zod-openapi': - specifier: ^0.19.5 - version: 0.19.5(hono@4.7.7)(zod@3.24.3) + specifier: ^0.10.0 + version: 0.10.1(hono@4.7.7)(zod@3.24.3) '@hookform/resolvers': specifier: ^5.0.1 version: 5.0.1(react-hook-form@7.56.1) @@ -2435,21 +2435,21 @@ packages: hono: 4.7.7 dev: false - /@hono/zod-openapi@0.19.5(hono@4.7.7)(zod@3.24.3): - resolution: {integrity: sha512-n2RqdZL7XIaWPwBNygctG/1eySyRtSBnS7l+pIsP3f2JW5P2l7Smm6SLluscrGwB5l2C2fxbfvhWoC6Ig+SxXw==} + /@hono/zod-openapi@0.10.1(hono@4.7.7)(zod@3.24.3): + resolution: {integrity: sha512-IIenwxruTH7wJ2cLPVfSg8j7bMv7F3+W69+mOOs8KsqSiBgNxwqUhkjm5ilQ1xL/Y/GHT2mB5AEBfvWb31Biiw==} engines: {node: '>=16.0.0'} peerDependencies: - hono: '>=4.3.6' + hono: '>=3.11.3' zod: 3.* dependencies: '@asteasolutions/zod-to-openapi': 7.3.0(zod@3.24.3) - '@hono/zod-validator': 0.4.3(hono@4.7.7)(zod@3.24.3) + '@hono/zod-validator': 0.2.1(hono@4.7.7)(zod@3.24.3) hono: 4.7.7 zod: 3.24.3 dev: false - /@hono/zod-validator@0.4.3(hono@4.7.7)(zod@3.24.3): - resolution: {integrity: sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==} + /@hono/zod-validator@0.2.1(hono@4.7.7)(zod@3.24.3): + resolution: {integrity: sha512-HFoxln7Q6JsE64qz2WBS28SD33UB2alp3aRKmcWnNLDzEL1BLsWfbdX6e1HIiUprHYTIXf5y7ax8eYidKUwyaA==} peerDependencies: hono: '>=3.9.0' zod: ^3.19.1