/** * @mandujs/core/email — Resend adapter * * Thin HTTP client for the Resend Send-Email API. Zero deps: uses `fetch`. * * Contract (https://resend.com/docs/api-reference/emails/send-email): * POST https://api.resend.com/emails * Headers: * Authorization: Bearer * Content-Type: application/json * Body: { * from, to (string | string[]), subject, * html?, text?, cc?, bcc?, reply_to?, headers? * } * Response 200: { id: string, ... } * Error 4xx/5xx: { name, message, statusCode } * * @module email/resend */ import { type EmailMessage, type EmailSender, type EmailSendResult, _coerceRecipients, _lowercaseHeaders, _validateMessage, } from "./index.js"; /** Config for the Resend adapter. */ export interface ResendOptions { /** Resend API key. Required. Pull from env (e.g. `RESEND_API_KEY`). */ apiKey: string; /** * Override API base URL. Useful for tests or private Resend deployments. * Default: `"https://api.resend.com"`. Trailing slash is stripped. */ baseUrl?: string; /** * Fetch implementation. Default: `globalThis.fetch`. Injected in tests so * we never hit the real network. */ fetch?: typeof globalThis.fetch; } /** Body shape posted to Resend. Fields are omitted when undefined. */ interface ResendRequestBody { from: string; to: string[]; subject: string; html?: string; text?: string; cc?: string[]; bcc?: string[]; reply_to?: string; headers?: Record; } /** Successful Resend response shape. We only care about `id`. */ interface ResendSuccessResponse { id: string; } /** How much of a failed-response body we include in the thrown error. */ const ERROR_BODY_EXCERPT_CHARS = 200; /** * Builds the request body in exactly the shape Resend expects. Exported for * tests so they can assert the payload shape without running `send()`. * * @internal */ export function _buildResendBody(message: EmailMessage): ResendRequestBody { const recipients = _coerceRecipients(message); const body: ResendRequestBody = { from: message.from, to: recipients.to, subject: message.subject, }; if (message.html !== undefined) body.html = message.html; if (message.text !== undefined) body.text = message.text; if (recipients.cc) body.cc = recipients.cc; if (recipients.bcc) body.bcc = recipients.bcc; if (message.replyTo !== undefined) body.reply_to = message.replyTo; const headers = _lowercaseHeaders(message.headers); if (headers && Object.keys(headers).length > 0) body.headers = headers; return body; } /** * Creates a Resend-backed `EmailSender`. * * @throws `TypeError` if `apiKey` is missing. */ export function createResendSender(options: ResendOptions): EmailSender { if (!options || typeof options.apiKey !== "string" || options.apiKey.length === 0) { throw new TypeError( "[@mandujs/core/email/resend] createResendSender: 'apiKey' is required.", ); } const baseUrl = (options.baseUrl ?? "https://api.resend.com").replace(/\/+$/, ""); const fetchImpl = options.fetch ?? globalThis.fetch; if (typeof fetchImpl !== "function") { throw new Error( "[@mandujs/core/email/resend] globalThis.fetch is unavailable — provide `options.fetch` or run in an environment that supplies fetch.", ); } const url = `${baseUrl}/emails`; const authHeader = `Bearer ${options.apiKey}`; async function send(message: EmailMessage): Promise { _validateMessage(message); const body = _buildResendBody(message); let response: Response; try { response = await fetchImpl(url, { method: "POST", headers: { Authorization: authHeader, "Content-Type": "application/json", }, body: JSON.stringify(body), }); } catch (err) { // Network-level failure (DNS, TCP reset, abort, offline). Re-throw // with clear context so ops dashboards can distinguish "provider said // no" from "we never reached the provider". const causeMsg = err instanceof Error ? err.message : String(err); throw new Error( `[@mandujs/core/email/resend] email send failed: ${causeMsg}`, { cause: err }, ); } if (!response.ok) { // Read the body best-effort so the error is actionable, but don't let // a malformed body mask the HTTP status. let excerpt = ""; try { const text = await response.text(); excerpt = text.slice(0, ERROR_BODY_EXCERPT_CHARS); } catch { excerpt = ""; } throw new Error( `[@mandujs/core/email/resend] email send failed: status=${response.status} body=${excerpt}`, ); } const parsed = (await response.json()) as ResendSuccessResponse; if (!parsed || typeof parsed.id !== "string") { throw new Error( "[@mandujs/core/email/resend] email send failed: provider response missing 'id'.", ); } return { id: parsed.id, sentAt: Date.now() }; } return { send }; }