From 4957ea822f4229c68bcde70b520db381e9de9423 Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sun, 1 Jun 2025 10:07:57 +1000 Subject: [PATCH] feat: add list emails api (#167) --- .../api-reference/contacts/get-contacts.mdx | 2 +- .../docs/api-reference/emails/list-emails.mdx | 3 + apps/docs/api-reference/openapi.json | 235 +++++++++++++++++- apps/docs/mint.json | 1 + apps/web/src/lib/constants/index.ts | 2 +- .../public-api/api/emails/list-emails.ts | 170 +++++++++++++ apps/web/src/server/public-api/index.ts | 2 + 7 files changed, 401 insertions(+), 14 deletions(-) create mode 100644 apps/docs/api-reference/emails/list-emails.mdx create mode 100644 apps/web/src/server/public-api/api/emails/list-emails.ts diff --git a/apps/docs/api-reference/contacts/get-contacts.mdx b/apps/docs/api-reference/contacts/get-contacts.mdx index d1a66ba..4ad096b 100644 --- a/apps/docs/api-reference/contacts/get-contacts.mdx +++ b/apps/docs/api-reference/contacts/get-contacts.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v1/contactBooks/{contactBookId}/contacts/ +openapi: get /v1/contactBooks/{contactBookId}/contacts --- diff --git a/apps/docs/api-reference/emails/list-emails.mdx b/apps/docs/api-reference/emails/list-emails.mdx new file mode 100644 index 0000000..c06140d --- /dev/null +++ b/apps/docs/api-reference/emails/list-emails.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/emails +--- diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 3fa3cfc..b7f3291 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -484,6 +484,223 @@ } }, "/v1/emails": { + "get": { + "parameters": [ + { + "schema": { + "type": "string", + "default": "1", + "example": "1" + }, + "required": false, + "name": "page", + "in": "query" + }, + { + "schema": { + "type": "string", + "default": "50", + "example": "50" + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "example": "2024-01-01T00:00:00Z" + }, + "required": false, + "name": "startDate", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "example": "2024-01-31T23:59:59Z" + }, + "required": false, + "name": "endDate", + "in": "query" + }, + { + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "example": "123" + }, + "required": false, + "name": "domainId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Retrieve a list of emails", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "to": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "replyTo": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "nullable": true + } + ] + }, + "cc": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "nullable": true + } + ] + }, + "bcc": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "nullable": true + } + ] + }, + "from": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "html": { + "type": "string", + "nullable": true + }, + "text": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "latestStatus": { + "type": "string", + "nullable": true, + "enum": [ + "SCHEDULED", + "QUEUED", + "SENT", + "DELIVERY_DELAYED", + "BOUNCED", + "REJECTED", + "RENDERING_FAILURE", + "DELIVERED", + "OPENED", + "CLICKED", + "COMPLAINED", + "FAILED", + "CANCELLED" + ] + }, + "scheduledAt": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "domainId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "id", + "to", + "from", + "subject", + "html", + "text", + "createdAt", + "updatedAt", + "latestStatus", + "scheduledAt", + "domainId" + ] + } + }, + "count": { + "type": "number" + } + }, + "required": [ + "data", + "count" + ] + } + } + } + } + } + }, "post": { "requestBody": { "required": true, @@ -495,21 +712,18 @@ "to": { "anyOf": [ { - "type": "string", - "format": "email" + "type": "string" }, { "type": "array", "items": { - "type": "string", - "format": "email" + "type": "string" } } ] }, "from": { - "type": "string", - "format": "email" + "type": "string" }, "subject": { "type": "string", @@ -652,21 +866,18 @@ "to": { "anyOf": [ { - "type": "string", - "format": "email" + "type": "string" }, { "type": "array", "items": { - "type": "string", - "format": "email" + "type": "string" } } ] }, "from": { - "type": "string", - "format": "email" + "type": "string" }, "subject": { "type": "string", diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 59e5264..7aab948 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -77,6 +77,7 @@ "group": "Emails", "pages": [ "api-reference/emails/get-email", + "api-reference/emails/list-emails", "api-reference/emails/send-email", "api-reference/emails/batch-email", "api-reference/emails/update-schedule", diff --git a/apps/web/src/lib/constants/index.ts b/apps/web/src/lib/constants/index.ts index 07db034..f2c7e36 100644 --- a/apps/web/src/lib/constants/index.ts +++ b/apps/web/src/lib/constants/index.ts @@ -1,4 +1,4 @@ -export const DEFAULT_QUERY_LIMIT = 30; +export const DEFAULT_QUERY_LIMIT = 50; /* Reputation constants */ export const HARD_BOUNCE_WARNING_RATE = 5; diff --git a/apps/web/src/server/public-api/api/emails/list-emails.ts b/apps/web/src/server/public-api/api/emails/list-emails.ts new file mode 100644 index 0000000..66b4228 --- /dev/null +++ b/apps/web/src/server/public-api/api/emails/list-emails.ts @@ -0,0 +1,170 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { db } from "~/server/db"; +import { EmailStatus, Prisma } from "@prisma/client"; +import { DEFAULT_QUERY_LIMIT } from "~/lib/constants"; + +const EmailSchema = z.object({ + id: z.string(), + to: z.string().or(z.array(z.string())), + replyTo: z.string().or(z.array(z.string())).optional().nullable(), + cc: z.string().or(z.array(z.string())).optional().nullable(), + bcc: z.string().or(z.array(z.string())).optional().nullable(), + from: z.string(), + subject: z.string(), + html: z.string().nullable(), + text: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), + latestStatus: z.nativeEnum(EmailStatus).nullable(), + scheduledAt: z.string().datetime().nullable(), + domainId: z.number().nullable(), +}); + +const route = createRoute({ + method: "get", + path: "/v1/emails", + request: { + query: z.object({ + page: z + .string() + .optional() + .default("1") + .transform(Number) + .openapi({ + param: { + name: "page", + in: "query", + }, + example: "1", + }), + limit: z + .string() + .optional() + .default(String(DEFAULT_QUERY_LIMIT)) + .pipe(z.coerce.number().min(1).max(DEFAULT_QUERY_LIMIT)) + .openapi({ + param: { + name: "limit", + in: "query", + }, + example: String(DEFAULT_QUERY_LIMIT), + }), + startDate: z + .string() + .datetime() + .optional() + .openapi({ + param: { + name: "startDate", + in: "query", + }, + example: "2024-01-01T00:00:00Z", + }), + endDate: z + .string() + .datetime() + .optional() + .openapi({ + param: { + name: "endDate", + in: "query", + }, + example: "2024-01-31T23:59:59Z", + }), + domainId: z + .union([z.string(), z.array(z.string())]) + .optional() + .transform((val) => { + if (!val) return undefined; + return (Array.isArray(val) ? val : [val]).map(Number); + }) + .openapi({ + param: { + name: "domainId", + in: "query", + }, + example: "123", // or ["123", "456"] + }), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + data: z.array(EmailSchema), + count: z.number(), + }), + }, + }, + description: "Retrieve a list of emails", + }, + }, +}); + +function listEmails(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + const { page, limit, startDate, endDate, domainId } = c.req.valid("query"); + + const whereClause: Prisma.EmailWhereInput = { + teamId: team.id, + }; + + if (startDate) { + whereClause.createdAt = { + gte: new Date(startDate), + }; + } + if (endDate) { + whereClause.createdAt = { + lte: new Date(endDate), + }; + } + + if (domainId && domainId.length > 0) { + whereClause.domainId = { in: domainId }; + } + + const [emails, count] = await db.$transaction([ + db.email.findMany({ + where: whereClause, + select: { + id: true, + to: true, + replyTo: true, + cc: true, + bcc: true, + from: true, + subject: true, + html: true, + text: true, + createdAt: true, + updatedAt: true, + latestStatus: true, + scheduledAt: true, + domainId: true, + }, + orderBy: { + createdAt: "desc", + }, + skip: (page - 1) * limit, + take: limit, + }), + db.email.count({ where: whereClause }), + ]); + + return c.json({ + data: emails.map((email) => ({ + ...email, + createdAt: email.createdAt.toISOString(), + updatedAt: email.updatedAt.toISOString(), + scheduledAt: email.scheduledAt ? email.scheduledAt.toISOString() : null, + })), + count, + }); + }); +} + +export default listEmails; diff --git a/apps/web/src/server/public-api/index.ts b/apps/web/src/server/public-api/index.ts index e63e698..02bb8b9 100644 --- a/apps/web/src/server/public-api/index.ts +++ b/apps/web/src/server/public-api/index.ts @@ -2,6 +2,7 @@ import { getApp } from "./hono"; import getDomains from "./api/domains/get-domains"; import sendEmail from "./api/emails/send-email"; import getEmail from "./api/emails/get-email"; +import listEmails from "./api/emails/list-emails"; import addContact from "./api/contacts/add-contact"; import updateContactInfo from "./api/contacts/update-contact"; import getContact from "./api/contacts/get-contact"; @@ -23,6 +24,7 @@ verifyDomain(app); /**Email related APIs */ getEmail(app); +listEmails(app); sendEmail(app); sendBatch(app); updateEmailScheduledAt(app);