TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { logger } from '@rareindex/shared';23export interface Mail {4 to: string | string[];5 subject: string;6 html: string;7 text: string;8 replyTo?: string;9 tags?: Array<{ name: string; value: string }>;10 /** idempotency key to avoid double-sends on retries */11 idempotencyKey?: string;12}1314export interface SendResult {15 ok: boolean;16 id: string | null;17 transport: 'resend' | 'console' | 'noop';18 error: string | null;19}2021/**22 * Transactional e-mail transport. `EMAIL_TRANSPORT=console` logs mails instead of sending (used in23 * development and tests); `resend` (default when RESEND_API_KEY is set) posts to the Resend API.24 * The API key is read server-side only and never logged.25 */26export function resolveTransport(): 'resend' | 'console' | 'noop' {27 const forced = process.env.EMAIL_TRANSPORT;28 if (forced === 'console' || forced === 'noop' || forced === 'resend') return forced;29 return process.env.RESEND_API_KEY ? 'resend' : 'console';30}3132export async function sendMail(mail: Mail): Promise<SendResult> {33 const transport = resolveTransport();34 const from = process.env.EMAIL_FROM ?? 'RareIndex <no-reply@rareindex.io>';35 const to = Array.isArray(mail.to) ? mail.to : [mail.to];36 if (transport === 'console') {37 // eslint-disable-next-line no-console38 console.log(`\n──── MAIL (console transport) ────\nTo: ${to.join(', ')}\nSubject: ${mail.subject}\n\n${mail.text}\n──────────────────────────────────\n`);39 return { ok: true, id: `console_${Date.now()}`, transport, error: null };40 }41 if (transport === 'noop') return { ok: true, id: null, transport, error: null };42 const key = process.env.RESEND_API_KEY;43 if (!key) return { ok: false, id: null, transport, error: 'RESEND_API_KEY missing' };44 try {45 const res = await fetch('https://api.resend.com/emails', {46 method: 'POST',47 headers: {48 authorization: `Bearer ${key}`,49 'content-type': 'application/json',50 ...(mail.idempotencyKey ? { 'idempotency-key': mail.idempotencyKey } : {}),51 },52 body: JSON.stringify({ from, to, subject: mail.subject, html: mail.html, text: mail.text, reply_to: mail.replyTo, tags: mail.tags }),53 });54 const body = (await res.json().catch(() => ({}))) as { id?: string; message?: string; name?: string };55 if (!res.ok) {56 logger.error({ status: res.status, err: body.message ?? body.name }, 'resend send failed');57 return { ok: false, id: null, transport, error: body.message ?? `HTTP ${res.status}` };58 }59 return { ok: true, id: body.id ?? null, transport, error: null };60 } catch (err) {61 const msg = err instanceof Error ? err.message : String(err);62 logger.error({ err: msg }, 'resend send threw');63 return { ok: false, id: null, transport, error: msg };64 }65}66