/** * Service email — Resend si `RESEND_API_KEY` est configuré, sinon log console. * * Le code consommateur ne doit jamais bloquer ni jeter d'erreur sur un échec * d'envoi (best-effort, le booking est l'action principale). */ import "server-only"; let resendClient: import("resend").Resend | null | undefined; async function getResend(): Promise { if (resendClient !== undefined) return resendClient; const key = process.env.RESEND_API_KEY?.trim(); if (!key) { resendClient = null; return null; } try { const { Resend } = await import("resend"); resendClient = new Resend(key); return resendClient; } catch (e) { console.error("[email] resend init failed:", e instanceof Error ? e.message : e); resendClient = null; return null; } } export type EmailOpts = { to: string | string[]; subject: string; html: string; text?: string; replyTo?: string; }; const DEFAULT_FROM = process.env.RESEND_FROM ?? "Karbé "; export async function sendEmail(opts: EmailOpts): Promise<{ ok: boolean; id?: string; reason?: string }> { const client = await getResend(); if (!client) { console.log( "[email] dry-run (no RESEND_API_KEY):", JSON.stringify({ to: opts.to, subject: opts.subject }), ); return { ok: true, reason: "dry-run" }; } try { const { data, error } = await client.emails.send({ from: DEFAULT_FROM, to: Array.isArray(opts.to) ? opts.to : [opts.to], subject: opts.subject, html: opts.html, text: opts.text, replyTo: opts.replyTo, }); if (error) { console.error("[email] resend error:", error); return { ok: false, reason: error.message }; } return { ok: true, id: data?.id }; } catch (e) { const msg = e instanceof Error ? e.message : String(e); console.error("[email] send failed:", msg); return { ok: false, reason: msg }; } } // ---------- Templates ---------- const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://karbe.cosmolan.fr"; const baseStyle = ` font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; max-width: 580px; margin: 0 auto; padding: 24px; line-height: 1.5; `; function wrap(title: string, content: string): string { return `

${title}

${content}

Karbé · ${SITE_URL}
Cet email a été envoyé suite à une action sur votre compte. Si ce n'est pas vous, ignorez-le.

`; } export async function sendSignupWelcome(to: string, firstName: string): Promise { await sendEmail({ to, subject: "Bienvenue sur Karbé", html: wrap( `Bienvenue ${firstName} !`, `

Votre compte Karbé est créé. Vous pouvez désormais réserver un séjour ou, si vous êtes hôte, publier votre carbet.

Découvrir les carbets

`, ), text: `Bienvenue ${firstName} ! Votre compte Karbé est créé. ${SITE_URL}/carbets`, }); } export async function sendBookingRequestToTenant( to: string, firstName: string, bookingId: string, carbetTitle: string, startDate: Date, endDate: Date, amount: string, currency: string, ): Promise { const fmt = (d: Date) => new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d); await sendEmail({ to, subject: `Demande de réservation enregistrée — ${carbetTitle}`, html: wrap( "Demande de réservation envoyée", `

Bonjour ${firstName},

Votre demande de réservation pour ${carbetTitle} a bien été enregistrée :

  • Arrivée : ${fmt(startDate)}
  • Départ : ${fmt(endDate)}
  • Montant : ${Number(amount).toFixed(2)} ${currency}

Vous recevrez un nouvel email dès que l'hôte ou l'équipe Karbé confirmera votre séjour.

Voir ma réservation

`, ), }); } export async function sendBookingRequestToOwner( to: string, ownerFirstName: string, bookingId: string, carbetTitle: string, tenantName: string, startDate: Date, endDate: Date, ): Promise { const fmt = (d: Date) => new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d); await sendEmail({ to, subject: `Nouvelle demande de réservation — ${carbetTitle}`, html: wrap( "Nouvelle demande à confirmer", `

Bonjour ${ownerFirstName},

${tenantName} souhaite réserver ${carbetTitle} :

  • Du ${fmt(startDate)} au ${fmt(endDate)}

Connectez-vous à votre espace hôte pour confirmer ou refuser.

Mon espace hôte

`, ), }); } export async function sendBookingConfirmed( to: string, firstName: string, bookingId: string, carbetTitle: string, startDate: Date, endDate: Date, ): Promise { const fmt = (d: Date) => new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d); await sendEmail({ to, subject: `Réservation confirmée — ${carbetTitle}`, html: wrap( "Votre séjour est confirmé", `

Bonjour ${firstName},

Votre réservation pour ${carbetTitle} du ${fmt(startDate)} au ${fmt(endDate)} est confirmée.

Voir ma réservation

`, ), }); } export async function sendPasswordReset( to: string, resetUrl: string, ): Promise { await sendEmail({ to, subject: "Réinitialisation de votre mot de passe Karbé", html: wrap( "Réinitialiser votre mot de passe", `

Vous avez demandé à réinitialiser votre mot de passe Karbé. Cliquez sur le lien ci-dessous pour choisir un nouveau mot de passe (valable 1 heure) :

Réinitialiser mon mot de passe

Si vous n'avez pas fait cette demande, ignorez simplement cet email — votre mot de passe ne change pas.

`, ), text: `Réinitialiser votre mot de passe Karbé : ${resetUrl} (valable 1h).`, }); } export async function sendBookingRefunded( to: string, firstName: string, bookingId: string, carbetTitle: string, amount: string, currency: string, ): Promise { await sendEmail({ to, subject: `Remboursement traité — ${carbetTitle}`, html: wrap( "Remboursement en cours", `

Bonjour ${firstName},

Votre réservation pour ${carbetTitle} a été annulée et le remboursement de ${Number(amount).toFixed(2)} ${currency} est en cours de traitement par Stripe (3 à 5 jours ouvrés).

Détails de la réservation

`, ), }); }