import { logger } from '@rareindex/shared'; export interface Mail { to: string | string[]; subject: string; html: string; text: string; replyTo?: string; tags?: Array<{ name: string; value: string }>; /** idempotency key to avoid double-sends on retries */ idempotencyKey?: string; } export interface SendResult { ok: boolean; id: string | null; transport: 'resend' | 'console' | 'noop'; error: string | null; } /** * Transactional e-mail transport. `EMAIL_TRANSPORT=console` logs mails instead of sending (used in * development and tests); `resend` (default when RESEND_API_KEY is set) posts to the Resend API. * The API key is read server-side only and never logged. */ export function resolveTransport(): 'resend' | 'console' | 'noop' { const forced = process.env.EMAIL_TRANSPORT; if (forced === 'console' || forced === 'noop' || forced === 'resend') return forced; return process.env.RESEND_API_KEY ? 'resend' : 'console'; } export async function sendMail(mail: Mail): Promise { const transport = resolveTransport(); const from = process.env.EMAIL_FROM ?? 'RareIndex '; const to = Array.isArray(mail.to) ? mail.to : [mail.to]; if (transport === 'console') { // eslint-disable-next-line no-console console.log(`\n──── MAIL (console transport) ────\nTo: ${to.join(', ')}\nSubject: ${mail.subject}\n\n${mail.text}\n──────────────────────────────────\n`); return { ok: true, id: `console_${Date.now()}`, transport, error: null }; } if (transport === 'noop') return { ok: true, id: null, transport, error: null }; const key = process.env.RESEND_API_KEY; if (!key) return { ok: false, id: null, transport, error: 'RESEND_API_KEY missing' }; try { const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json', ...(mail.idempotencyKey ? { 'idempotency-key': mail.idempotencyKey } : {}), }, body: JSON.stringify({ from, to, subject: mail.subject, html: mail.html, text: mail.text, reply_to: mail.replyTo, tags: mail.tags }), }); const body = (await res.json().catch(() => ({}))) as { id?: string; message?: string; name?: string }; if (!res.ok) { logger.error({ status: res.status, err: body.message ?? body.name }, 'resend send failed'); return { ok: false, id: null, transport, error: body.message ?? `HTTP ${res.status}` }; } return { ok: true, id: body.id ?? null, transport, error: null }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger.error({ err: msg }, 'resend send threw'); return { ok: false, id: null, transport, error: msg }; } }