Skip to content

Commit 7cd08aa

Browse files
author
ledouxm
committed
feat: add DSFR mail wrapper and integrate into email content generation
1 parent 1122c14 commit 7cd08aa

File tree

6 files changed

+288
-14
lines changed

6 files changed

+288
-14
lines changed
5 KB
Loading
4.74 KB
Loading

packages/backend/src/features/bordereau.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Selectable } from "kysely";
22
import { Database } from "../db/db";
33
import { deserializePreconisations } from "@cr-vif/pdf/constat";
4+
import { wrapWithDsfrMail } from "./mail/dsfrMailWrapper";
45

56
export const createBordereauMailContent = ({
67
stateReport,
@@ -10,7 +11,10 @@ export const createBordereauMailContent = ({
1011
user: Selectable<Database["user"]>;
1112
}) => {
1213
const preconisations = deserializePreconisations(stateReport.preconisations || "");
13-
return `<p><b>Madame, Monsieur,</b></p>
14+
const title = stateReport.titre_edifice
15+
? `Constat d'état : ${stateReport.titre_edifice}`
16+
: "Constat d'état";
17+
const inner = `<p><b>Madame, Monsieur,</b></p>
1418
1519
<p>Veuillez trouver ci-joint le rapport établi à la suite de la visite de votre monument historique réalisée, en date du ${
1620
stateReport.date_visite
@@ -66,4 +70,5 @@ Références réglementaires et ressources : <br />
6670
- <a href="https://www.legifrance.gouv.fr/download/pdf/circ?id=30077">Circulaire n° 2009-024 du 1er décembre 2009 relative au contrôle scientifique et technique des services de l'État sur la conservation des monuments historiques classés et inscrits</a> <br />
6771
- <a href="https://www.culture.gouv.fr/thematiques/monuments-sites/ressources/les-essentiels/glossaire-des-termes-relatifs-aux-interventions-sur-les-monuments-historiques">Glossaire des termes relatifs aux interventions sur les monuments historiques</a> <br />
6872
</p>`;
73+
return wrapWithDsfrMail({ title, content: inner });
6974
};

packages/backend/src/features/mail.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Selectable } from "kysely";
88
import { getStateReportMailName, MinimalAlert } from "@cr-vif/pdf/constat";
99
import { createBordereauMailContent } from "./bordereau";
1010
import { getAlertMailSubject, createAlertEmailContent } from "./mail/alertMail";
11+
import { wrapWithDsfrMail } from "./mail/dsfrMailWrapper";
1112
import { AuthUser } from "../routes/authMiddleware";
1213

1314
const transporter = createTransport({
@@ -63,11 +64,10 @@ export const sendReportMail = ({
6364
from: ENV.EMAIL_EMITTER,
6465
to: recipients,
6566
subject: "Compte-rendu UDAP" + (report?.title ? ` : ${report.title}` : ""),
66-
text: `Bonjour,
67-
68-
Vous trouverez ci-joint le compte-rendu de notre rendez-vous.
69-
70-
Cordialement`,
67+
html: wrapWithDsfrMail({
68+
title: "Compte-rendu UDAP" + (report?.title ? ` : ${report.title}` : ""),
69+
content: `<p>Bonjour,</p><p>Vous trouverez ci-joint le compte-rendu de notre rendez-vous.</p><p>Cordialement</p>`,
70+
}),
7171
attachments: [
7272
{
7373
filename: getPDFInMailName(report),
@@ -78,11 +78,15 @@ Cordialement`,
7878
};
7979

8080
export const sendPasswordResetMail = ({ email, temporaryLink }: { email: string; temporaryLink: string }) => {
81+
const resetLink = `${ENV.FRONTEND_URL}/reset-password/${temporaryLink}`;
8182
return transporter.sendMail({
8283
from: ENV.EMAIL_EMITTER,
8384
to: email,
8485
subject: "Patrinotes - Réinitialisation de mot de passe",
85-
text: `Voici le lien de réinitialisation de votre mot de passe : ${ENV.FRONTEND_URL}/reset-password/${temporaryLink}`,
86+
html: wrapWithDsfrMail({
87+
title: "Réinitialisation de mot de passe",
88+
content: `<p>Voici le lien de réinitialisation de votre mot de passe :</p><p><a href="${resetLink}">${resetLink}</a></p>`,
89+
}),
8690
});
8791
};
8892

@@ -104,12 +108,15 @@ export const sendValidationRequestMail = ({
104108
from: ENV.EMAIL_EMITTER,
105109
to: validatorEmail,
106110
subject: `[Validation requise] Constat d'état${title}`,
107-
html: `<p>Bonjour,</p>
111+
html: wrapWithDsfrMail({
112+
title: `Validation requise — Constat d'état${title}`,
113+
content: `<p>Bonjour,</p>
108114
<p>${creatorName} vous soumet un constat d'état${title} pour validation.</p>
109115
<p>Veuillez consulter le document et l'accepter ou le refuser en cliquant sur le lien ci-dessous :</p>
110116
<p><a href="${link}">${link}</a></p>
111117
<p>Ce lien est valable 7 jours.</p>
112118
<p>Cordialement</p>`,
119+
}),
113120
});
114121
};
115122

@@ -133,10 +140,13 @@ export const sendValidationResultMail = ({
133140
from: ENV.EMAIL_EMITTER,
134141
to: creatorEmail,
135142
subject: `Constat d'état${title}${accepted ? "Accepté" : "Refusé"} par le validateur`,
136-
html: `<p>Bonjour,</p>
143+
html: wrapWithDsfrMail({
144+
title: `Constat d'état${title}${accepted ? "Accepté" : "Refusé"}`,
145+
content: `<p>Bonjour,</p>
137146
<p>Votre constat d'état${title} a été <strong>${decision}</strong> par ${validatorEmail}.</p>
138147
${comment ? `<p>Commentaire : ${comment}</p>` : ""}
139148
<p>Cordialement</p>`,
149+
}),
140150
});
141151
};
142152

