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)}
}) |
${esc(brand.associationName)} |
|
${content}
${footer(brand)}
|
`;
}
function cta(url: string, label: string) {
const safe = safeHttpUrl(url);
if (!safe || !label) return "";
return ``;
}
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 }) };
}