Skip to content
File

Blob: src/client/lib/email-html/index.ts

typescript88 lines
1import { sanitizeStyleSheet } from "./css";
2import { BLOCKED_TAGS, createBlockedImagePlaceholder, sanitizeElementAttributes } from "./dom";
3import { serializeBodyAttributes, serializeStyleTag } from "./serialize";
4import { isAllowedInlineResourceUrl, isRemoteResourceUrl } from "./url-policy";
5 
6export interface PreparedEmailHtml {
7 headHtml: string;
8 html: string;
9 bodyAttributes: string;
10 blockedRemoteContent: boolean;
11}
12 
13export function prepareEmailHtml(rawHtml: string, allowRemoteContent: boolean): PreparedEmailHtml {
14 if (typeof DOMParser === "undefined") {
15 return {
16 headHtml: "",
17 html: "",
18 bodyAttributes: "",
19 blockedRemoteContent: false,
20 };
21 }
22 
23 const parser = new DOMParser();
24 const doc = parser.parseFromString(rawHtml, "text/html");
25 let blockedRemoteContent = sanitizeElementAttributes(doc.body, allowRemoteContent);
26 const headStyles: string[] = [];
27 
28 for (const element of Array.from(doc.head.querySelectorAll("style"))) {
29 const sanitized = sanitizeStyleSheet(element.textContent ?? "", allowRemoteContent);
30 blockedRemoteContent = blockedRemoteContent || sanitized.blockedRemoteContent;
31 
32 if (sanitized.css) {
33 headStyles.push(serializeStyleTag(sanitized.css));
34 }
35 }
36 
37 for (const element of Array.from(doc.body.querySelectorAll("*"))) {
38 const tagName = element.tagName.toLowerCase();
39 
40 if (BLOCKED_TAGS.has(tagName)) {
41 element.remove();
42 continue;
43 }
44 
45 if (tagName === "style") {
46 const sanitized = sanitizeStyleSheet(element.textContent ?? "", allowRemoteContent);
47 blockedRemoteContent = blockedRemoteContent || sanitized.blockedRemoteContent;
48 
49 if (!sanitized.css) {
50 element.remove();
51 continue;
52 }
53 
54 element.textContent = sanitized.css;
55 continue;
56 }
57 
58 if (tagName === "img") {
59 const src = element.getAttribute("src");
60 if (src && !isAllowedInlineResourceUrl(src)) {
61 if (!allowRemoteContent && isRemoteResourceUrl(src)) {
62 blockedRemoteContent = true;
63 element.replaceWith(createBlockedImagePlaceholder(doc, element.getAttribute("alt")));
64 continue;
65 }
66 
67 if (!isRemoteResourceUrl(src)) {
68 element.removeAttribute("src");
69 }
70 }
71 }
72 
73 blockedRemoteContent = sanitizeElementAttributes(element, allowRemoteContent) || blockedRemoteContent;
74 
75 if (element.hasAttribute("href") || element.hasAttribute("xlink:href")) {
76 element.setAttribute("target", "_blank");
77 element.setAttribute("rel", "noopener noreferrer nofollow");
78 }
79 }
80 
81 return {
82 headHtml: headStyles.join(""),
83 html: doc.body.innerHTML,
84 bodyAttributes: serializeBodyAttributes(doc.body),
85 blockedRemoteContent,
86 };
87}