import { richTextToEmailHtml, safeHttpUrl } from "@/lib/rich-text"; export type EmailBrand = { associationName: string; address?: string; email?: string; phone?: string; siret?: string; }; export type GeneralEmailContent = { subject: string; kicker: string; title: string; body: string; buttonLabel: string; buttonUrl: string; signature: string; }; export type MeetingEmailTexts = { greeting: string; intro: string; outro: string; signature: string; }; export type MeetingEmailData = { id: number; title: string; startsAt: string; location: string | null; description: string | null; capacity: number; registered: number; }; function esc(value: unknown) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function lines(value: unknown) { return esc(value).replace(/\r?\n/g, "
"); } function paragraphs(value: string) { return value .split(/\r?\n\s*\r?\n/) .filter(Boolean) .map( (paragraph, index, all) => `${lines(paragraph)}`, ) .join(""); } function footer(brand: EmailBrand) { const contact = [brand.email, brand.phone].filter(Boolean).map(esc).join(" · "); return ` ${esc(brand.associationName)}${brand.address ? ` · ${lines(brand.address)}` : ""}
${contact}${brand.siret ? `${contact ? "
" : ""}SIRET : ${esc(brand.siret)}` : ""} `; } function emailShell({ title, content, baseUrl, brand }: { title: string; content: string; baseUrl: string; brand: EmailBrand }) { const logoUrl = `${baseUrl.replace(/\/$/, "")}/assets/logo.png`; return ` ${esc(title)}
${content} ${footer(brand)}
${esc(brand.associationName)} ${esc(brand.associationName)}
`; } function cta(url: string, label: string) { const safe = safeHttpUrl(url); if (!safe || !label) return ""; return `
${esc(label)}
`; } export function buildGeneralEmail(content: GeneralEmailContent, baseUrl: string, brand: EmailBrand) { const button = cta(content.buttonUrl, content.buttonLabel); const body = ` ${content.kicker ? `
${esc(content.kicker)}
` : ""}
${esc(content.title)}
${paragraphs(content.body)}
${button ? `${button}` : ""} ${lines(content.signature)}`; return { subject: content.subject, html: emailShell({ title: content.subject, content: body, baseUrl, brand }) }; } export function defaultMeetingEmailTexts(brand: EmailBrand): MeetingEmailTexts { return { greeting: "Chère adhérente, cher adhérent,", intro: "Plein R a le plaisir de vous convier à sa prochaine rencontre. Un moment pour créer des liens, partager les actualités locales et faire grandir notre réseau.", outro: "Les places sont limitées : pensez à confirmer votre présence dès maintenant.", signature: `À très bientôt,\nL'équipe de ${brand.associationName}`, }; } export function buildMeetingEmail(meeting: MeetingEmailData, texts: MeetingEmailTexts, baseUrl: string, brand: EmailBrand) { const date = new Date(meeting.startsAt); const dateLong = new Intl.DateTimeFormat("fr-FR", { weekday: "long", day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit", }).format(date); const remaining = Math.max(0, meeting.capacity - meeting.registered); const registrationUrl = `${baseUrl.replace(/\/$/, "")}/inscription/${meeting.id}`; const subject = `Invitation Plein R — ${meeting.title} · ${dateLong}`; const button = cta(registrationUrl, "Je m'inscris à la rencontre"); const body = `
Invitation · Rencontre Plein R
${texts.greeting ? `
${lines(texts.greeting)}
` : ""} ${texts.intro ? `
${lines(texts.intro)}
` : ""}
${esc(meeting.title)}
${meeting.description ? `
${lines(meeting.description)}
` : ""}
${esc(dateLong.charAt(0).toUpperCase() + dateLong.slice(1))}
${meeting.location ? `${esc(meeting.location)}
` : ""} ${remaining > 0 ? `${remaining} place${remaining > 1 ? "s" : ""} restante${remaining > 1 ? "s" : ""}` : "Rencontre complète"}
${button} ${texts.outro ? `
${lines(texts.outro)}
` : ""} ${texts.signature ? `
${lines(texts.signature)}
` : ""} Si bouton bloqué, copiez ce lien :
${esc(registrationUrl)}`; return { subject, html: emailShell({ title: subject, content: body, baseUrl, brand }), registrationUrl }; } export type CredentialsEmailData = { name: string; email: string; tempPassword: string; /** « Votre compte adhérent », « Votre accès à l'administration »… */ intro: string; }; /** * Identifiants d'un compte qui vient d'être créé ou réinitialisé. * * Construit dans la portée de l'action qui émet le mot de passe et expédié * aussitôt : ce message ne passe **jamais** par la file d'attente, dont le * corps est stocké en base. */ export function buildCredentialsEmail(data: CredentialsEmailData, baseUrl: string, brand: EmailBrand) { const loginUrl = `${baseUrl.replace(/\/$/, "")}/login`; const subject = `Vos identifiants ${brand.associationName}`; const body = `
${esc(brand.associationName)}
Bonjour ${esc(data.name)},
${lines(data.intro)}
Identifiant
${esc(data.email)}
Mot de passe temporaire
${esc(data.tempPassword)}
${cta(loginUrl, "Me connecter")} Ce mot de passe est provisoire : il vous sera demandé d'en choisir un nouveau dès votre première connexion. Si vous n'êtes pas à l'origine de cette demande, prévenez l'association. Si le bouton ne fonctionne pas, copiez ce lien :
${esc(loginUrl)}`; return { subject, html: emailShell({ title: subject, content: body, baseUrl, brand }) }; } export type InformationEmailData = { id: number; title: string; /** Texte balisé, analysé par `src/lib/rich-text.ts`. */ body: string; hasImage: boolean; }; /** * Une information de l'association, mise en e-mail. * * Un constructeur à part plutôt qu'un élargissement de `GeneralEmailContent` : * le contrat du studio de composition reste intact. * * L'image de couverture part par **URL** (`/api/informations//image`) et * non en data-URI : Gmail et Outlook suppriment les ``, et * l'incorporer gonflerait chaque ligne de la file d'attente à plusieurs méga-octets. */ export function buildInformationEmail(data: InformationEmailData, baseUrl: string, brand: EmailBrand) { const base = baseUrl.replace(/\/$/, ""); const spaceUrl = `${base}/backend/espace/informations`; const subject = `${brand.associationName} — ${data.title}`; const body = `
Information · ${esc(brand.associationName)}
${esc(data.title)}
${ data.hasImage ? `` : "" } ${richTextToEmailHtml(data.body)} ${cta(spaceUrl, "Lire dans mon espace")} Vous recevez ce message en tant qu'adhérent de ${esc(brand.associationName)}.
Si le bouton ne fonctionne pas, copiez ce lien : ${esc(spaceUrl)} `; return { subject, html: emailShell({ title: subject, content: body, baseUrl, brand }) }; }