diff --git a/frontend/utils/sanitizeMailHtml.ts b/frontend/utils/sanitizeMailHtml.ts new file mode 100644 index 0000000..53de52b --- /dev/null +++ b/frontend/utils/sanitizeMailHtml.ts @@ -0,0 +1,160 @@ +import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify' + +/** + * Options de sanitization du corps HTML d'un mail. + */ +export type SanitizeMailHtmlOptions = { + /** + * Si true, les images distantes (http/https) sont affichées directement. + * Par défaut false — les images distantes sont remplacées par un placeholder + * cliquable pour éviter le tracking par pixel. + */ + allowImages?: boolean +} + +/** + * Configuration DOMPurify bloquante pour les corps de mail. + * - Bloque les balises dangereuses : script, iframe, object, embed, style, link, meta, form, input + * - Bloque les attributs événements (on*) et les URI javascript: + * - Autorise les URI data: uniquement pour les images (PNG/JPEG/GIF/WEBP) — images inline CID + */ +const DOMPURIFY_CONFIG: DOMPurifyConfig = { + FORBID_TAGS: [ + 'script', + 'iframe', + 'object', + 'embed', + 'style', + 'link', + 'meta', + 'form', + 'input', + 'button', + 'textarea', + 'select', + 'base', + 'applet', + ], + FORBID_ATTR: [ + 'onerror', + 'onload', + 'onclick', + 'onmouseover', + 'onmouseout', + 'onmouseenter', + 'onmouseleave', + 'onfocus', + 'onblur', + 'onchange', + 'onsubmit', + 'onreset', + 'onkeydown', + 'onkeyup', + 'onkeypress', + 'ondblclick', + 'oncontextmenu', + 'onwheel', + 'ondrag', + 'ondrop', + 'oncopy', + 'oncut', + 'onpaste', + 'action', + 'formaction', + 'xlink:href', + ], + ALLOWED_URI_REGEXP: /^(?:https?|mailto|tel|cid|data:image\/(?:png|jpeg|gif|webp)(?:;base64,)?)(?::|$)/i, + FORCE_BODY: true, + WHOLE_DOCUMENT: false, +} + +/** + * Remplace les balises avec src http(s):// par un bouton placeholder. + * Le src original est stocké en data-mail-image-src pour permettre l'affichage + * à la demande de l'utilisateur (Phase 5 — MailMessageViewer). + */ +function replaceRemoteImages(html: string): string { + // Utiliser un DOMParser côté client uniquement (SSR-safe : le guard process.client + // est géré par l'appelant dans un composant Vue — ce helper ne tourne que client-side) + const parser = new DOMParser() + const doc = parser.parseFromString(html, 'text/html') + const images = doc.querySelectorAll('img') + + images.forEach((img) => { + const src = img.getAttribute('src') ?? '' + const isRemote = /^https?:\/\//i.test(src) + if (!isRemote) return + + // Remplacer par un span cliquable (pas de