import {
emailSummary,
mutationPreview,
parseAction,
resendRequest,
unwrapList,
type MutationGuardInput,
type ResendAuthOptions,
type ResendDryRun,
} from './resend-core.ts'
export type SendEmailFields = {
from: string
to: string | string[]
subject: string
html?: string
text?: string
cc?: string | string[]
bcc?: string | string[]
reply_to?: string | string[]
headers?: Record<string, string>
attachments?: Array<{
filename: string
content?: string
path?: string
content_type?: string
}>
tags?: Array<{ name: string; value: string }>
scheduled_at?: string
template?: { id: string; variables?: Record<string, unknown> }
}
export type SendEmailInput = MutationGuardInput &
SendEmailFields & {
idempotencyKey?: string
}
function requireSendFields(input: Partial<SendEmailFields>, action: string) {
if (!input.from) throw new Error(action + ': from is required (e.g. "Name <hello@yourdomain.com>")')
if (!input.to) throw new Error(action + ': to is required')
if (!input.subject) throw new Error(action + ': subject is required')
if (!input.html && !input.text && !input.template) {
throw new Error(action + ': provide html, text, or template')
}
}
function emailBody(input: SendEmailFields) {
return {
from: input.from,
to: input.to,
subject: input.subject,
html: input.html,
text: input.text,
cc: input.cc,
bcc: input.bcc,
reply_to: input.reply_to,
headers: input.headers,
attachments: input.attachments,
tags: input.tags,
scheduled_at: input.scheduled_at,
template: input.template,
}
}
/**
* POST /emails — send or schedule one email. Sends real mail unless
* `dryRun: true`. Live sends also require `confirm: true`.
*/
export async function sendEmail(input: SendEmailInput): Promise<ResendDryRun | { id: string }> {
requireSendFields(input, 'sendEmail')
const body = emailBody(input)
const preview = mutationPreview(input, {
action: 'send email',
method: 'POST',
path: '/emails',
body,
requireConfirm: true,
})
if (preview) return preview
return await resendRequest({
path: '/emails',
method: 'POST',
body,
idempotencyKey: input.idempotencyKey,
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
}
export type SendBatchInput = MutationGuardInput & {
emails: SendEmailFields[]
idempotencyKey?: string
}
/**
* POST /emails/batch — send up to 100 emails. Batch does not support
* `scheduled_at` or `attachments`. Live sends require `confirm: true`.
*/
export async function sendBatch(input: SendBatchInput): Promise<ResendDryRun | unknown> {
if (!Array.isArray(input.emails) || input.emails.length === 0) {
throw new Error('sendBatch: pass a non-empty emails array')
}
if (input.emails.length > 100) throw new Error('sendBatch: max 100 emails per batch')
for (const [index, email] of input.emails.entries()) {
requireSendFields(email, 'sendBatch emails[' + index + ']')
}
const body = input.emails.map((email) => emailBody(email))
const preview = mutationPreview(input, {
action: 'send batch of ' + body.length + ' emails',
method: 'POST',
path: '/emails/batch',
body,
requireConfirm: true,
})
if (preview) return preview
return await resendRequest({
path: '/emails/batch',
method: 'POST',
body,
idempotencyKey: input.idempotencyKey,
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
}
/** GET /emails/{email_id} — full detail including delivery `last_event`. */
export async function getEmail(input: ResendAuthOptions & { emailId: string }) {
if (!input.emailId) throw new Error('getEmail: emailId is required')
return await resendRequest({
path: '/emails/' + encodeURIComponent(input.emailId),
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
}
export type ListEmailsInput = ResendAuthOptions & {
limit?: number
after?: string
before?: string
}
/** GET /emails — one page of recent sent emails as compact summaries. */
export async function listEmails(input: ListEmailsInput = {}) {
const body = await resendRequest({
path: '/emails',
query: { limit: input.limit, after: input.after, before: input.before },
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
return unwrapList(body).map((email) => emailSummary(email)).filter(Boolean)
}
export type RescheduleEmailInput = MutationGuardInput & {
emailId: string
scheduledAt: string
}
/** PATCH /emails/{email_id} — reschedule a not-yet-sent scheduled email. */
export async function rescheduleEmail(input: RescheduleEmailInput) {
if (!input.emailId) throw new Error('rescheduleEmail: emailId is required')
if (!input.scheduledAt) throw new Error('rescheduleEmail: scheduledAt is required')
const body = { scheduled_at: input.scheduledAt }
const preview = mutationPreview(input, {
action: 'reschedule email ' + input.emailId,
method: 'PATCH',
path: '/emails/' + input.emailId,
body,
})
if (preview) return preview
return await resendRequest({
path: '/emails/' + encodeURIComponent(input.emailId),
method: 'PATCH',
body,
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
}
/** POST /emails/{email_id}/cancel — cancel a scheduled email before it sends. */
export async function cancelScheduledEmail(
input: MutationGuardInput & { emailId: string },
) {
if (!input.emailId) throw new Error('cancelScheduledEmail: emailId is required')
const preview = mutationPreview(input, {
action: 'cancel scheduled email ' + input.emailId,
method: 'POST',
path: '/emails/' + input.emailId + '/cancel',
requireConfirm: true,
})
if (preview) return preview
return await resendRequest({
path: '/emails/' + encodeURIComponent(input.emailId) + '/cancel',
method: 'POST',
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
}
/** GET /emails/receiving — inbound email (requires inbound on a domain). */
export async function listReceivedEmails(input: ListEmailsInput = {}) {
const body = await resendRequest({
path: '/emails/receiving',
query: { limit: input.limit, after: input.after, before: input.before },
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
return unwrapList(body).map((email) => emailSummary(email)).filter(Boolean)
}
/** GET /emails/receiving/{email_id} */
export async function getReceivedEmail(input: ResendAuthOptions & { emailId: string }) {
if (!input.emailId) throw new Error('getReceivedEmail: emailId is required')
return await resendRequest({
path: '/emails/receiving/' + encodeURIComponent(input.emailId),
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
}
/** GET /emails/receiving/{email_id}/attachments */
export async function listReceivedEmailAttachments(
input: ResendAuthOptions & { emailId: string },
) {
if (!input.emailId) throw new Error('listReceivedEmailAttachments: emailId is required')
const body = await resendRequest({
path: '/emails/receiving/' + encodeURIComponent(input.emailId) + '/attachments',
account: input.account,
secretName: input.secretName,
integration: input.integration,
})
return unwrapList(body)
}
const emailActions = [
'list-emails',
'get-email',
'send-email',
'send-batch',
'reschedule-email',
'cancel-scheduled-email',
'list-received-emails',
'get-received-email',
'list-received-email-attachments',
] as const
/** Emails dispatcher. Defaults to list-emails. */
export default async function emails(input: Record<string, unknown> = {}) {
const action = parseAction(input.action, emailActions, 'list-emails', 'emails')
switch (action) {
case 'list-emails':
return await listEmails(input as ListEmailsInput)
case 'get-email':
return await getEmail(input as never)
case 'send-email':
return await sendEmail(input as SendEmailInput)
case 'send-batch':
return await sendBatch(input as SendBatchInput)
case 'reschedule-email':
return await rescheduleEmail(input as RescheduleEmailInput)
case 'cancel-scheduled-email':
return await cancelScheduledEmail(input as never)
case 'list-received-emails':
return await listReceivedEmails(input as ListEmailsInput)
case 'get-received-email':
return await getReceivedEmail(input as never)
case 'list-received-email-attachments':
return await listReceivedEmailAttachments(input as never)
default: {
const exhaustive: never = action
throw new Error('Unhandled emails action: ' + String(exhaustive))
}
}
}