Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 64x 64x 64x 64x 64x 64x 83x 111x 64x 3x 3x 64x 4x 4x 1x 64x 44x 1x 1x 1x 1x 64x 37x 37x 37x 37x 37x 37x 37x 37x 5x 5x 32x 37x 6x 6x 26x 17x 64x 46x 26x 26x 26x 28x 28x 28x 28x 28x 28x 28x 24x 4x 20x 9x 238x 236x 236x 235x 12x 10x 9x 2x | import DOMPurify from "dompurify";
/**
* Sanitize an HTML email body: strip unsafe tags/attributes, block tracking
* pixels and event handlers, then force all links to open in a new tab.
*
* When `blockRemoteImages` is true, all `<img>` tags with remote `src`
* (http/https) are replaced with placeholders. This prevents senders from
* using tracking pixels or fingerprinting via image loads. The caller can
* re-render with `blockRemoteImages: false` when the user clicks "Show images".
*/
export function sanitizeEmailHtml(
html: string,
opts?: { blockRemoteImages?: boolean; messageId?: number },
): string {
const blockImages = opts?.blockRemoteImages ?? true;
const messageId = opts?.messageId;
const clean = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
ADD_ATTR: ["target"],
FORBID_TAGS: ["style", "script", "form", "meta", "link", "object", "embed", "iframe"],
});
// Post-process: strip event handler attributes, enforce safe links,
// and block tracking pixels (1x1 images)
const div = document.createElement("div");
div.innerHTML = clean;
// Remove all event handler attributes (on*)
for (const el of div.querySelectorAll("*")) {
for (const attr of [...el.attributes]) {
Iif (attr.name.startsWith("on")) {
el.removeAttribute(attr.name);
}
}
}
// Force all anchors to open in new tab with safe referrer policy
for (const a of div.querySelectorAll("a[href]")) {
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener noreferrer");
}
// Strip CSS url() references that could exfiltrate data (e.g. background-image: url('https://evil.com/track'))
for (const el of div.querySelectorAll("[style]")) {
const style = el.getAttribute("style") ?? "";
if (/url\s*\(/i.test(style)) {
el.setAttribute("style", style.replace(/url\s*\([^)]*\)/gi, "url()"));
}
}
// Rewrite cid: URLs to API endpoint (inline images in MIME emails)
if (messageId) {
for (const img of div.querySelectorAll('img[src^="cid:"]')) {
const src = img.getAttribute("src") ?? "";
const contentId = src.slice(4); // strip "cid:"
Eif (contentId) {
img.setAttribute(
"src",
`/api/attachments/by-cid/${messageId}/${encodeURIComponent(contentId)}`,
);
}
}
}
// Handle images: always remove tracking pixels, optionally block all remote images
for (const img of div.querySelectorAll("img")) {
// Check both HTML attributes and inline style for pixel dimensions.
// Tracking pixels often use width="1" height="1" or style="width:1px;height:1px".
const attrW = img.getAttribute("width");
const attrH = img.getAttribute("height");
const style = img.getAttribute("style") ?? "";
const styleW = style.match(/width\s*:\s*(\d+)/)?.[1];
const styleH = style.match(/height\s*:\s*(\d+)/)?.[1];
const w = attrW ?? styleW;
const h = attrH ?? styleH;
// Always strip tracking pixels (1x1 or 0x0)
if ((w === "1" || w === "0") && (h === "1" || h === "0")) {
img.remove();
continue;
}
const src = img.getAttribute("src") ?? "";
// Always strip known tracking patterns
if (
src.includes("/track") ||
src.includes("/pixel") ||
src.includes("/open") ||
src.includes("beacon")
) {
img.remove();
continue;
}
// Block remote images when enabled (http/https URLs)
if (blockImages && (src.startsWith("http://") || src.startsWith("https://"))) {
img.remove();
}
}
return div.innerHTML;
}
/** Returns true if the HTML contains remote images (http/https src) that would be blocked.
* Uses a lightweight regex pre-check to avoid a redundant DOMPurify pass. */
export function hasRemoteImages(html: string): boolean {
// Quick exit: no img tags at all
if (!/<img\s/i.test(html)) return false;
// Parse just enough to check src attributes — uses already-sanitized output
// when called after sanitizeEmailHtml, but also works on raw HTML
const div = document.createElement("div");
div.innerHTML = html;
for (const img of div.querySelectorAll("img")) {
const src = img.getAttribute("src") ?? "";
const attrW = img.getAttribute("width");
const attrH = img.getAttribute("height");
const style = img.getAttribute("style") ?? "";
const w = attrW ?? style.match(/width\s*:\s*(\d+)/)?.[1];
const h = attrH ?? style.match(/height\s*:\s*(\d+)/)?.[1];
// Skip tracking pixels
if ((w === "1" || w === "0") && (h === "1" || h === "0")) continue;
if (
src.includes("/track") ||
src.includes("/pixel") ||
src.includes("/open") ||
src.includes("beacon")
)
continue;
if (src.startsWith("http://") || src.startsWith("https://")) return true;
}
return false;
}
export function formatFullDate(dateStr: string | null | undefined): string {
if (!dateStr) return "";
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return dateStr;
return d.toLocaleString(undefined, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
export function formatFileSize(bytes: number | null): string {
if (bytes === null || bytes === 0) return "";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1048576).toFixed(1)} MB`;
}
|