SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.8 KB · 66 lines typescript
Raw Blame History
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