import { createElement } from 'Common/Globals'; import { forEachObjectEntry, pInt } from 'Common/Utils'; import { SettingsUserStore } from 'Stores/User/Settings'; const tpl = createElement('template'), htmlre = /[&<>"']/g, httpre = /^(https?:)?\/\//i, htmlmap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }, blockquoteSwitcher = () => { SettingsUserStore.collapseBlockquotes() && // tpl.content.querySelectorAll('blockquote').forEach(node => { [...tpl.content.querySelectorAll('blockquote')].reverse().forEach(node => { const el = Element.fromHTML('
•••
'); node.replaceWith(el); el.append(node); }); }, replaceWithChildren = node => node.replaceWith(...[...node.childNodes]), url = /(^|\s|\n|\/?>)(https?:\/\/[-A-Z0-9+&#/%?=()~_|!:,.;]*[-A-Z0-9+&#/%=~()_|])/gi, // eslint-disable-next-line max-len email = /(^|\s|\n|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi, // rfc3966 tel = /(^|\s|\n|\/?>)(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g, // Strip tracking /** TODO: implement other url strippers like from * https://www.bleepingcomputer.com/news/security/new-firefox-privacy-feature-strips-urls-of-tracking-parameters/ * https://github.com/newhouse/url-tracking-stripper * https://github.com/svenjacobs/leon * https://maxchadwick.xyz/tracking-query-params-registry/ * https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java */ // eslint-disable-next-line max-len stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_|email_hash)/i, urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url, base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')), decode = decodeURIComponent, stripTracking = url => { try { let nurl = url // Copernica .replace(/^.+\/(https%253A[^/?&]+).*$/i, (...m) => decode(decode(m[1]))) .replace(/tracking\.(printabout\.nl[^?]+)\?.*/i, (...m) => m[1]) .replace(/(zalando\.nl[^?]+)\?.*/i, (...m) => m[1]) .replace(/^.+(awstrack\.me|redditmail\.com)\/.+(https:%2F%2F[^/]+).*/i, (...m) => decode(m[2])) .replace(/^.+(www\.google|safelinks\.protection\.outlook\.com|mailchimp\.com).+url=.+$/i, () => urlGetParam(url, 'url')) .replace(/^.+click\.godaddy\.com.+$/i, () => urlGetParam(url, 'redir')) .replace(/^.+delivery-status\.com.+$/i, () => urlGetParam(url, 'fb')) .replace(/^.+go\.dhlparcel\.nl.+\/([A-Za-z0-9_-]+)$/i, (...m) => base64Url(m[1])) .replace(/^(.+mopinion\.com.+)\?.*$/i, (...m) => m[1]) .replace(/^.+sellercentral\.amazon\.com\/nms\/redirect.+$/i, () => base64Url(urlGetParam(url, 'u'))) .replace(/^.+amazon\.com\/gp\/r\.html.+$/i, () => urlGetParam(url, 'U')) // Mandrill .replace(/^.+\/track\/click\/.+\?p=.+$/i, () => { let d = urlGetParam(url, 'p'); try { d = JSON.parse(base64Url(d)); if (d?.p) { d = JSON.parse(d.p); } } catch (e) { console.error(e); } return d?.url || url; }) // Remove invalid URL characters .replace(/[\s<>]+/gi, ''); nurl = new URL(nurl); let s = nurl.searchParams; [...s.keys()].forEach(key => stripParams.test(key) && s.delete(key)); return nurl.toString(); } catch (e) { console.dir({ error:e, url:url }); } return url; }; export const /** * @param {string} text * @returns {string} */ encodeHtml = text => (text?.toString?.() || '' + text).replace(htmlre, m => htmlmap[m]), /** * Clears the Message Html for viewing * @param {string} text * @returns {string} */ cleanHtml = (html, oAttachments) => { let aColor; const debug = false, // Config()->Get('debug', 'enable', false); detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'), bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()), result = { hasExternals: false }, findAttachmentByCid = cId => oAttachments.findByCid(cId), findLocationByCid = cId => { const attachment = findAttachmentByCid(cId); return attachment?.contentLocation ? attachment : 0; }, // convert body attributes to CSS tasks = { link: value => aColor = value, text: (value, node) => node.style.color = value, topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px', leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px', bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px', rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px' }, allowedAttributes = [ // defaults 'name', 'dir', 'lang', 'style', 'title', 'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href', 'border', 'bordercolor', 'charset', 'direction', // a 'download', 'hreflang', // body 'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink', // col 'align', 'valign', // font 'color', 'face', 'size', // hr 'noshade', // img 'hspace', 'sizes', 'srcset', 'vspace', // meter 'low', 'high', 'optimum', 'value', // ol 'reversed', 'start', // table 'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing', // th 'abbr', 'scope', // td 'colspan', 'rowspan', 'headers' ], disallowedTags = [ 'STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META', 'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA', 'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP' // Not supported by