import { createElement } from 'Common/Globals';
import { forEachObjectEntry, isArray, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings';
const
tmpl = createElement('template'),
turndown = new TurndownService(),
htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i,
htmlmap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
},
keepTagContent = 'form,button,data', // font
allowedTags = [
// Structural Elements:
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
'article','aside','header','footer','main','section',
'details','summary','nav',
// List Elements
'dd','dl','dt','li','ol','ul',
// Text Formatting Elements
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
'span','strong','sub','sup','time','u','var',
// Deprecated by HTML Standard
'acronym','big','center','dir','font','marquee',
'nobr','plaintext','rb','rtc','strike','tt',
// Media Elements
'img',//'picture','source',
// Table Elements
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
// Disallowed but converted later
'style','xmp'
].join(','),
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
],
blockquoteSwitcher = () => {
SettingsUserStore.collapseBlockquotes() &&
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
[...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
const el = createElement('details', {class:'sm-bq-switcher'});
el.innerHTML = '
]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n
')) // Not supported by element // .replace(/]*>/gi, '') // .replace(/<\?xml[^>]*\?>/gi, '') .replace(/<(\/?)head(\s[^>]*)?>/gi, '') .replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>') // .replace(/<\/?(html|head)[^>]*>/gi, '') // Fix Reddit https://github.com/the-djmaze/snappymail/issues/540 .replace(//, '') // https://github.com/the-djmaze/snappymail/issues/900 .replace(/\u2028/g,' ') // https://github.com/the-djmaze/snappymail/issues/1415 .replace(/
\s*<\/p>/gi,'') .trim(); html = ''; // Strip all comments const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT); while (nodeIterator.nextNode()) { nodeIterator.referenceNode.remove(); } /** * Basic support for Linked Data (Structured Email) * https://json-ld.org/ * https://structured.email/ **/ tmpl.content.querySelectorAll('script[type="application/ld+json"]').forEach(oElement => { // Could be array of objects or single object try { const data = JSON.parse(oElement.textContent); (isArray(data) ? data : [data]).forEach(entry => result.linkedData.push(entry)); } catch (e) { console.error(e, oElement.textContent); } }); // https://github.com/the-djmaze/snappymail/issues/1125 tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement)); tmpl.content.querySelectorAll( ':not('+allowedTags+'),a:empty,span:empty' + (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '') ).forEach(oElement => oElement.remove()); /* // Is this slower or faster? ).forEach(oElement => { if (!node || !node.contains(oElement)) { oElement.remove(); node = oElement; } }); */ // https://github.com/the-djmaze/snappymail/issues/1641 let body = tmpl.content.querySelector('.mail-body'); [...tmpl.content.querySelectorAll('.mail-body + .mail-body')] .forEach(oElement => body.append(...oElement.childNodes)); /* .forEach(oElement => { let bq = createElement('blockquote'); bq.append(...oElement.childNodes); body.replaceWith(bq); }); */ [...tmpl.content.querySelectorAll('*')].forEach(oElement => { const name = oElement.tagName, oStyle = oElement.style; if ('STYLE' === name) { let css = msgId ? parseCSS(oElement.textContent) : []; if (css.length) { css.applyNamespace(msgId); css = css.toString(); if (SettingsUserStore.removeColors()) { css = css.replace(/(background-)?color:[^};]+;?/g, ''); } oElement.textContent = css; } else { oElement.remove(); } return; } if ('XMP' === name) { const pre = createElement('pre'); pre.innerHTML = encodeHtml(oElement.innerHTML); oElement.replaceWith(pre); return; } // \MailSo\Base\HtmlUtils::ClearTags() if ('none' == oStyle.display || 'hidden' == oStyle.visibility // || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight) // || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight) // || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth) // || ('0' === oStyle.opacity ) { oElement.remove(); return; } const aAttrsForRemove = [], className = oElement.className, hasAttribute = name => oElement.hasAttribute(name), getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '', setAttribute = (name, value) => oElement.setAttribute(name, value), delAttribute = name => { let value = getAttribute(name); oElement.removeAttribute(name); return value; }; if ('mail-body' === className) { forEachObjectEntry(tasks, (name, cb) => hasAttribute(name) && cb(delAttribute(name), oElement) ); } else if (msgId && className) { oElement.className = className.replace(/(^|\s+)/g, '$1msg-'); } if (oElement.hasAttributes()) { let i = oElement.attributes.length; while (i--) { let sAttrName = oElement.attributes[i].name.toLowerCase(); if (!allowedAttributes.includes(sAttrName) && ('class' !== sAttrName || 'mail-body' !== className)) { delAttribute(sAttrName); aAttrsForRemove.push(sAttrName); } } } let value; // if ('TABLE' === name || 'TD' === name || 'TH' === name) { if (!oStyle.backgroundImage) { if ('TD' !== name && 'TH' !== name) { ['width','height'].forEach(key => { if (hasAttribute(key)) { value = delAttribute(key); oStyle[key] || (oStyle[key] = value.includes('%') ? value : value + 'px'); } }); // Make width responsive value = oStyle.width; if (100 < parseInt(value,10) && !oStyle.maxWidth) { oStyle.maxWidth = value; oStyle.width = '100%'; } // Make height responsive value = oStyle.removeProperty('height'); if (value && !oStyle.maxHeight) { oStyle.maxHeight = value; } } } // } else if ('A' === name) { value = oElement.href; if (!/^([a-z]+):/i.test(value)) { setAttribute('data-x-href-broken', value); delAttribute('href'); } else { oElement.href = stripTracking(value); if (oElement.href != value) { result.tracking = true; setAttribute('data-x-href-tracking', value); } setAttribute('target', '_blank'); // setAttribute('rel', 'external nofollow noopener noreferrer'); } setAttribute('tabindex', '-1'); aColor && !oElement.style.color && (oElement.style.color = aColor); } // if (['CENTER','FORM'].includes(name)) { if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) { ('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement); return; } // SVG xlink:href /* if (hasAttribute('xlink:href')) { delAttribute('xlink:href'); } */ let skipStyle = false; if (isMsg) { value = isMsg && delAttribute('src'); if (value) { if ('IMG' === name) { oElement.loading = 'lazy'; let attachment; if (value.startsWith('cid:')) { value = value.slice(4); setAttribute('data-x-src-cid', value); attachment = findAttachmentByCid(value); if (attachment?.download) { oElement.src = attachment.linkPreview(); oElement.title += ' ('+attachment.fileName+')'; attachment.isInline(true); attachment.isLinked(true); } } else if ((attachment = findLocationByCid(value))) { if (attachment.download) { oElement.src = attachment.linkPreview(); attachment.isLinked(true); } } else if (detectHiddenImages && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in' || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in' || (oStyle.width && 2 > pInt(oStyle.width)) || [ 'email.microsoftemail.com/open', 'github.com/notifications/beacon/', '/track/open', // mandrillapp.com list-manage.com 'google-analytics.com' ].filter(uri => value.toLowerCase().includes(uri)).length )) { skipStyle = true; oStyle.display = 'none'; // setAttribute('style', 'display:none'); setAttribute('data-x-src-hidden', value); // result.tracking = true; } else if (httpre.test(value)) { let src = stripTracking(value); if (src != value) { result.tracking = true; setAttribute('data-x-src-tracking', value); } setAttribute('data-x-src', src); result.hasExternals = true; oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20)); } else if (value.startsWith('data:image/')) { oElement.src = value; } else { setAttribute('data-x-src-broken', value); } } else { setAttribute('data-x-src-broken', value); } } } if (hasAttribute('background')) { oStyle.backgroundImage = 'url("' + delAttribute('background') + '")'; } if (hasAttribute('bgcolor')) { oStyle.backgroundColor = delAttribute('bgcolor'); } if (hasAttribute('color')) { oStyle.color = delAttribute('color'); } if (!skipStyle) { /* if ('fixed' === oStyle.position) { oStyle.position = 'absolute'; } */ oStyle.removeProperty('behavior'); oStyle.removeProperty('cursor'); oStyle.removeProperty('min-width'); const urls_remote = [], // 'data-x-style-url' urls_broken = []; // 'data-x-broken-style-src' ['backgroundImage', 'listStyleImage', 'content'].forEach(property => { if (oStyle[property]) { let value = oStyle[property], found = value.match(/url\s*\(([^)]+)\)/i); if (found) { oStyle[property] = null; found = found[1].replace(/^["'\s]+|["'\s]+$/g, ''); let lowerUrl = found.toLowerCase(); if (lowerUrl.startsWith('cid:')) { const attachment = findAttachmentByCid(found); if (attachment?.linkPreview && name) { oStyle[property] = "url('" + attachment.linkPreview() + "')"; attachment.isInline(true); attachment.isLinked(true); } } else if (httpre.test(lowerUrl)) { result.hasExternals = true; urls_remote.push([property, found]); } else if (lowerUrl.startsWith('data:image/')) { oStyle[property] = value; } else { urls_broken.push([property, found]); } } } }); // oStyle.removeProperty('background-image'); // oStyle.removeProperty('list-style-image'); if (urls_remote.length) { setAttribute('data-x-style-url', JSON.stringify(urls_remote)); } if (urls_broken.length) { setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken)); } /* // https://github.com/the-djmaze/snappymail/issues/1082 if (11 > pInt(oStyle.fontSize)) { oStyle.removeProperty('font-size'); } */ // Removes background and color // Many e-mails incorrectly only define one, not both // And in dark theme mode this kills the readability if (SettingsUserStore.removeColors()) { oStyle.removeProperty('background-color'); oStyle.removeProperty('background-image'); oStyle.removeProperty('color'); } oStyle.cssText && (oStyle.cssText = cleanCSS(oStyle.cssText)); } if (debug && aAttrsForRemove.length) { setAttribute('data-removed-attrs', aAttrsForRemove.join(', ')); } }); isMsg && blockquoteSwitcher(); // return tmpl.content.firstChild; result.html = tmpl.innerHTML.trim(); return result; }, /** * @param {string} html * @returns {string} */ htmlToPlain = html => { if (SettingsUserStore.markdown()) { return htmlToMarkdown(html); } const hr = '⎯'.repeat(64), forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn), blockquotes = node => { let bq; while ((bq = node.querySelector('blockquote'))) { // Convert child blockquote first blockquotes(bq); // Convert blockquote // bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '> '); // replaceWithChildren(bq); bq.replaceWith( '\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ') ); } }; html = html .replace(/]*>([\s\S]*?)<\/pre>/gim, (...args) => 1 < args.length ? args[1].toString().replace(/\n/g, '
') : '') // Remove line duplication .replace(/
<\/div>/gi, '') .replace(/\r?\n/g, '') .replace(/\s+/gm, ' '); while (/<(div|tr)[\s>]/i.test(html)) { html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n'); } while (/<\/(div|tr)[\s>]/i.test(html)) { html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n'); } tmpl.innerHTML = html .replace(//gi, '\t') .replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n'); forEach('style', node => node.remove()); // lines forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`)); // headings forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`)); // paragraphs forEach('p', node => { node.prepend('\n\n'); if ('' == node.textContent.trim()) { node.remove(); } else { node.after('\n\n'); } }); // proper indenting and numbering of (un)ordered lists forEach('ol,ul', node => { let prefix = '', parent = node, ordered = 'OL' == node.tagName, i = 0; while ((parent = parent?.parentNode?.closest?.('ol,ul'))) { prefix = ' ' + prefix; } node.querySelectorAll(':scope > li').forEach(li => { li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * ')); }); node.prepend('\n\n'); node.after('\n\n'); }); // Convert anchors forEach('a', a => { let txt = a.textContent, href = a.href; return a.replaceWith( txt.replace(/[\s()-]+/g, '').includes(href.replace(/^[a-z]:/, '').replace(/[\s()-]+/g, '')) ? txt : txt + ' ' + href + ' ' ); }); // Bold forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`)); // Italic forEach('i,em', i => i.replaceWith(`*${i.textContent}*`)); // Convert line-breaks tmpl.innerHTML = tmpl.innerHTML .replace(/\n{3,}/gm, '\n\n') .replace(/\n
]*>/g, '\n') .replace(/
]*>\n/g, '\n'); forEach('br', br => br.replaceWith('\n')); // Blockquotes must be last blockquotes(tmpl.content); return (tmpl.content.textContent || '').trim(); }, htmlToMarkdown = html => { tmpl.innerHTML = html; return turndown.turndown(tmpl.content); }, /** * @param {string} plain * @param {boolean} findEmailAndLinksInText = false * @returns {string} */ plainToHtml = plain => { plain = plain.toString() .replace(/\r/g, '') .replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match)) // https://github.com/the-djmaze/snappymail/issues/900 .replace(/\u2028/g,' '); let bIn = false, bDo = true, bStart = true, aNextText = [], aText = plain.split('\n'); do { bDo = false; aNextText = []; aText.forEach(sLine => { bStart = '>' === sLine.slice(0, 1); if (bStart && !bIn) { bDo = true; bIn = true; aNextText.push('~~~blockquote~~~'); aNextText.push(sLine.slice(1)); } else if (!bStart && bIn) { if (sLine) { bIn = false; aNextText.push('~~~/blockquote~~~'); aNextText.push(sLine); } else { aNextText.push(sLine); } } else if (bStart && bIn) { aNextText.push(sLine.slice(1)); } else { aNextText.push(sLine); } }); if (bIn) { bIn = false; aNextText.push('~~~/blockquote~~~'); } aText = aNextText; } while (bDo); tmpl.innerHTML = aText.join('\n') // .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n') .replace(/&/g, '&') .replace(/>/g, '>') .replace(/ { m[0] = stripTracking(m[0]); return `${m[0]}`; }) .replace(email, '$1$2') .replace(tel, '$1') .replace(/~~~blockquote~~~\s*/g, '') .replace(/\s*~~~\/blockquote~~~/g, '') .replace(/\n/g, '
'); blockquoteSwitcher(); return tmpl.innerHTML.trim(); }; rl.Utils = { cleanHtml: cleanHtml, htmlToPlain: htmlToPlain, plainToHtml: plainToHtml, htmlToMarkdown: htmlToMarkdown // markdownToHtml: md => marked.parse(md) };