/**
* @module sanitizeHTML
* @file SusiMail HTML sanitizer and display fixer.
* Addresses layout and security issues in iframed HTML messages:
* removes !important declarations, normalises div margins, blocks
* remote images, and surfaces blocked-image/web-bug counts.
* @author dr|z3d
* @license AGPL3 or later
*/
/**
* Iterates every element in the document, normalising inline styles,
* hiding remote images behind a blocked placeholder, and updating
* the blocked-image and web-bug UI counters in the parent frame.
* @function sanitizeHTML
* @returns {void}
*/
function sanitizeHTML() {
"use strict";
const elements = document.querySelectorAll("*");
elements.forEach(function(element) {
const blockedImages = window.parent.document.getElementById("blockedImages");
const blockedImgCount = window.parent.document.getElementById("blockedImgCount");
const iframe = document.getElementById("iframeSusiHtmlView") || window.parent.document.getElementById("iframeSusiHtmlView");
const style = element.style.cssText;
const webBugs = iframe.contentWindow.document.querySelectorAll(".webBug");
var imgCount = 0;
if (style) {
if (style.toLowerCase().includes("height") && style.includes("100%")) {
element.style.removeProperty("height");
}
// remove all !important declarations
if (style.toLowerCase().indexOf("!important") !== -1) {
const newStyle = style.replace(/!important/gi, "");
element.setAttribute("style", newStyle);
}
}
/* setup script counter
var script = 0;
if (element.tagName.toLowerCase() === "script") {
script++;
}
*/
// set divs without explicit margins to auto
if (element.tagName.toLowerCase() === "div") {
var marginStyle = getComputedStyle(element).getPropertyValue("margin");
if (marginStyle === "0px") {element.style.margin = "auto";}
}
// change remote img src attribute to data-src, replace with inline blocked image icon
if (element.tagName.toLowerCase() === "img") {
if (element.getAttribute("data-src-blocked") !== null) {
// hide all remote images unless configured to be shown
if (iframe && !iframe.classList.contains("showBlockedImages")) {
const remoteImages = iframe.contentWindow.document.querySelectorAll("img");
// Hide all the images within the iframe
if (remoteImages) {
remoteImages.forEach(function(image) {
image.style.removeProperty("display");
image.style.display = "none";
imgCount++;
});
// display blocked image count if > 0
if (blockedImages && imgCount > 0) {
blockedImages.removeAttribute("hidden");
blockedImgCount.textContent = imgCount;
}
}
if (webBugs && webBugs.length > 0) {
const info = window.parent.document.querySelector("#blockedImages .info");
const linebreak = window.parent.document.querySelector("#webBugs br");
const bullet = " • ";
window.parent.document.getElementById("webBugs").removeAttribute("hidden");
window.parent.document.getElementById("webBugCount").innerText = webBugs.length;
info.classList.add("hasWebBugs");
if (((imgCount && imgCount < 1) || !imgCount) && linebreak) {linebreak.remove();}
else if (linebreak) {linebreak.outerHTML = bullet;}
}
}
//element.setAttribute("data-src", element.getAttribute("src"));
const domImages = document.querySelectorAll("img");
domImages.forEach(function(image) {
const blockedURL = image.getAttribute("data-src-blocked");
image.setAttribute("title", blockedURL);
image.setAttribute("src", "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cg transform='translate(-5.33 -5.33)'%3E%3Cpath fill='%2374bbf0' d='M52.14 8H11.86C9.71 8 8 10.05 8 12.54v38.91C8 54.03 9.71 56 11.86 56h40.29c2.14 0 3.86-2.06 3.86-4.54V12.54C56 9.97 54.29 8 52.14 8z'/%3E%3Cpath fill='%23fff' d='M8 29.17a13.76 13.76 0 0 1 6.17-1.46c4.46 0 8.14 1.89 9.43 4.46 1.76-.62 3.62-.91 5.49-.86 5.49 0 10.03 2.57 10.63 6.09 4.71.34 8.4 2.91 8.57 6l-11.57 7.46H8z'/%3E%3Cpath fill='%236c994e' d='M21.2 45.54S13.49 43.4 8 48.28v5.57C8 55.14 9.29 56 10.91 56l11.57-6.6z'/%3E%3Cpath d='M8 51.11c5.57-4.8 13.2-2.66 13.2-2.66l.43 1.37.86-.43-1.29-3.86S13.49 43.4 8 48.28z' opacity='.1'/%3E%3Cpath fill='%237eb35b' d='M10.91 56c3.86-7.71 15.94-16.89 26.06-11.74l-3.43 8.14-8.4 3.6z'/%3E%3Cpath d='m36.63 45.11.34-.86C26.86 39.11 14.77 48.28 10.91 56h3.69c4.29-6.43 13.46-13.03 22.03-10.89z' opacity='.1'/%3E%3Cpath fill='%23a5eb78' d='M56 53.77V39.11C36.63 37.65 25.14 56 25.14 56h27.94c1.63 0 2.91-.94 2.91-2.23z'/%3E%3Cpath d='M56 41v-1.89C36.63 37.65 25.14 56 25.14 56H29c3.43-4.37 12.94-14.83 27-15z' opacity='.1'/%3E%3Ccircle cx='44.69' cy='19.31' r='8.57' fill='%23ffe97a'/%3E%3Cpath fill='red' d='M32 12.51a19.49 19.49 0 1 0 0 38.98 19.49 19.49 0 1 0 0-38.98zm0 4.87A14.62 14.62 0 0 1 46.62 32a14.62 14.62 0 0 1-2.44 7.92l-20.1-20.1A14.62 14.62 0 0 1 32 17.38zm-12.18 6.7 20.1 20.1A14.62 14.62 0 0 1 32 46.62 14.62 14.62 0 0 1 17.38 32a14.62 14.62 0 0 1 2.44-7.92z'/%3E%3C/g%3E%3C/svg%3E");
});
}
}
});
// set html background color
document.documentElement.style.background = "#fff";
}
/**
* Toggles the visibility of all blocked images and web bugs inside the
* HTML mail iframe. Updates the toggle button state accordingly.
* @function toggleBlockedImages
* @returns {void}
*/
function toggleBlockedImages() {
const button = window.parent.document.getElementById("toggleBlockedImages");
const iframe = document.getElementById("iframeSusiHtmlView") || window.parent.document.getElementById("iframeSusiHtmlView");
const blockedImgs = Array.from(iframe.contentWindow.document.querySelectorAll("img"));
const webBugs = Array.from(iframe.contentWindow.document.querySelectorAll(".webBug"));
const images = [...blockedImgs, ...webBugs];
if (!iframe || !button) {return;}
webBugs.forEach(webBug => {
webBug.style.background = "url(/themes/susimail/images/webBug.svg) no-repeat center center/32px";
webBug.style.width = "32px";
webBug.style.height = "32px";
});
button.classList.toggle("on");
images.forEach(image => {
if (button.classList.contains("on")) {
image.style.display = "none";
image.setAttribute("hidden", "hidden");
} else {
image.style.display = "inline-block";
image.removeAttribute("hidden");
}
});
}
/**
* Creates a "Toggle Images" button and appends it to the blocked-images
* info bar if the iframe does not have the `showBlockedImages` class.
* @function createButton
* @returns {void}
*/
function createButton() {
const iframe = document.getElementById("iframeSusiHtmlView") || window.parent.document.getElementById("iframeSusiHtmlView");
const toggle = window.parent.document.getElementById("toggleBlockedImages");
if (!iframe) {return;}
if (iframe && !iframe.classList.contains("showBlockedImages")) {
const remoteImages = iframe.contentWindow.document.querySelectorAll("img") || iframe.contentWindow.document.querySelectorAll(".webBug");
if (!remoteImages) {return;}
if (toggle) {toggle.remove();}
const button = document.createElement("button");
button.id = "toggleBlockedImages";
button.innerText = "Toggle Images";
button.className = "on";
button.addEventListener("click", function(event) {
event.preventDefault();
toggleBlockedImages();
});
const info = window.parent.document.querySelector("#blockedImages .info");
info.appendChild(button);
}
}
document.addEventListener("DOMContentLoaded", () => {
sanitizeHTML();
createButton();
});