Skip to content
File

Blob: tests/client/email-html/prepare-email-html.test.ts

typescript125 lines
1import { describe, expect, it } from "vitest";
2import { buildExternalLinkRedirectHref } from "@/client/lib/external-link";
3import { prepareEmailHtml } from "@/client/lib/email-html";
4 
5function parsePreparedHtml(result: ReturnType<typeof prepareEmailHtml>) {
6 return new DOMParser().parseFromString(
7 `<html><head>${result.headHtml}</head><body ${result.bodyAttributes}>${result.html}</body></html>`,
8 "text/html",
9 );
10}
11 
12describe("prepareEmailHtml", () => {
13 it("sanitizes hostile markup and blocks remote content by default", () => {
14 const result = prepareEmailHtml(
15 `
16 <html>
17 <head>
18 <style>
19 @import url("https://cdn.example.com/theme.css");
20 body { background-image: url("https://cdn.example.com/background.png"); color: red; }
21 </style>
22 </head>
23 <body onclick="alert('xss')" style="background-image:url('https://cdn.example.com/body.png'); color: blue;">
24 <script>alert("xss")</script>
25 <iframe src="https://evil.example/frame"></iframe>
26 <svg><circle /></svg>
27 <a id="remote" href="https://example.com/path?q=1">Remote link</a>
28 <a id="mailto" href="mailto:test@example.com">Mail</a>
29 <a id="tel" href="tel:+15555550123">Call</a>
30 <a id="unsafe" href="javascript:alert('xss')">Unsafe</a>
31 <img src="https://images.example.com/hero.png" alt="Hero image" />
32 <img id="inline-image" src="cid:hero-image" alt="Inline image" />
33 <div id="srcdoc-holder" srcdoc="<p>bad</p>"></div>
34 </body>
35 </html>
36 `,
37 false,
38 );
39 
40 const document = parsePreparedHtml(result);
41 const remoteLink = document.querySelector<HTMLAnchorElement>("#remote");
42 const mailtoLink = document.querySelector<HTMLAnchorElement>("#mailto");
43 const telLink = document.querySelector<HTMLAnchorElement>("#tel");
44 const unsafeLink = document.querySelector<HTMLAnchorElement>("#unsafe");
45 const blockedImage = document.querySelector<HTMLElement>('[data-remote-blocked="image"]');
46 const inlineImage = document.querySelector<HTMLImageElement>("#inline-image");
47 const srcdocHolder = document.querySelector<HTMLElement>("#srcdoc-holder");
48 
49 expect(result.blockedRemoteContent).toBe(true);
50 expect(document.querySelector("script")).toBeNull();
51 expect(document.querySelector("iframe")).toBeNull();
52 expect(document.querySelector("svg")).toBeNull();
53 expect(document.body.hasAttribute("onclick")).toBe(false);
54 expect(document.body.getAttribute("style")).toContain("background-image:none");
55 expect(document.body.getAttribute("style")).toContain("color: blue");
56 expect(result.headHtml).toContain("background-image: none");
57 expect(result.headHtml).not.toContain("https://cdn.example.com/theme.css");
58 expect(remoteLink?.getAttribute("href")).toBe(buildExternalLinkRedirectHref("https://example.com/path?q=1"));
59 expect(remoteLink?.getAttribute("target")).toBe("_blank");
60 expect(remoteLink?.getAttribute("rel")).toBe("noopener noreferrer nofollow");
61 expect(mailtoLink?.getAttribute("href")).toBe("mailto:test@example.com");
62 expect(telLink?.getAttribute("href")).toBe("tel:+15555550123");
63 expect(unsafeLink?.hasAttribute("href")).toBe(false);
64 expect(blockedImage?.textContent).toContain("Remote image blocked: Hero image");
65 expect(inlineImage?.getAttribute("src")).toBe("cid:hero-image");
66 expect(srcdocHolder?.hasAttribute("srcdoc")).toBe(false);
67 });
68 
69 it("allows remote content when explicitly enabled while preserving other safety rules", () => {
70 const result = prepareEmailHtml(
71 `
72 <html>
73 <head>
74 <style>
75 body { background-image: url("https://cdn.example.com/background.png"); }
76 </style>
77 </head>
78 <body>
79 <a id="remote" href="https://example.com/path?q=1">Remote link</a>
80 <a id="unsafe" href="file:///tmp/secrets.txt">Unsafe</a>
81 <img id="remote-image" src="https://images.example.com/hero.png" alt="Hero image" />
82 </body>
83 </html>
84 `,
85 true,
86 );
87 
88 const document = parsePreparedHtml(result);
89 const remoteLink = document.querySelector<HTMLAnchorElement>("#remote");
90 const unsafeLink = document.querySelector<HTMLAnchorElement>("#unsafe");
91 const remoteImage = document.querySelector<HTMLImageElement>("#remote-image");
92 
93 expect(result.blockedRemoteContent).toBe(false);
94 expect(result.headHtml).toContain("https://cdn.example.com/background.png");
95 expect(remoteImage?.getAttribute("src")).toBe("https://images.example.com/hero.png");
96 expect(remoteLink?.getAttribute("href")).toBe(buildExternalLinkRedirectHref("https://example.com/path?q=1"));
97 expect(unsafeLink?.hasAttribute("href")).toBe(false);
98 expect(document.querySelector('[data-remote-blocked="image"]')).toBeNull();
99 });
100 
101 it("preserves inline resources and does not report blocked remote content when none exists", () => {
102 const result = prepareEmailHtml(
103 `
104 <html>
105 <body dir="rtl" lang="ar">
106 <img id="inline-image" src="data:image/png;base64,abc123" alt="Inline" />
107 <a id="mailto" href="mailto:test@example.com">Mail</a>
108 <a id="tel" href="tel:+15555550123">Call</a>
109 </body>
110 </html>
111 `,
112 false,
113 );
114 
115 const document = parsePreparedHtml(result);
116 
117 expect(result.blockedRemoteContent).toBe(false);
118 expect(document.body.getAttribute("dir")).toBe("rtl");
119 expect(document.body.getAttribute("lang")).toBe("ar");
120 expect(document.querySelector("#inline-image")?.getAttribute("src")).toBe("data:image/png;base64,abc123");
121 expect(document.querySelector("#mailto")?.getAttribute("href")).toBe("mailto:test@example.com");
122 expect(document.querySelector("#tel")?.getAttribute("href")).toBe("tel:+15555550123");
123 });
124});