import { RESEND_ATTACHMENT_SCHEMA, RESEND_ATTACHMENT_WIRE_SCHEMA, RESEND_EMAIL_SCHEMA, RESEND_EMAIL_MUTATION_RESPONSE_SCHEMA, RESEND_EMAIL_SUMMARY_SCHEMA, RESEND_EMAIL_SUMMARY_WIRE_SCHEMA, RESEND_EMAIL_WIRE_SCHEMA, RESEND_ID_RESPONSE_SCHEMA, RESEND_RECEIVED_EMAIL_SCHEMA, RESEND_RECEIVED_EMAIL_SUMMARY_SCHEMA, RESEND_RECEIVED_EMAIL_SUMMARY_WIRE_SCHEMA, RESEND_RECEIVED_EMAIL_WIRE_SCHEMA, resendListSchema, resendListWireSchema, } from "@automate.ax/integration-contracts/resend" import type { JsonObject } from "type-fest" import * as z from "zod" import { defineAction } from "../../automation/actions" import { hasEmailBody, normalizeEmailBody, OPTIONAL_EMAIL_BODY_SCHEMA, } from "../../lib/email" import { getResendApi, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS, RESEND_SEND_ACCOUNT_OPTIONS, } from "./lib" const RESEND_ACCOUNT = "resend" const EMAIL_ADDRESS_SCHEMA = z.email() const EMAIL_ADDRESSES_SCHEMA = z .union([EMAIL_ADDRESS_SCHEMA, EMAIL_ADDRESS_SCHEMA.array().min(1).max(50)]) .transform((value) => (Array.isArray(value) ? value : [value])) const OPTIONAL_EMAIL_ADDRESSES_SCHEMA = z .union([EMAIL_ADDRESS_SCHEMA, EMAIL_ADDRESS_SCHEMA.array().min(1)]) .transform((value) => (Array.isArray(value) ? value : [value])) .optional() const TAG_SCHEMA = z.object({ /** Tag name used to categorize the message. */ name: z .string() .regex(/^[A-Za-z0-9_-]+$/) .max(256), /** Tag value used to categorize the message. */ value: z .string() .regex(/^[A-Za-z0-9_-]+$/) .max(256), }) const TEMPLATE_SCHEMA = z.object({ /** Published template ID or alias. */ id: z.string().min(1), /** Values substituted into the published template. */ variables: z .record( z .string() .regex(/^[A-Za-z0-9_]+$/) .max(50), z.union([z.string().max(2_000), z.number().max(Number.MAX_SAFE_INTEGER)]), ) .optional(), }) const ATTACHMENT_SCHEMA = z.union([ z.object({ /** Base64-encoded attachment content. */ content: z.string().min(1), /** MIME content ID used to embed the attachment inline. */ contentId: z.string().min(1).optional(), /** Explicit MIME type. */ contentType: z.string().min(1).optional(), /** Filename shown to recipients. */ filename: z.string().min(1), }), z.object({ /** MIME content ID used to embed the attachment inline. */ contentId: z.string().min(1).optional(), /** Explicit MIME type. */ contentType: z.string().min(1).optional(), /** Optional filename override. */ filename: z.string().min(1).optional(), /** Public URL Resend should fetch. */ path: z.url(), }), ]) const SEND_EMAIL_FIELDS_SCHEMA = z.object({ ...OPTIONAL_EMAIL_BODY_SCHEMA.shape, /** Base64 content or public-URL attachments, up to 40 MB after encoding. */ attachments: ATTACHMENT_SCHEMA.array().optional(), /** Blind-copy recipients. */ bcc: OPTIONAL_EMAIL_ADDRESSES_SCHEMA, /** Carbon-copy recipients. */ cc: OPTIONAL_EMAIL_ADDRESSES_SCHEMA, /** Verified sender, optionally including a friendly name. */ from: z.string().trim().min(1).optional(), /** Custom email headers. */ headers: z.record(z.string().min(1), z.string()).optional(), /** Idempotency key retained by Resend for 24 hours. */ idempotencyKey: z.string().min(1).max(256).optional(), /** Addresses that should receive replies. */ replyTo: OPTIONAL_EMAIL_ADDRESSES_SCHEMA, /** Natural-language or ISO 8601 future delivery time. */ scheduledAt: z.string().min(1).optional(), /** Message subject, unless inherited from a template. */ subject: z.string().min(1).optional(), /** Resend metadata used for reporting and webhook correlation. */ tags: TAG_SCHEMA.array().optional(), /** Published Resend template and variables. */ template: TEMPLATE_SCHEMA.optional(), /** Topic used to honor a contact's subscription preference. */ topicId: z.string().min(1).optional(), /** One or more primary recipients. */ to: EMAIL_ADDRESSES_SCHEMA, }) const SEND_EMAIL_SCHEMA = SEND_EMAIL_FIELDS_SCHEMA.superRefine(validateEmail) const BATCH_EMAIL_SCHEMA = SEND_EMAIL_FIELDS_SCHEMA.omit({ attachments: true, idempotencyKey: true, }).superRefine(validateEmail) const PAGE_INPUT_SCHEMA = z .object({ /** Return records after this cursor. */ after: z.string().min(1).optional(), /** Return records before this cursor. */ before: z.string().min(1).optional(), /** Maximum records to return. */ limit: z.number().int().min(1).max(100).prefault(20), }) .refine(({ after, before }) => !(after && before), { message: "after and before cannot be used together.", }) const EMAIL_LIST_SCHEMA = resendListSchema(RESEND_EMAIL_SUMMARY_SCHEMA) const EMAIL_LIST_WIRE_SCHEMA = resendListWireSchema( RESEND_EMAIL_SUMMARY_WIRE_SCHEMA, ) const ATTACHMENT_LIST_SCHEMA = resendListSchema(RESEND_ATTACHMENT_SCHEMA) const ATTACHMENT_LIST_WIRE_SCHEMA = resendListWireSchema( RESEND_ATTACHMENT_WIRE_SCHEMA, ) const RECEIVED_EMAIL_LIST_SCHEMA = resendListSchema( RESEND_RECEIVED_EMAIL_SUMMARY_SCHEMA, ) const RECEIVED_EMAIL_LIST_WIRE_SCHEMA = resendListWireSchema( RESEND_RECEIVED_EMAIL_SUMMARY_WIRE_SCHEMA, ) const SHARE_EMAIL_RESPONSE_SCHEMA = z.object({ id: z.string(), object: z.literal("email"), url: z.url(), }) const EMAIL_METRIC_SCHEMA = z.enum([ "bounce_rate", "bounced", "bounced_permanent", "bounced_transient", "bounced_undetermined", "click_rate", "clicked", "complained", "complaint_rate", "delivered", "delivery_delayed", "delivery_rate", "failed", "open_rate", "opened", "received", "sent", "suppressed", "unique_clicked", "unique_opened", "unsubscribe_rate", "unsubscribed", ]) const EMAIL_DIMENSION_SCHEMA = z.enum([ "broadcast", "domain", "email", "period", ]) const EMAIL_METRICS_INPUT_SCHEMA = z .object({ /** Broadcast IDs used to restrict metrics. */ broadcastIds: z.string().min(1).array().max(100).optional(), /** Dimensions used to split metric rows. */ dimensions: EMAIL_DIMENSION_SCHEMA.array().optional(), /** Domain IDs used to restrict metrics. */ domainIds: z.string().min(1).array().max(100).optional(), /** Email IDs used to restrict metrics. */ emailIds: z.string().min(1).array().max(100).optional(), /** Inclusive range end; defaults to now. */ endDate: z.string().min(1).optional(), /** Period bucket size. */ granularity: z .enum(["daily", "hourly", "monthly", "weekly"]) .prefault("daily"), /** Metrics to include; omission requests every provider metric. */ metrics: EMAIL_METRIC_SCHEMA.array().optional(), /** Inclusive range start; defaults to six days before the end. */ startDate: z.string().min(1).optional(), /** IANA timezone used to bucket periods. */ timezone: z.string().min(1).prefault("UTC"), }) .superRefine((input, ctx) => { if ( input.emailIds && (input.broadcastIds || input.dimensions?.includes("broadcast")) ) { ctx.addIssue({ code: "custom", message: "emailIds cannot be combined with broadcastIds or the broadcast dimension.", }) } if ( input.broadcastIds && (input.emailIds || input.dimensions?.includes("email")) ) { ctx.addIssue({ code: "custom", message: "broadcastIds cannot be combined with emailIds or the email dimension.", }) } if ( input.dimensions?.includes("email") && input.dimensions.includes("broadcast") ) { ctx.addIssue({ code: "custom", message: "email and broadcast dimensions cannot be combined.", }) } }) const EMAIL_METRICS_WIRE_SCHEMA = z.object({ data: z .looseObject({ broadcast_id: z.string().optional(), broadcast_name: z.string().optional(), domain_id: z.string().optional(), domain_name: z.string().optional(), email_id: z.string().optional(), period: z.string().optional(), }) .catchall(z.number()) .array() .optional(), dimensions: EMAIL_DIMENSION_SCHEMA.array(), end_date: z.string(), granularity: z.enum(["daily", "hourly", "monthly", "weekly"]), metrics: EMAIL_METRIC_SCHEMA.array(), object: z.literal("metrics"), start_date: z.string(), totals: z.record(z.string(), z.number()), }) const EMAIL_METRICS_SCHEMA = EMAIL_METRICS_WIRE_SCHEMA.transform( ({ data, end_date: endDate, start_date: startDate, ...metrics }) => ({ ...metrics, data: data?.map( ({ broadcast_id: broadcastId, broadcast_name: broadcastName, domain_id: domainId, domain_name: domainName, email_id: emailId, ...row }) => ({ ...row, broadcastId, broadcastName, domainId, domainName, emailId, }), ), endDate, startDate, }), ) /** Sends or schedules a transactional Resend email. */ export const sendResendEmail = defineAction("Send Resend email") .describe("Sends or schedules a rich transactional email through Resend.") .account(RESEND_ACCOUNT, RESEND_SEND_ACCOUNT_OPTIONS) .input(SEND_EMAIL_SCHEMA) .output(RESEND_ID_RESPONSE_SCHEMA) .retry({ replaySafety: ({ idempotencyKey }) => (idempotencyKey ? "safe" : "unsafe"), }) .handler(async ({ account, input }) => { const { idempotencyKey, ...email } = input return await getResendApi(account).call("/emails", { body: toResendEmail(email), headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : undefined, httpMethod: "POST", responseSchema: RESEND_ID_RESPONSE_SCHEMA, }) }) /** Sends up to 100 Resend emails in one provider request. */ export const sendResendEmailBatch = defineAction("Send Resend email batch") .describe("Sends up to 100 transactional emails in one Resend API request.") .account(RESEND_ACCOUNT, RESEND_SEND_ACCOUNT_OPTIONS) .input( z.object({ /** Idempotency key retained by Resend for 24 hours. */ idempotencyKey: z.string().min(1).max(256).optional(), /** Ordered emails; response IDs use the same order. */ emails: BATCH_EMAIL_SCHEMA.array().min(1).max(100), }), ) .output(z.object({ data: RESEND_ID_RESPONSE_SCHEMA.array() })) .retry({ replaySafety: ({ idempotencyKey }) => (idempotencyKey ? "safe" : "unsafe"), }) .handler( async ({ account, input }) => await getResendApi(account).call("/emails/batch", { body: input.emails.map(toResendEmail), headers: input.idempotencyKey ? { "Idempotency-Key": input.idempotencyKey } : undefined, httpMethod: "POST", responseSchema: z.object({ data: RESEND_ID_RESPONSE_SCHEMA.array() }), }), ) /** Retrieves a sent or scheduled Resend email by ID. */ export const getResendEmail = defineAction("Get Resend email") .describe("Retrieves content and latest delivery state for a Resend email.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(z.object({ emailId: z.string().min(1) })) .output(RESEND_EMAIL_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call(`/emails/${encode(input.emailId)}`, { responseSchema: RESEND_EMAIL_WIRE_SCHEMA, }), ) /** Lists sent and scheduled Resend emails with cursor pagination. */ export const listResendEmails = defineAction("List Resend emails") .describe("Lists sent and scheduled Resend emails with cursor pagination.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(PAGE_INPUT_SCHEMA) .output(EMAIL_LIST_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call("/emails", { query: input, responseSchema: EMAIL_LIST_WIRE_SCHEMA, }), ) /** Changes the delivery time of a scheduled Resend email. */ export const updateScheduledResendEmail = defineAction( "Update scheduled Resend email", ) .describe("Changes the future delivery time of a scheduled Resend email.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input( z.object({ emailId: z.string().min(1), scheduledAt: z.iso.datetime({ offset: true }), }), ) .output(RESEND_EMAIL_MUTATION_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await getResendApi(account).call(`/emails/${encode(input.emailId)}`, { body: { scheduled_at: input.scheduledAt }, httpMethod: "PATCH", responseSchema: RESEND_EMAIL_MUTATION_RESPONSE_SCHEMA, }), ) /** Cancels a scheduled Resend email before delivery begins. */ export const cancelScheduledResendEmail = defineAction( "Cancel scheduled Resend email", ) .describe("Cancels a scheduled email before Resend begins delivery.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(z.object({ emailId: z.string().min(1) })) .output(RESEND_EMAIL_MUTATION_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `/emails/${encode(input.emailId)}/cancel`, { httpMethod: "POST", responseSchema: RESEND_EMAIL_MUTATION_RESPONSE_SCHEMA, }, ), ) /** Creates a temporary share link for a sent or received email. */ export const shareResendEmail = defineAction("Share Resend email") .describe("Creates a Resend-hosted email preview link for up to 48 hours.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input( z.object({ emailId: z.string().min(1), expiresIn: z.string().min(1).optional(), }), ) .output(SHARE_EMAIL_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `/emails/${encode(input.emailId)}/share`, { body: input.expiresIn ? { expires_in: input.expiresIn } : {}, httpMethod: "POST", responseSchema: SHARE_EMAIL_RESPONSE_SCHEMA, }, ), ) /** Lists attachments for a sent Resend email. */ export const listResendEmailAttachments = defineAction( "List Resend email attachments", ) .describe("Lists downloadable attachments for a sent Resend email.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(PAGE_INPUT_SCHEMA.safeExtend({ emailId: z.string().min(1) })) .output(ATTACHMENT_LIST_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { emailId, ...query } = input return await getResendApi(account).call( `/emails/${encode(emailId)}/attachments`, { query, responseSchema: ATTACHMENT_LIST_WIRE_SCHEMA }, ) }) /** Retrieves one sent-email attachment and its temporary download URL. */ export const getResendEmailAttachment = defineAction( "Get Resend email attachment", ) .describe("Retrieves one attachment from a sent Resend email.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input( z.object({ attachmentId: z.string().min(1), emailId: z.string().min(1), }), ) .output(RESEND_ATTACHMENT_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `/emails/${encode(input.emailId)}/attachments/${encode(input.attachmentId)}`, { responseSchema: RESEND_ATTACHMENT_WIRE_SCHEMA }, ), ) /** Lists inbound emails received by Resend. */ export const listReceivedResendEmails = defineAction( "List received Resend emails", ) .describe("Lists inbound emails received by Resend with cursor pagination.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(PAGE_INPUT_SCHEMA) .output(RECEIVED_EMAIL_LIST_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call("/emails/receiving", { query: input, responseSchema: RECEIVED_EMAIL_LIST_WIRE_SCHEMA, }), ) /** Retrieves one inbound email including its content and headers. */ export const getReceivedResendEmail = defineAction("Get received Resend email") .describe("Retrieves content, headers, and metadata for an inbound email.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(z.object({ emailId: z.string().min(1) })) .output(RESEND_RECEIVED_EMAIL_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `/emails/receiving/${encode(input.emailId)}`, { responseSchema: RESEND_RECEIVED_EMAIL_WIRE_SCHEMA }, ), ) /** Lists attachments for an inbound Resend email. */ export const listReceivedResendEmailAttachments = defineAction( "List received Resend email attachments", ) .describe("Lists downloadable attachments for an inbound Resend email.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(PAGE_INPUT_SCHEMA.safeExtend({ emailId: z.string().min(1) })) .output(ATTACHMENT_LIST_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { emailId, ...query } = input return await getResendApi(account).call( `/emails/receiving/${encode(emailId)}/attachments`, { query, responseSchema: ATTACHMENT_LIST_WIRE_SCHEMA }, ) }) /** Retrieves one inbound-email attachment and its temporary download URL. */ export const getReceivedResendEmailAttachment = defineAction( "Get received Resend email attachment", ) .describe("Retrieves one attachment from an inbound Resend email.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input( z.object({ attachmentId: z.string().min(1), emailId: z.string().min(1), }), ) .output(RESEND_ATTACHMENT_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `/emails/receiving/${encode(input.emailId)}/attachments/${encode(input.attachmentId)}`, { responseSchema: RESEND_ATTACHMENT_WIRE_SCHEMA }, ), ) /** Retrieves account-level Resend email delivery and engagement metrics. */ export const getResendEmailMetrics = defineAction("Get Resend email metrics") .describe("Retrieves totals and optional dimension rows for email metrics.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(EMAIL_METRICS_INPUT_SCHEMA) .output(EMAIL_METRICS_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call("/emails/metrics", { query: { broadcast_id: input.broadcastIds, dimensions: input.dimensions, domain_id: input.domainIds, email_id: input.emailIds, end_date: input.endDate, granularity: input.granularity, metrics: input.metrics, start_date: input.startDate, timezone: input.timezone, }, responseSchema: EMAIL_METRICS_WIRE_SCHEMA, }), ) /** * Applies Resend's custom-content or template exclusivity rules. * * @param input - Validated email input. * @param ctx - Zod refinement context receiving validation issues. */ function validateEmail( input: z.output, ctx: z.RefinementCtx, ) { if (input.template && hasEmailBody(input)) { ctx.addIssue({ code: "custom", message: "Template emails cannot also provide a custom body.", }) return } if (input.template) return if (!input.from) ctx.addIssue({ code: "custom", message: "from is required." }) if (!input.subject) { ctx.addIssue({ code: "custom", message: "subject is required." }) } if (!hasEmailBody(input)) { ctx.addIssue({ code: "custom", message: "html, markdown, or text is required.", }) } } /** * Converts code-first email fields to Resend's REST wire format. * * @param email - Validated single or batch email input. */ function toResendEmail( email: | z.output | z.output, ): JsonObject { const attachments = "attachments" in email ? email.attachments : undefined return { ...(attachments && { attachments: attachments.map( ({ contentId, contentType, ...attachment }) => ({ ...attachment, ...(contentId && { content_id: contentId }), ...(contentType && { content_type: contentType }), }), ), }), ...(email.bcc && { bcc: email.bcc }), ...(email.cc && { cc: email.cc }), ...(email.from && { from: email.from }), ...(email.headers && { headers: email.headers }), ...(hasEmailBody(email) ? normalizeEmailBody(email) : {}), ...(email.replyTo && { reply_to: email.replyTo }), ...(email.scheduledAt && { scheduled_at: email.scheduledAt }), ...(email.subject && { subject: email.subject }), ...(email.tags && { tags: email.tags }), ...(email.template && { template: email.template }), ...(email.topicId && { topic_id: email.topicId }), to: email.to, } } /** * URL-encodes one Resend path identifier. * * @param value - Provider resource identifier. */ function encode(value: string) { return encodeURIComponent(value) }