packages/backend/src/features/mail/alertMail.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Selectable } from "kysely";
22
import { Database } from "../../db/db";
33
import { MinimalAlert } from "@cr-vif/pdf/constat";
4+
import { wrapWithDsfrMail } from "./dsfrMailWrapper";
45
import {
56
ABORDS_DE_L_EDIFICE_SECTION,
67
ARCHEOLOGIE_SECTION,
@@ -59,7 +60,7 @@ export const createAlertEmailContent = async ({
5960
}),
6061
);
6162

62-
const html = `
63+
const innerHtml = `
6364
<p>Madame, Monsieur,</p>
6465
<p>Dans le cadre d’un constat d’état réalisé sur le monument historique <b>${uppercaseFirstLetter(stateReport.titre_edifice!)}</b>${stateReport.commune ? `, situé à ${stateReport.commune}` : ``},
6566
${getProblemDescription({ alert, user })} l’agent ${getServicePronom(user.service.name!)}, en charge du contrôle scientifique et technique, ${user.name} :</p>
@@ -78,12 +79,13 @@ export const createAlertEmailContent = async ({
7879
<b>${user.email}</b>
7980
</p>
8081
<p>Merci,</p>
81-
82-
<p>Ministère de la culture</p>
83-
84-
<p>(Envoi automatique depuis le service numérique Patrinotes)</p>
8582
`;
8683

84+
const html = wrapWithDsfrMail({
85+
title: `Alerte ${alert.alert}${stateReport.titre_edifice ? ` — ${stateReport.titre_edifice}` : ""}`,
86+
content: innerHtml,
87+
});
88+
8789
return { html, attachments: mailAttachments };
8890
};
8991

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import fs from "fs";
2+
import path from "path";
3+
4+
const assetsDir = path.join(__dirname, "../../assets");
5+
6+
const marianneLightBase64 = fs.readFileSync(path.join(assetsDir, "Marianne-Light@2x.png")).toString("base64");
7+
const marianneDarkBase64 = fs.readFileSync(path.join(assetsDir, "Marianne-Dark@2x.png")).toString("base64");
8+
9+
const marianneLightSrc = `data:image/png;base64,${marianneLightBase64}`;
10+
const marianneDarkSrc = `data:image/png;base64,${marianneDarkBase64}`;
11+
12+
const DSFR_CSS = `
13+
.hide-white {
14+
display: none !important;
15+
}
16+
.hide-black {
17+
display: block !important;
18+
}
19+
/********* DARK MODE ************/
20+
:root {
21+
color-scheme: light dark;
22+
supported-color-schemes: light dark;
23+
}
24+
@media (prefers-color-scheme: dark) {
25+
body {
26+
background: #161616 !important;
27+
font-color: #ffffff !important;
28+
}
29+
svg { fill: white; filter: invert(1); }
30+
.white { fill: #ffffff !important; filter: invert(1); }
31+
.hide-black { display: none !important; }
32+
.hide-white { display: block !important; }
33+
.darkmode {
34+
background-color: #161616 !important;
35+
font-color: #ffffff !important;
36+
color: #ffffff !important;
37+
background: none !important;
38+
border-color: #2A2A2A !important;
39+
}
40+
.darkmode-1 {
41+
background-color: #161616 !important;
42+
font-color: #CECECE !important;
43+
color: #CECECE !important;
44+
background: none !important;
45+
}
46+
.darkmode-2 {
47+
background-color: #242424 !important;
48+
font-color: #ffffff !important;
49+
color: #ffffff !important;
50+
border-color: #2A2A2A !important;
51+
}
52+
.darkmode-3 {
53+
background-color: #1E1E1E !important;
54+
font-color: #ffffff !important;
55+
color: #ffffff !important;
56+
border-color: #2A2A2A !important;
57+
}
58+
.darkmode-4 {
59+
background-color: #1B1B35 !important;
60+
font-color: #CECECE !important;
61+
color: #CECECE !important;
62+
border-color: #2A2A2A !important;
63+
}
64+
.darkmode-5 {
65+
background-color: #1A1A3D !important;
66+
font-color: #ffffff !important;
67+
color: #ffffff !important;
68+
border-color: #2A2A2A !important;
69+
}
70+
.darkmode-6 {
71+
background-color: #1F1F4A !important;
72+
font-color: #ffffff !important;
73+
color: #ffffff !important;
74+
border-color: #2A2A2A !important;
75+
}
76+
a[href] { color: #8585F6 !important; }
77+
a.darkmode-button-color-primary[href] { font-color: #000091 !important; color: #000091 !important; }
78+
.darkmode-button-primary {
79+
background-color: #8585F6 !important;
80+
font-color: #000091 !important;
81+
color: #000091 !important;
82+
border: solid 1px #8585F6 !important;
83+
}
84+
[data-ogsc] .darkmode { background-color: #161616 !important; }
85+
[data-ogsc] h1,[data-ogsc] h2,[data-ogsc] p,[data-ogsc] span,[data-ogsc] a,[data-ogsc] b { color: #ffffff !important; }
86+
[data-ogsc] .link { color: #7C7CFF !important; }
87+
}
88+
body {
89+
width: 100%;
90+
background-color: #ffffff;
91+
margin: 0;
92+
padding: 0;
93+
-webkit-text-size-adjust: none;
94+
-webkit-font-smoothing: antialiased;
95+
-ms-text-size-adjust: none;
96+
}
97+
a[href] { color: #000091; }
98+
table { border-collapse: collapse; }
99+
table, td { border-collapse: collapse; padding: 0px; margin: 0px; }
100+
table.wlkm-mw { min-width: 0px !important; }
101+
@media only screen and (max-width: 600px) {
102+
.wlkm-mw { width: 480px !important; padding-left: 0 !important; padding-right: 0 !important; }
103+
.wlkm-cl { width: 460px !important; }
104+
.wlkm-hAuto { height: auto !important; }
105+
.wlkm-resp { width: auto !important; max-width: 460px !important; }
106+
.wlkm-resp2 { width: auto !important; max-width: 480px !important; }
107+
.wlkm-hide { display: none !important; }
108+
.wlkm-alignCenter { display: block !important; text-align: center !important; }
109+
.wlkm-alignCenter img { margin: 0 auto; }
110+
.img-max { width: 460px !important; }
111+
}
112+
@media only screen and (max-width: 480px) {
113+
.wlkm-cl { width: 90% !important; margin: 0 auto; }
114+
.wlkm-resp { max-width: 280px !important; }
115+
.img-max { width: 90% !important; margin: 0 auto; }
116+
.wlkm-resp2 { max-width: 300px !important; }
117+
.wlkm-mw { width: 100% !important; margin: 0 auto; }
118+
}
119+
`;
120+
121+
export function wrapWithDsfrMail({
122+
title,
123+
preheader,
124+
content,
125+
serviceName = "Ministère de la Culture",
126+
}: {
127+
title: string;
128+
preheader?: string;
129+
content: string;
130+
serviceName?: string;
131+
}): string {
132+
return `<!doctype html>
133+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
134+
<head>
135+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
136+
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;" />
137+
<title>${escapeHtml(title)}</title>
138+
<style type="text/css">${DSFR_CSS}</style>
139+
</head>
140+
<body style="font-family: Tahoma, Geneva, sans-serif;">
141+
142+
${
143+
preheader
144+
? `<!-- Preheader -->
145+
<table style="min-width:100%!important; width:100%;" width="100%" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ffffff" class="wlkm-mw darkmode">
146+
<tr><td align="center" class="darkmode">
147+
<table style="min-width:600px; margin:0 auto; width:600px;" class="wlkm-mw darkmode" width="600" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ffffff" align="center">
148+
<tr><td class="darkmode">
149+
<table style="margin:0 auto; width:496px;" class="wlkm-cl darkmode" width="496" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ffffff" align="center">
150+
<tr><td class="darkmode-1" width="496" align="left" valign="top" style="padding:4px 0px 8px 0px; line-height:18px; font-size:12px; color:#6b6b6b; font-family:'Marianne',Arial,Helvetica,sans-serif;">
151+
<span style="font-family:'Marianne',Arial,Helvetica,sans-serif !important;">${escapeHtml(preheader)}</span>
152+
</td></tr>
153+
</table>
154+
</td></tr>
155+
</table>
156+
</td></tr>
157+
</table>`
158+
: ""
159+
}
160+
161+
<!-- Institutional header with Marianne logo -->
162+
<table width="100%" border="0" align="center" cellpadding="0" cellspacing="0" bgcolor="#ffffff" class="darkmode" style="min-width:100%!important;width:100%;" role="presentation">
163+
<tr><td align="center">
164+
<table style="min-width:620px; margin:0 auto; width:620px;" width="620" cellspacing="0" cellpadding="0" role="presentation" border="0" align="center" class="wlkm-mw darkmode">
165+
<tr><td align="center">
166+
<table style="min-width:600px; margin:0 auto; width:600px; border-left:1px #e5e5e5 solid; border-right:1px #e5e5e5 solid; border-top:1px #e5e5e5 solid;" class="wlkm-mw darkmode" width="600" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ffffff" align="center">
167+
<tr><td>
168+
<table style="margin:0 auto; width:496px;" class="wlkm-cl darkmode" width="496" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ffffff" align="center">
169+
<tr><td style="width:100%;" width="100%" valign="top" align="center">
170+
<!-- Marianne logo left -->
171+
<table style="border-collapse:collapse;" width="76" cellspacing="0" cellpadding="0" role="presentation" border="0" align="left" bgcolor="#ffffff" class="darkmode">
172+
<tr><td align="center">
173+
<table style="border-collapse:collapse;" width="100%" cellspacing="0" cellpadding="0" role="presentation" border="0" align="left" bgcolor="#ffffff" class="darkmode">
174+
<tr><td height="12" style="height:12px; line-height:12px; font-size:12px;">&nbsp;</td></tr>
175+
<tr><td class="hide-black" align="left">
176+
<img src="${marianneLightSrc}" alt="République française" style="display:block; height:auto; width:76px;" width="76" border="0" class="hide-black">
177+
</td></tr>
178+
<tr><td class="hide-white" align="left">
179+
<img src="${marianneDarkSrc}" alt="République française" style="display:block; height:auto; width:76px;" width="76" border="0" class="hide-white">
180+
</td></tr>
181+
<tr><td height="12" style="height:12px; line-height:12px; font-size:12px;">&nbsp;</td></tr>
182+
</table>
183+
</td></tr>
184+
</table>
185+
<!-- Service name right -->
186+
<table style="border-collapse:collapse;" width="200" cellspacing="0" cellpadding="0" role="presentation" border="0" align="right" bgcolor="#ffffff" class="darkmode">
187+
<tr><td align="right">
188+
<table style="border-collapse:collapse;" width="100%" cellspacing="0" cellpadding="0" role="presentation" border="0" align="right" bgcolor="#ffffff" class="darkmode">
189+
<tr><td height="12" style="height:12px; line-height:12px; font-size:12px;">&nbsp;</td></tr>
190+
<tr><td class="darkmode" style="line-height:20px; font-size:12px; color:#161616; font-family:'Marianne',Arial,Helvetica,sans-serif;" align="right">
191+
<span style="font-family:'Marianne',Arial,Helvetica,sans-serif !important;"><strong>${escapeHtml(serviceName)}</strong></span>
192+
</td></tr>
193+
<tr><td height="12" style="height:12px; line-height:12px; font-size:12px;">&nbsp;</td></tr>
194+
</table>
195+
</td></tr>
196+
</table>
197+
</td></tr>
198+
</table>
199+
</td></tr>
200+
</table>
201+
</td></tr>
202+
</table>
203+
</td></tr>
204+
</table>
205+
206+
<!-- Title band -->
207+
<table style="min-width:100%!important; width:100%;" width="100%" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ffffff" class="wlkm-mw darkmode">
208+
<tr><td align="center">
209+
<table style="min-width:620px; margin:0 auto; width:620px; background:linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(234,234,234,1) 5%, rgba(234,234,234,1) 95%, rgba(255,255,255,1) 100%);" width="620" cellspacing="0" cellpadding="0" role="presentation" border="0" align="center" class="wlkm-mw darkmode">
210+
<tr><td align="center">
211+
<table style="min-width:600px; margin:0 auto; width:600px; border-left:1px #e5e5e5 solid; border-right:1px #e5e5e5 solid;" class="wlkm-mw darkmode-4" width="600" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ECECFE" align="center">
212+
<tr><td>
213+
<table style="margin:0 auto; width:496px;" class="wlkm-cl darkmode" width="496" cellspacing="0" cellpadding="0" role="presentation" border="0" bgcolor="#ECECFE" align="center">
214+
<tr><td class="darkmode-4" width="496" align="left" valign="top" style="padding:20px 10px 20px 10px; line-height:32px; font-size:24px; color:#161616; font-family:'Marianne',Arial,Helvetica,sans-serif;">
215+
<span style="font-family:'Marianne',Arial,Helvetica,sans-serif !important;"><strong>${escapeHtml(title)}</strong></span>
216+
</td></tr>
217+
</table>
218+
</td></tr>
219+
</table>
220+
</td></tr>
221+
</table>
222+
</td></tr>
223+
</table>
224+
225+
<!-- Content section -->
226+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" bgcolor="#ffffff" width="100%" style="min-width:100%!important; width:100%;" class="darkmode">
227+
<tr><td>
228+
<table style="min-width:620px; margin:0 auto; width:620px; background:linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(234,234,234,1) 5%, rgba(234,234,234,1) 95%, rgba(255,255,255,1) 100%);" width="620" cellspacing="0" cellpadding="0" role="presentation" border="0" align="center" class="wlkm-mw darkmode">
229+
<tr><td align="center">
230+
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" bgcolor="#ffffff" width="600" style="min-width:600px; margin:0 auto; width:600px; border-left:1px #e5e5e5 solid; border-right:1px #e5e5e5 solid;" class="wlkm-mw darkmode">
231+
<tr><td align="left" valign="top" style="padding:24px 10px 24px 10px; line-height:24px; font-size:14px; color:#161616; font-family:Tahoma,Geneva,sans-serif;" class="wlkm-cl darkmode">
232+
<span style="font-family:Tahoma,Geneva,sans-serif !important;">${content}</span>
233+
</td></tr>
234+
</table>
235+
</td></tr>
236+
</table>
237+
</td></tr>
238+
</table>
239+
240+
<!-- Footer -->
241+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" bgcolor="#ffffff" width="100%" style="min-width:100%!important; width:100%;" class="darkmode">
242+
<tr><td align="center">
243+
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" bgcolor="#ffffff" width="600" style="min-width:600px; margin:0 auto; width:600px;" class="wlkm-mw darkmode">
244+
<tr><td align="left" valign="top" style="padding:10px 10px 20px 10px; line-height:18px; font-size:12px; color:#585858; font-family:Tahoma,Geneva,sans-serif;" class="wlkm-cl darkmode-1">
245+
<span style="font-family:Tahoma,Geneva,sans-serif !important;">Envoi automatique depuis le service numérique Patrinotes</span>
246+
</td></tr>
247+
</table>
248+
</td></tr>
249+
</table>
250+
251+
</body>
252+
</html>`;
253+
}
254+
255+
function escapeHtml(str: string): string {
256+
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
257+
}

0 commit comments

Comments
 (0)