JavaScript 56.7%
TypeScript 42.6%
1/**2 * Envoi de courriels via Resend (API REST, aucune dépendance).3 * Variables : RESEND_API_KEY (obligatoire), MAIL_FROM (ex. « SPB Cloud <no-reply@spboucher.ai> »).4 * ⚠ Sans domaine vérifié dans Resend, seul « onboarding@resend.dev » est accepté comme expéditeur,5 * et il ne livre qu'à l'adresse propriétaire du compte Resend.6 */78export interface MailInput {9 to: string;10 subject: string;11 html: string;12 text: string;13}1415export class MailerError extends Error {16 constructor(message: string, public status?: number, public code?: string) { super(message); }17}1819export function mailerConfigured(): boolean {20 return !!process.env.RESEND_API_KEY;21}2223export function mailFrom(): string {24 return process.env.MAIL_FROM || "SPB Cloud <onboarding@resend.dev>";25}2627export async function sendMail(input: MailInput): Promise<{ id: string }> {28 const key = process.env.RESEND_API_KEY;29 if (!key) throw new MailerError("RESEND_API_KEY manquante", 500, "not_configured");30 const res = await fetch("https://api.resend.com/emails", {31 method: "POST",32 headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },33 body: JSON.stringify({ from: mailFrom(), to: [input.to], subject: input.subject, html: input.html, text: input.text }),34 });35 const body = (await res.json().catch(() => ({}))) as { id?: string; message?: string; name?: string; statusCode?: number };36 if (!res.ok) throw new MailerError(body.message || `Resend HTTP ${res.status}`, res.status, body.name);37 return { id: body.id || "" };38}3940/** Gabarit du code de connexion (clair, lisible sur mobile, marque SPB Cloud). */41export function loginCodeEmail(code: string, opts: { name?: string | null; minutes: number; ip?: string; device?: string }): { subject: string; html: string; text: string } {42 const spaced = code.split("").join(" ");43 const greeting = opts.name ? `Bonjour ${opts.name},` : "Bonjour,";44 const subject = `${code} — votre code de connexion SPB Cloud`;45 const text = `${greeting}\n\nVoici votre code de connexion à SPB Cloud : ${code}\n\nIl expire dans ${opts.minutes} minutes et ne peut être utilisé qu'une fois.\nSi vous n'êtes pas à l'origine de cette demande, ignorez ce courriel.\n\n${opts.device ? `Appareil : ${opts.device}\n` : ""}${opts.ip ? `Adresse IP : ${opts.ip}\n` : ""}\n— SPB Cloud · cloud.spboucher.ai`;46 const html = `<!doctype html><html lang="fr"><body style="margin:0;background:#f5f6f8;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#0f172a">47 <div style="max-width:520px;margin:0 auto;padding:32px 20px">48 <div style="display:flex;align-items:center;gap:10px;margin-bottom:20px">49 <div style="width:36px;height:36px;border-radius:11px;background:linear-gradient(135deg,#6366f1,#4338ca);display:inline-block;vertical-align:middle"></div>50 <span style="font-size:17px;font-weight:600;vertical-align:middle;margin-left:10px">SPB <span style="color:#4f46e5">Cloud</span></span>51 </div>52 <div style="background:#fff;border:1px solid #e2e6eb;border-radius:20px;padding:28px 24px">53 <p style="margin:0 0 10px;font-size:15px">${greeting}</p>54 <p style="margin:0 0 18px;font-size:15px;color:#3a404b">Voici votre code de connexion. Il expire dans <strong>${opts.minutes} minutes</strong> et ne sert qu'une fois.</p>55 <div style="text-align:center;margin:22px 0">56 <span style="display:inline-block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:34px;letter-spacing:8px;font-weight:700;padding:14px 22px;border-radius:14px;background:#eef0f3;color:#0f172a">${spaced}</span>57 </div>58 <p style="margin:18px 0 0;font-size:13px;color:#6b7280">Si vous n'êtes pas à l'origine de cette demande, ignorez simplement ce courriel — personne ne peut se connecter sans ce code.</p>59 ${opts.device || opts.ip ? `<p style="margin:12px 0 0;font-size:12px;color:#9aa1ad">${opts.device ? `Appareil : ${opts.device}` : ""}${opts.device && opts.ip ? " · " : ""}${opts.ip ? `IP : ${opts.ip}` : ""}</p>` : ""}60 </div>61 <p style="margin:18px 0 0;text-align:center;font-size:12px;color:#9aa1ad">SPB Cloud · <a href="https://cloud.spboucher.ai" style="color:#6b7280">cloud.spboucher.ai</a> · cloud personnel privé et chiffré</p>62 </div></body></html>`;63 return { subject, html, text };64}65