From e265a0f1c175a989e82ff2a6ce2411f9e9582a48 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 2 Feb 2022 13:02:48 +0100 Subject: [PATCH] Moved the message HTML parsing from PHP to JavaScript Now we can properly parse PGP/MIME HTML messages --- dev/Common/Html.js | 305 +++++++++- dev/Common/Links.js | 10 + dev/Model/Attachment.js | 4 + dev/Model/AttachmentCollection.js | 4 +- dev/Model/Message.js | 67 ++- dev/Stores/User/Message.js | 4 - dev/Styles/User/MessageView.less | 4 +- dev/View/User/MailBox/MessageView.js | 226 +++----- .../app/libraries/MailSo/Base/HtmlUtils.php | 544 +----------------- .../libraries/RainLoop/Actions/Response.php | 83 +-- .../app/libraries/RainLoop/ServiceActions.php | 11 +- .../templates/Views/User/MailMessageView.html | 122 ++-- .../v/0.0.0/themes/SquaresDark/styles.css | 16 +- 13 files changed, 563 insertions(+), 837 deletions(-) diff --git a/dev/Common/Html.js b/dev/Common/Html.js index f035c2699..56afe8e84 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -1,4 +1,6 @@ -import { createElement } from 'Common/Globals'; +import { createElement, SettingsGet } from 'Common/Globals'; +import { forEachObjectEntry, pInt } from 'Common/Utils'; +import { proxy } from 'Common/Links'; const /* @@ -36,20 +38,303 @@ export const encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]), /** + * Clears the Message Html for viewing * @param {string} text * @returns {string} */ - clearHtml = html => { - html = html.replace(/(]*>)([\s\S]*?)(<\/pre>)/gi, aMatches => { - return (aMatches[1] + aMatches[2].trim() + aMatches[3].trim()).replace(/\r?\n/g, '
'); - }); + clearHtml = (html, contentLocationUrls) => { + const debug = false, // Config()->Get('debug', 'enable', false); + useProxy = !!SettingsGet('UseLocalProxyForExternalImages'), + detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'), + + result = { + hasExternals: false, + foundCIDs: [], + foundContentLocationUrls: [] + }, + + tpl = document.createElement('template'); + tpl.innerHTML = html + .replace(/(]*>)([\s\S]*?)(<\/pre>)/gi, aMatches => { + return (aMatches[1] + aMatches[2].trim() + aMatches[3].trim()).replace(/\r?\n/g, '
'); + }) + // \MailSo\Base\HtmlUtils::ClearComments() + .replace(//g, '') + // \MailSo\Base\HtmlUtils::ClearTags() + // eslint-disable-next-line max-len + .replace(/<\/?(link|form|center|base|meta|bgsound|keygen|source|object|embed|applet|mocha|i?frame|frameset|video|audio|area|map)(\s[\s\S]*?)?>/gi, '') + // GetDomFromText + .replace('', '') + .replace('', '') + .replace('', '') + // https://github.com/the-djmaze/snappymail/issues/187 + .replace(/]*>(((?!<\/a).)+]*><\/p>/i, '') + .replace(/]*>/i, '') + .replace(/<\?xml [^>]*\?>/i, '') + .trim(); + html = ''; + + // convert body attributes to CSS + const tasks = { + link: value => { + if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) { + tpl.content.querySelectorAll('a').forEach(node => node.style.color = 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' + }; + +// if (static::Config()->Get('labs', 'strict_html_parser', true)) + let allowedAttributes = [ + // defaults + 'name', + 'dir', 'lang', 'style', 'title', + 'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href', + 'border', 'bordercolor', 'charset', 'direction', 'language', + // a + 'coords', 'download', 'hreflang', 'shape', + // body + 'alink', 'bgproperties', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink', + 'marginwidth', 'marginheight', 'offset', + // button, + 'disabled', 'type', 'value', + // col + 'align', 'valign', + // font + 'color', 'face', 'size', + // form + 'novalidate', + // hr + 'noshade', + // img + 'hspace', 'sizes', 'srcset', 'vspace', 'usemap', + // input, textarea + 'checked', 'max', 'min', 'maxlength', 'multiple', 'pattern', 'placeholder', 'readonly', + 'required', 'step', 'wrap', + // label + 'for', + // meter + 'low', 'high', 'optimum', + // ol + 'reversed', 'start', + // option + 'selected', 'label', + // table + 'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing', + // th + 'abbr', 'scope', + // td + 'axis', 'colspan', 'rowspan', 'headers', 'nowrap' + ]; + + let disallowedAttributes = [ + 'id', 'class', 'contenteditable', 'designmode', 'formaction', 'manifest', 'action', + 'data-bind', 'data-reactid', 'xmlns', 'srcset', + 'fscommand', 'seeksegmenttime' + ]; + + tpl.content.querySelectorAll('*').forEach(oElement => { + const name = oElement.tagName.toUpperCase(), + oStyle = oElement.style, + getAttribute = name => oElement.hasAttribute(name) ? oElement.getAttribute(name).trim() : ''; + + if (['HEAD','STYLE','SVG','SCRIPT','TITLE','INPUT','BUTTON','TEXTAREA','SELECT'].includes(name) + || '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; + } + + if ('BODY' === name) { + forEachObjectEntry(tasks, (name, cb) => { + if (oElement.hasAttribute(name)) { + cb(getAttribute(name), oElement); + oElement.removeAttribute(name); + } + }); + } + + if ('TABLE' === name && oElement.hasAttribute('width')) { + let value = getAttribute('width'); + oElement.removeAttribute('width'); + oStyle.maxWidth = value + (/^[0-9]+$/.test(value) ? 'px' : ''); + oStyle.removeProperty('width'); + oStyle.removeProperty('min-width'); + } + + const aAttrsForRemove = []; + + if (oElement.hasAttributes()) { + let i = oElement.attributes.length; + while (i--) { + let sAttrName = oElement.attributes[i].name.toLowerCase(); + if (!allowedAttributes.includes(sAttrName) + || 'on' === sAttrName.slice(0, 2) + || 'form' === sAttrName.slice(0, 4) +// || 'data-' === sAttrName.slice(0, 5) +// || sAttrName.includes(':') + || disallowedAttributes.includes(sAttrName)) + { + oElement.removeAttribute(sAttrName); + aAttrsForRemove.push(sAttrName); + } + } + } + + if (oElement.hasAttribute('href')) { + let sHref = getAttribute('href'); + if (!/^([a-z]+):/i.test(sHref) && '//' !== sHref.slice(0, 2)) { + oElement.setAttribute('data-x-broken-href', sHref); + oElement.removeAttribute('href'); + } + if ('A' === name) { + oElement.setAttribute('rel', 'external nofollow noopener noreferrer'); + } + } + + // SVG xlink:href + /* + if (oElement.hasAttribute('xlink:href')) { + oElement.removeAttribute('xlink:href'); + } + */ + + if ('A' === name) { + oElement.setAttribute('tabindex', '-1'); + oElement.setAttribute('target', '_blank'); + } + + let skipStyle = false; + if (oElement.hasAttribute('src')) { + let sSrc = getAttribute('src'); + oElement.removeAttribute('src'); + + if (detectHiddenImages + && 'IMG' === name + && (('' != getAttribute('height') && 2 > pInt(getAttribute('height'))) + || ('' != getAttribute('width') && 2 > pInt(getAttribute('width'))) + || [ + 'email.microsoftemail.com/open', + 'github.com/notifications/beacon/', + 'mandrillapp.com/track/open', + 'list-manage.com/track/open' + ].filter(uri => sSrc.toLowerCase().includes(uri)).length + )) { + skipStyle = true; + oElement.setAttribute('style', 'display:none'); + oElement.setAttribute('data-x-hidden-src', sSrc); + } + else if (contentLocationUrls[sSrc]) + { + oElement.setAttribute('data-x-src-location', sSrc); + result.foundContentLocationUrls.push(sSrc); + } + else if ('cid:' === sSrc.slice(0, 4)) + { + oElement.setAttribute('data-x-src-cid', sSrc.slice(4)); + result.foundCIDs.push(sSrc.slice(4)); + } + else if (/^https?:\/\//i.test(sSrc) || '//' === sSrc.slice(0, 2)) + { + oElement.setAttribute('data-x-src', useProxy ? proxy(sSrc) : sSrc); + result.hasExternals = true; + } + else if ('data:image/' === sSrc.slice(0, 11)) + { + oElement.setAttribute('src', sSrc); + } + else + { + oElement.setAttribute('data-x-broken-src', sSrc); + } + } + + if (oElement.hasAttribute('background')) { + let sBackground = getAttribute('background'); + if (sBackground) { + oStyle.backgroundImage = 'url("' + sBackground + '")'; + } + oElement.removeAttribute('background'); + } + + if (oElement.hasAttribute('bgcolor')) { + let sBackgroundColor = getAttribute('bgcolor'); + if (sBackgroundColor) { + oStyle.backgroundColor = sBackgroundColor; + } + oElement.removeAttribute('bgcolor'); + } + + if (!skipStyle) { /* - \MailSo\Base\HtmlUtils::ClearHtml( - $sHtml, $bHasExternals, $aFoundCIDs, $aContentLocationUrls, $aFoundContentLocationUrls, - $fAdditionalExternalFilter, !!$this->Config()->Get('labs', 'try_to_detect_hidden_images', false) - ); + if ('fixed' === oStyle.position) { + oStyle.position = 'absolute'; + } */ - return html; + oStyle.removeProperty('behavior'); + oStyle.removeProperty('cursor'); + + const urls = { + cid: [], // 'data-x-style-cid' + remote: [], // 'data-x-style-url' + broken: [] // 'data-x-broken-style-src' + }; + ['backgroundImage', 'listStyleImage', 'content'].forEach(property => { + if (oStyle[property]) { + let value = oStyle[property], + found = value.match(/url\s*\(([^)]+)\)/gi); + if (found) { + oStyle[property] = null; + found = found[0].replace(/^["'\s]+|["'\s]+$/g, ''); + let lowerUrl = found.toLowerCase(); + if ('cid:' === lowerUrl.slice(0, 4)) { + found = found.slice(4); + urls.cid[property] = found + result.foundCIDs.push(found); + } else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) { + result.hasExternals = true; + urls.remote[property] = useProxy ? proxy(found) : found; + } else if ('data:image/' === lowerUrl.slice(0, 11)) { + oStyle[property] = value; + } else { + urls.broken[property] = found; + } + } + } + }); +// oStyle.removeProperty('background-image'); +// oStyle.removeProperty('list-style-image'); + + if (urls.cid.length) { + oElement.setAttribute('data-x-style-cid', JSON.stringify(urls.cid)); + } + if (urls.remote.length) { + oElement.setAttribute('data-x-style-url', JSON.stringify(urls.remote)); + } + if (urls.broken.length) { + oElement.setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken)); + } + } + + if (debug && aAttrsForRemove) { + oElement.setAttribute('data-removed-attrs', aAttrsForRemove.join(', ')); + } + }); + +// return tpl.content.firstChild; + result.html = tpl.innerHTML; + return result; }, // Removes background and color diff --git a/dev/Common/Links.js b/dev/Common/Links.js index 82ea80755..c34ff0964 100644 --- a/dev/Common/Links.js +++ b/dev/Common/Links.js @@ -47,6 +47,16 @@ export const attachmentDownload = (download, customSpecSuffix) => serverRequestRaw('Download', download, customSpecSuffix), + proxy = url => + SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), +/* + return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array( + 'Rnd' => \md5(\microtime(true)), + 'Token' => Utils::GetConnectionToken(), + 'Url' => $sUrl + )).'/'; +*/ + /** * @param {string} type * @returns {string} diff --git a/dev/Model/Attachment.js b/dev/Model/Attachment.js index 661e9a99c..db264acee 100644 --- a/dev/Model/Attachment.js +++ b/dev/Model/Attachment.js @@ -49,6 +49,10 @@ export class AttachmentModel extends AbstractModel { return attachment; } + contentId() { + return this.cid.replace(/^<+|>+$/g, ''); + } + /** * @returns {boolean} */ diff --git a/dev/Model/AttachmentCollection.js b/dev/Model/AttachmentCollection.js index 8fd67abae..40a0c49ca 100644 --- a/dev/Model/AttachmentCollection.js +++ b/dev/Model/AttachmentCollection.js @@ -32,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel * @returns {*} */ findByCid(cid) { - let regex = /^<+|>+$/g, cidc = cid.replace(regex, ''); - return this.find(item => cid === item.cid || cidc === item.cid || cidc === item.cid.replace(regex, '')); + cid = cid.replace(/^<+|>+$/g, ''); + return this.find(item => cid === item.contentId()); } } diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 1920217a4..402b75c18 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -4,7 +4,7 @@ import { MessagePriority } from 'Common/EnumsUser'; import { i18n } from 'Common/Translator'; import { doc } from 'Common/Globals'; -import { encodeHtml, removeColors, plainToHtml } from 'Common/Html'; +import { encodeHtml, removeColors, plainToHtml, clearHtml } from 'Common/Html'; import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils'; import { serverRequestRaw } from 'Common/Links'; @@ -82,6 +82,7 @@ export class MessageModel extends AbstractModel { isHtml: false, hasImages: false, hasExternals: false, + hasAttachments: false, pgpSigned: null, pgpEncrypted: null, @@ -121,7 +122,6 @@ export class MessageModel extends AbstractModel { this.uid = 0; this.hash = ''; this.requestHash = ''; - this.externalProxy = false; this.emails = []; this.from = new EmailCollectionModel; this.to = new EmailCollectionModel; @@ -160,6 +160,7 @@ export class MessageModel extends AbstractModel { this.isHtml(false); this.hasImages(false); this.hasExternals(false); + this.hasAttachments(false); this.attachments(new AttachmentCollectionModel); this.pgpSigned(null); @@ -178,6 +179,11 @@ export class MessageModel extends AbstractModel { this.hasFlaggedSubMessage(false); } + spamStatus() { + let spam = this.spamResult(); + return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : ''; + } + /** * @param {Array} properties * @returns {Array} @@ -393,7 +399,27 @@ export class MessageModel extends AbstractModel { html = removeColors(html); } - body.innerHTML = html; + const contentLocationUrls = {}, + oAttachments = this.attachments(); + oAttachments.forEach(oAttachment => { + if (oAttachment.cid && oAttachment.contentLocation) { + contentLocationUrls[oAttachment.contentId()] = oAttachment.contentLocation; + } + }); + + let result = clearHtml(html, contentLocationUrls); + this.hasExternals(result.hasExternals); +// this.hasInternals = result.foundCIDs.length || result.foundContentLocationUrls.length; + this.hasImages(body.rlHasImages = !!result.hasExternals); + + // Hide valid inline attachments in message view 'attachments' section + oAttachments.forEach(oAttachment => { + oAttachment.isLinked = result.foundCIDs.includes(oAttachment.contentId()) + || result.foundContentLocationUrls.includes(oAttachment.contentLocation) + }); + this.hasAttachments(oAttachments.hasVisible()); + + body.innerHTML = result.html; body.classList.toggle('html', 1); body.classList.toggle('plain', 0); @@ -422,12 +448,12 @@ export class MessageModel extends AbstractModel { el.src = attachment.linkPreview(); } } else if (data.xStyleCid) { - const name = data.xStyleCidName, - attachment = findAttachmentByCid(data.xStyleCid); - if (attachment && attachment.linkPreview && name) { - el.setAttribute('style', name + ": url('" + attachment.linkPreview() + "');" - + (el.getAttribute('style') || '')); - } + forEachObjectEntry(JSON.parse(data.xStyleCid), (name, cid) => { + const attachment = findAttachmentByCid(cid); + if (attachment && attachment.linkPreview && name) { + el.style[name] = "url('" + attachment.linkPreview() + "')"; + } + }); } }); @@ -453,7 +479,7 @@ export class MessageModel extends AbstractModel { .replace(email, '$1$2'); this.isHtml(false); - this.hasImages(this.hasExternals()); + this.hasImages(false); this.initView(); return true; } @@ -474,9 +500,6 @@ export class MessageModel extends AbstractModel { }); } - /** - * @param {boolean=} print = false - */ viewPopupMessage(print) { const timeStampInUTC = this.dateTimeStampInUTC() || 0, ccLine = this.ccToLine(false), @@ -503,6 +526,13 @@ export class MessageModel extends AbstractModel { } } + /** + * @param {boolean=} print = false + */ + popupMessage() { + this.viewPopupMessage(false); + } + printMessage() { this.viewPopupMessage(true); } @@ -539,7 +569,7 @@ export class MessageModel extends AbstractModel { this.priority(message.priority()); this.hasExternals(message.hasExternals()); - this.externalProxy = message.externalProxy; + this.hasAttachments(message.hasAttachments()); this.emails = message.emails; @@ -573,7 +603,7 @@ export class MessageModel extends AbstractModel { this.hasImages(false); body.rlHasImages = false; - let attr = this.externalProxy ? 'data-x-additional-src' : 'data-x-src'; + let attr = 'data-x-src'; body.querySelectorAll('[' + attr + ']').forEach(node => { if (node.matches('img')) { node.loading = 'lazy'; @@ -581,11 +611,8 @@ export class MessageModel extends AbstractModel { node.src = node.getAttribute(attr); }); - attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url'; - body.querySelectorAll('[' + attr + ']').forEach(node => { - node.setAttribute('style', ((node.getAttribute('style')||'') - + ';' + node.getAttribute(attr)) - .replace(/^[;\s]+/,'')); + body.querySelectorAll('[data-x-style-url]').forEach(node => { + forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')"); }); } } diff --git a/dev/Stores/User/Message.js b/dev/Stores/User/Message.js index 209b24300..2c3818877 100644 --- a/dev/Stores/User/Message.js +++ b/dev/Stores/User/Message.js @@ -374,10 +374,6 @@ export const MessageUserStore = new class { + (message.isPgpEncrypted() ? ' openpgp-encrypted' : '') + '">' + ''); - - body.rlHasImages = !!json.HasExternals; - message.hasImages(body.rlHasImages); - message.body = body; if (!SettingsUserStore.viewHTML() || !message.viewHtml()) { message.viewPlain(); diff --git a/dev/Styles/User/MessageView.less b/dev/Styles/User/MessageView.less index eef8b9245..da319ff88 100644 --- a/dev/Styles/User/MessageView.less +++ b/dev/Styles/User/MessageView.less @@ -174,7 +174,7 @@ html.rl-no-preview-pane { } } - .messageItem { + #messageItem { color: #000; height: 100%; @@ -532,7 +532,7 @@ html.rl-message-fullscreen { } } -html:not(.rl-mobile) .messageItem { +html:not(.rl-mobile) #messageItem { .buttonFull { display: none !important; } diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index f14cf8c2c..52104c9f4 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -14,7 +14,16 @@ import { MessageSetAction } from 'Common/EnumsUser'; -import { $htmlCL, leftPanelDisabled, keyScopeReal, moveAction, Settings, getFullscreenElement, exitFullscreen } from 'Common/Globals'; +import { + elementById, + $htmlCL, + leftPanelDisabled, + keyScopeReal, + moveAction, + Settings, + getFullscreenElement, + exitFullscreen +} from 'Common/Globals'; import { arrayLength, inFocus } from 'Common/Utils'; import { mailToHelper, showMessageComposer, initFullscreen } from 'Common/UtilsUser'; @@ -43,54 +52,58 @@ import { AbstractViewRight } from 'Knoin/AbstractViews'; import { PgpUserStore } from 'Stores/User/Pgp'; import { OpenPGPUserStore } from 'Stores/User/OpenPGP'; -const currentMessage = () => MessageUserStore.message(); - import PostalMime from '../../../../vendors/postal-mime/src/postal-mime.js'; import { AttachmentModel } from 'Model/Attachment'; -const mimeToMessage = (data, message) => { - // TODO: Check multipart/signed - const headers = data.split(/\r?\n\r?\n/)[0]; - if (/Content-Type:.+; boundary=/.test(headers)) { - // https://github.com/postalsys/postal-mime - (new PostalMime).parse(data).then(result => { - let html = result.html, - regex = /^<+|>+$/g; - result.attachments.forEach(data => { - let attachment = new AttachmentModel; - attachment.mimeType = data.mimeType; - attachment.fileName = data.filename; - attachment.content = data.content; // ArrayBuffer - attachment.cid = data.contentId || ''; - // Parse inline attachments from result.attachments - if (attachment.cid) { - if (html) { - let cid = 'cid:' + attachment.cid.replace(regex, ''), - b64 = 'data:' + data.mimeType + ';base64,' + btoa(String.fromCharCode(...new Uint8Array(data.content))); - html = html - .replace('src="' + cid + '"', 'src="' + b64 + '"') - .replace("src='" + cid + "'", "src='" + b64 + "'"); +const + oMessageScrollerDom = () => elementById('messageItem') || {}, + + currentMessage = () => MessageUserStore.message(), + + mimeToMessage = (data, message) => { + // TODO: Check multipart/signed + const headers = data.split(/\r?\n\r?\n/)[0]; + if (/Content-Type:.+; boundary=/.test(headers)) { + // https://github.com/postalsys/postal-mime + (new PostalMime).parse(data).then(result => { + let html = result.html, + regex = /^<+|>+$/g; + result.attachments.forEach(data => { + let attachment = new AttachmentModel; + attachment.mimeType = data.mimeType; + attachment.fileName = data.filename; + attachment.content = data.content; // ArrayBuffer + attachment.cid = data.contentId || ''; + // Parse inline attachments from result.attachments + if (attachment.cid) { + if (html) { + let cid = 'cid:' + attachment.cid.replace(regex, ''), + b64 = 'data:' + data.mimeType + ';base64,' + btoa(String.fromCharCode(...new Uint8Array(data.content))); + html = html + .replace('src="' + cid + '"', 'src="' + b64 + '"') + .replace("src='" + cid + "'", "src='" + b64 + "'"); + } } - } // data.disposition // data.related = true; - message.attachments.push(attachment); + message.attachments.push(attachment); + }); + message.hasAttachments(message.attachments.hasVisible()); +// result.headers; + // TODO: strip script tags and all other security that PHP also does + message.plain(result.text || ''); + if (html) { + message.html(html.replace(/<\/?script[\s\S]*?>/gi, '') || ''); + message.viewHtml(); + } else { + message.viewPlain(); + } }); -// result.headers; - // TODO: strip script tags and all other security that PHP also does - message.plain(result.text || ''); - if (html) { - message.html(html.replace(/<\/?script[\s\S]*?>/gi, '') || ''); - message.viewHtml(); - } else { - message.viewPlain(); - } - }); - return; - } - message.plain(data); - message.viewPlain(); -}; + return; + } + message.plain(data); + message.viewPlain(); + }; export class MailMessageView extends AbstractViewRight { constructor() { @@ -112,14 +125,17 @@ export class MailMessageView extends AbstractViewRight { } }, this.messageVisibility); - this.oMessageScrollerDom = null; - this.addObservables({ showAttachmentControls: false, downloadAsZipLoading: false, lastReplyAction_: '', showFullInfo: '1' === Local.get(ClientSideKeyName.MessageHeaderFullInfo), - moreDropdownTrigger: false + moreDropdownTrigger: false, + + // viewer + viewFromShort: '', + viewFromDkimData: ['none', ''], + viewToShort: '' }); this.moveAction = moveAction; @@ -161,32 +177,7 @@ export class MailMessageView extends AbstractViewRight { this.notSpamCommand = createCommandActionHelper(FolderType.NotSpam, true); // viewer - - this.viewFolder = ''; - this.viewUid = ''; this.viewHash = ''; - this.addObservables({ - viewSubject: '', - viewFromShort: '', - viewFromDkimData: ['none', ''], - viewToShort: '', - viewFrom: '', - viewTo: '', - viewCc: '', - viewBcc: '', - viewReplyTo: '', - viewTimeStamp: 0, - viewSize: '', - viewSpamScore: 0, - viewSpamStatus: '', - viewLineAsCss: '', - viewViewLink: '', - viewUnsubscribeLink: '', - viewDownloadLink: '', - viewIsImportant: false, - viewIsFlagged: false, - hasVirus: null - }); this.addComputables({ allowAttachmentControls: () => this.attachmentsActions.length && Settings.capa(Capa.AttachmentsActions), @@ -228,9 +219,6 @@ export class MailMessageView extends AbstractViewRight { return ''; }, - pgpSigned: () => currentMessage() && !!currentMessage().pgpSigned(), - pgpEncrypted: () => currentMessage() - && !!(currentMessage().pgpEncrypted() || currentMessage().isPgpEncrypted()), pgpSupported: () => currentMessage() && PgpUserStore.isSupported(), messageListOrViewLoading: @@ -258,36 +246,13 @@ export class MailMessageView extends AbstractViewRight { this.scrollMessageToTop(); } - let spam = message.spamResult(); - - this.viewFolder = message.folder; - this.viewUid = message.uid; this.viewHash = message.hash; - this.viewSubject(message.subject()); this.viewFromShort(message.fromToLine(true, true)); this.viewFromDkimData(message.fromDkimData()); this.viewToShort(message.toToLine(true, true)); - this.viewFrom(message.fromToLine()); - this.viewTo(message.toToLine()); - this.viewCc(message.ccToLine()); - this.viewBcc(message.bccToLine()); - this.viewReplyTo(message.replyToToLine()); - this.viewTimeStamp(message.dateTimeStampInUTC()); - this.viewSize(message.friendlySize()); - this.viewSpamScore(message.spamScore()); - this.viewSpamStatus(spam ? i18n(message.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : ''); - this.viewLineAsCss(message.lineAsCss()); - this.viewViewLink(message.viewLink()); - this.viewUnsubscribeLink(message.getFirstUnsubsribeLink()); - this.viewDownloadLink(message.downloadLink()); - this.viewIsImportant(message.isImportant()); - this.viewIsFlagged(message.isFlagged()); - this.hasVirus(message.hasVirus()); } else { MessageUserStore.selectorMessageSelected(null); - this.viewFolder = ''; - this.viewUid = ''; this.viewHash = ''; this.scrollMessageToTop(); @@ -303,11 +268,6 @@ export class MailMessageView extends AbstractViewRight { } }); - MessageUserStore.messageViewTrigger.subscribe(() => { - const message = currentMessage(); - this.viewIsFlagged(message ? message.isFlagged() : false); - }); - this.lastReplyAction(Local.get(ClientSideKeyName.LastReplyAction) || ComposeType.Reply); addEventListener('mailbox.message-view.toggle-full-screen', () => this.toggleFullScreen()); @@ -357,8 +317,6 @@ export class MailMessageView extends AbstractViewRight { } onBuild(dom) { - this.oMessageScrollerDom = dom.querySelector('.messageItem'); - this.fullScreenMode.subscribe(value => value && currentMessage() && AppUserStore.focusedState(Scope.MessageView)); @@ -533,7 +491,7 @@ export class MailMessageView extends AbstractViewRight { // change focused state shortcuts.add('arrowleft', '', Scope.MessageView, () => { if (!this.fullScreenMode() && currentMessage() && SettingsUserStore.usePreviewPane() - && !this.oMessageScrollerDom.scrollLeft) { + && !oMessageScrollerDom().scrollLeft) { AppUserStore.focusedState(Scope.MessageList); return false; } @@ -606,11 +564,11 @@ export class MailMessageView extends AbstractViewRight { } scrollMessageToTop() { - this.oMessageScrollerDom.scrollTop = (50 < this.oMessageScrollerDom.scrollTop) ? 50 : 0; + oMessageScrollerDom().scrollTop = (50 < oMessageScrollerDom().scrollTop) ? 50 : 0; } scrollMessageToLeft() { - this.oMessageScrollerDom.scrollLeft = 0; + oMessageScrollerDom().scrollLeft = 0; } downloadAsZip() { @@ -637,7 +595,7 @@ export class MailMessageView extends AbstractViewRight { * @returns {void} */ showImages() { - currentMessage() && currentMessage().showExternalImages(); + currentMessage().showExternalImages(); } /** @@ -654,7 +612,7 @@ export class MailMessageView extends AbstractViewRight { */ readReceipt() { let oMessage = currentMessage() - if (oMessage && oMessage.readReceipt()) { + if (oMessage.readReceipt()) { Remote.request('SendReadReceiptMessage', null, { MessageFolder: oMessage.folder, MessageUid: oMessage.uid, @@ -672,41 +630,39 @@ export class MailMessageView extends AbstractViewRight { } } - pgpDecrypt(self) { - const message = self.message(); - message && PgpUserStore.decrypt(message).then(result => { + pgpDecrypt() { + const oMessage = currentMessage(); + PgpUserStore.decrypt(oMessage).then(result => { if (result && result.data) { - mimeToMessage(result.data, message); + mimeToMessage(result.data, oMessage); } }); } - pgpVerify(self) { - if (self.pgpSigned()) { - const message = self.message(), - sender = message && message.from[0].email; - PgpUserStore.hasPublicKeyForEmails([message.from[0].email]).then(mode => { + pgpVerify() { + const oMessage = currentMessage(); + if (oMessage.pgpSigned()) { + const sender = oMessage.from[0].email; + PgpUserStore.hasPublicKeyForEmails([oMessage.from[0].email]).then(mode => { if ('gnupg' === mode) { - let params = message.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" } - if (params) { - params.Folder = message.folder; - params.Uid = message.uid; - rl.app.Remote.post('MessagePgpVerify', null, params) - .then(data => { - // TODO - console.dir(data); - }) - .catch(error => { - // TODO - console.dir(error); - }); - } + let params = oMessage.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" } + params.Folder = oMessage.folder; + params.Uid = oMessage.uid; + rl.app.Remote.post('MessagePgpVerify', null, params) + .then(data => { + // TODO + console.dir(data); + }) + .catch(error => { + // TODO + console.dir(error); + }); } else if ('openpgp' === mode) { const publicKey = OpenPGPUserStore.getPublicKeyFor(sender); - OpenPGPUserStore.verify(message.plain(), null/*detachedSignature*/, publicKey).then(result => { + OpenPGPUserStore.verify(oMessage.plain(), null/*detachedSignature*/, publicKey).then(result => { if (result) { - message.plain(result.data); - message.viewPlain(); + oMessage.plain(result.data); + oMessage.viewPlain(); console.dir({signatures:result.signatures}); } /* @@ -714,7 +670,7 @@ export class MailMessageView extends AbstractViewRight { i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', { USER: validKey.user + ' (' + validKey.id + ')' }); - message.getText() + oMessage.getText() } else { const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null, additional = keyIds diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php index 65e782f10..84ef65bcc 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php @@ -66,16 +66,40 @@ abstract class HtmlUtils $sText = \tidy_repair_string($sText, $tidyConfig, 'utf8'); } - $sHtmlAttrs = $sBodyAttrs = ''; + $sText = \preg_replace(array( + '/]*><\/p>/i', + '/]*>/i', + '/<\?xml [^>]*\?>/i' + ), '', $sText); - $sText = static::ClearFastTags($sText); + $sHtmlAttrs = $sBodyAttrs = ''; $sText = static::ClearBodyAndHtmlTag($sText, $sHtmlAttrs, $sBodyAttrs); + $aMatch = array(); + if (\preg_match('/]+)>/im', $sText, $aMatch) && !empty($aMatch[1])) { + $sHtmlAttrs = $aMatch[1]; + } + + $aMatch = array(); + if (\preg_match('/]+)>/im', $sText, $aMatch) && !empty($aMatch[1])) { + $sBodyAttrs = $aMatch[1]; + } + +// $sText = \preg_replace('/^.*]*)>/si', '', $sText); + $sText = \preg_replace('/<\/?(head|body|html)(\\s[^>]*)?>/si', '', $sText); + + $sHtmlAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/i', '', $sHtmlAttrs); + $sHtmlAttrs = \preg_replace('/xmlns:[a-z]=\'[^\']*\'/i', '', $sHtmlAttrs); + $sHtmlAttrs = \preg_replace('/xmlns="[^"]*"/i', '', $sHtmlAttrs); + $sHtmlAttrs = \preg_replace('/xmlns=\'[^\']*\'/i', '', $sHtmlAttrs); + $sBodyAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/i', '', $sBodyAttrs); + $sBodyAttrs = \preg_replace('/xmlns:[a-z]=\'[^\']*\'/i', '', $sBodyAttrs); + $oDom = self::createDOMDocument(); @$oDom->loadHTML(''. - ''. + ''. ''. - ''.\MailSo\Base\Utils::Utf8Clear($sText).''); + ''.\MailSo\Base\Utils::Utf8Clear($sText).''); $oDom->normalizeDocument(); @@ -168,70 +192,6 @@ abstract class HtmlUtils return $sResult; } - private static function ClearBodyAndHtmlTag(string $sHtml, string &$sHtmlAttrs = '', string &$sBodyAttrs = '') : string - { - $aMatch = array(); - if (\preg_match('/]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1])) - { - $sHtmlAttrs = $aMatch[1]; - } - - $aMatch = array(); - if (\preg_match('/]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1])) - { - $sBodyAttrs = $aMatch[1]; - } - -// $sHtml = \preg_replace('/^.*]*)>/si', '', $sHtml); - $sHtml = \preg_replace('/<\/?(head|body|html)(\\s[^>]*)?>/si', '', $sHtml); - - $sHtmlAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/i', '', $sHtmlAttrs); - $sHtmlAttrs = \preg_replace('/xmlns:[a-z]=\'[^\']*\'/i', '', $sHtmlAttrs); - $sHtmlAttrs = \preg_replace('/xmlns="[^"]*"/i', '', $sHtmlAttrs); - $sHtmlAttrs = \preg_replace('/xmlns=\'[^\']*\'/i', '', $sHtmlAttrs); - $sBodyAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/i', '', $sBodyAttrs); - $sBodyAttrs = \preg_replace('/xmlns:[a-z]=\'[^\']*\'/i', '', $sBodyAttrs); - - $sHtmlAttrs = \trim($sHtmlAttrs); - $sBodyAttrs = \trim($sBodyAttrs); - - return $sHtml; - } - - private static function ClearFastTags(string $sHtml) : string - { - return \preg_replace(array( - '/]*><\/p>/i', - '/]*>/i', - '/<\?xml [^>]*\?>/i' - ), '', $sHtml); - } - - private static function ClearComments(\DOMDocument $oDom) : void - { - $aRemove = array(); - - $oXpath = new \DOMXpath($oDom); - $oComments = $oXpath->query('//comment()'); - if ($oComments) - { - foreach ($oComments as $oComment) - { - $aRemove[] = $oComment; - } - } - - unset($oXpath, $oComments); - - foreach ($aRemove as /* @var $oElement \DOMElement */ $oElement) - { - if (isset($oElement->parentNode)) - { - @$oElement->parentNode->removeChild($oElement); - } - } - } - private static function ClearTags(\DOMDocument $oDom, bool $bClearStyleAndHead = true) : void { $aRemoveTags = array( @@ -275,454 +235,6 @@ abstract class HtmlUtils } } - /** - * @param callback|null $fAdditionalExternalFilter = null - */ - private static function ClearStyle(string $sStyle, \DOMElement $oElement, bool &$bHasExternals, - array &$aFoundCIDs, array $aContentLocationUrls, array &$aFoundContentLocationUrls, callable $fAdditionalExternalFilter = null) - { - $sStyle = \trim($sStyle, " \n\r\t\v\0;"); - $aOutStyles = array(); - $aStyles = \explode(';', $sStyle); - - $aMatch = array(); - foreach ($aStyles as $sStyleItem) - { - $aStyleValue = \explode(':', $sStyleItem, 2); - $sName = \trim(\strtolower($aStyleValue[0])); - $sValue = isset($aStyleValue[1]) ? \trim($aStyleValue[1]) : ''; - - if ('position' === $sName && 'fixed' === \strtolower($sValue)) - { - $sValue = 'absolute'; - } - - if (!\strlen($sName) || !\strlen($sValue)) - { - continue; - } - - $sStyleItem = $sName.': '.$sValue; - $aStyleValue = array($sName, $sValue); - - /*if (\in_array($sName, array('position', 'left', 'right', 'top', 'bottom', 'behavior', 'cursor'))) - { - // skip - } - else */if (\in_array($sName, array('behavior', 'pointer-events', 'visibility')) || - ('cursor' === $sName && !\in_array(\strtolower($sValue), array('none', 'cursor'))) || - ('display' === $sName && 'none' === \strtolower($sValue)) || - \preg_match('/expression/i', $sValue) || - ('text-indent' === $sName && '-' === \substr(trim($sValue), 0, 1)) - ) - { - // skip - } - else if (\in_array($sName, array('background-image', 'background', 'list-style', 'list-style-image', 'content')) - && \preg_match('/url[\s]?\(([^)]+)\)/im', $sValue, $aMatch) && !empty($aMatch[1])) - { - $sFullUrl = \trim($aMatch[0], '"\' '); - $sUrl = \trim($aMatch[1], '"\' '); - $sStyleValue = \trim(\preg_replace('/[\s]+/', ' ', \str_replace($sFullUrl, '', $sValue))); - $sStyleItem = empty($sStyleValue) ? '' : $sName.': '.$sStyleValue; - - if ('cid:' === \strtolower(\substr($sUrl, 0, 4))) - { - if ($oElement) - { - $oElement->setAttribute('data-x-style-cid-name', - 'background' === $sName ? 'background-image' : $sName); - - $oElement->setAttribute('data-x-style-cid', \substr($sUrl, 4)); - - $aFoundCIDs[] = \substr($sUrl, 4); - } - } - else - { - if ($oElement) - { - if (\preg_match('/http[s]?:\/\//i', $sUrl) || '//' === \substr($sUrl, 0, 2)) - { - $bHasExternals = true; - if (\in_array($sName, array('background-image', 'list-style-image', 'content'))) - { - $sStyleItem = ''; - } - - $sTemp = ''; - if ($oElement->hasAttribute('data-x-style-url')) - { - $sTemp = \trim($oElement->getAttribute('data-x-style-url')); - } - - $sTemp = empty($sTemp) ? '' : (';' === \substr($sTemp, -1) ? $sTemp.' ' : $sTemp.'; '); - - $oElement->setAttribute('data-x-style-url', \trim($sTemp. - ('background' === $sName ? 'background-image' : $sName).': '.$sFullUrl, ' ;')); - - if ($fAdditionalExternalFilter) - { - $sAdditionalResult = $fAdditionalExternalFilter($sUrl); - if (\strlen($sAdditionalResult)) - { - $oElement->setAttribute('data-x-additional-style-url', - ('background' === $sName ? 'background-image' : $sName).': url('.$sAdditionalResult.')'); - } - } - } - else if ('data:image/' !== \strtolower(\substr(\trim($sUrl), 0, 11))) - { - $oElement->setAttribute('data-x-broken-style-src', $sFullUrl); - } - } - } - - if (!empty($sStyleItem)) - { - $aOutStyles[] = $sStyleItem; - } - } - else if ('height' === $sName) - { -// $aOutStyles[] = 'min-'.ltrim($sStyleItem); - $aOutStyles[] = $sStyleItem; - } - else - { - $aOutStyles[] = $sStyleItem; - } - } - - return \implode(';', $aOutStyles); - } - - /** - * Clears the MailSo\Mail\Message Html for viewing - */ - public static function ClearHtml(string $sHtml, bool &$bHasExternals = false, array &$aFoundCIDs = array(), - array $aContentLocationUrls = array(), array &$aFoundContentLocationUrls = array(), - callable $fAdditionalExternalFilter = null, bool $bTryToDetectHiddenImages = false) - { - $sResult = ''; - - $sHtml = null === $sHtml ? '' : \trim($sHtml); - if (!\strlen($sHtml)) - { - return ''; - } - - if ($fAdditionalExternalFilter && !\is_callable($fAdditionalExternalFilter)) - { - $fAdditionalExternalFilter = null; - } - - $bHasExternals = false; - - // Dom Part - $oDom = static::GetDomFromText($sHtml); - unset($sHtml); - - if (!$oDom) - { - return ''; - } - - static::ClearComments($oDom); - static::ClearTags($oDom); - - $sLinkColor = ''; - $aNodes = $oDom->getElementsByTagName('*'); - foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) - { - $aAttrsForRemove = array(); - $sTagNameLower = \strtolower($oElement->tagName); - - $sStyles = $oElement->hasAttribute('style') ? \trim($oElement->getAttribute('style'), " \n\r\t\v\0;") : ''; - - // convert body attributes to styles - if ('body' === $sTagNameLower) - { - $aAttrs = array( - 'link' => '', - 'text' => '', - 'topmargin' => '', - 'leftmargin' => '', - 'bottommargin' => '', - 'rightmargin' => '' - ); - - if (isset($oElement->attributes)) - { - foreach ($oElement->attributes as $sAttrName => /* @var $oAttributeNode \DOMNode */ $oAttributeNode) - { - if ($oAttributeNode && isset($oAttributeNode->nodeValue)) - { - $sAttrNameLower = \trim(\strtolower($sAttrName)); - if (isset($aAttrs[$sAttrNameLower]) && '' === $aAttrs[$sAttrNameLower]) - { - $aAttrs[$sAttrNameLower] = array($sAttrName, \trim($oAttributeNode->nodeValue)); - } - } - } - } - - $aStyles = array(); - foreach ($aAttrs as $sIndex => $aItem) - { - if (\is_array($aItem)) - { - $oElement->removeAttribute($aItem[0]); - - switch ($sIndex) - { - case 'link': - $sLinkColor = \trim($aItem[1]); - if (!\preg_match('/^#[abcdef0-9]{3,6}$/i', $sLinkColor)) - { - $sLinkColor = ''; - } - break; - case 'text': - $aStyles[] = 'color: '.$aItem[1]; - break; - case 'topmargin': - $aStyles[] = 'margin-top: '.((int) $aItem[1]).'px'; - break; - case 'leftmargin': - $aStyles[] = 'margin-left: '.((int) $aItem[1]).'px'; - break; - case 'bottommargin': - $aStyles[] = 'margin-bottom: '.((int) $aItem[1]).'px'; - break; - case 'rightmargin': - $aStyles[] = 'margin-right: '.((int) $aItem[1]).'px'; - break; - } - } - } - - if (\count($aStyles)) - { - $sStyles .= '; ' . \implode('; ', $aStyles); - } - } - - else if ('iframe' === $sTagNameLower || 'frame' === $sTagNameLower) - { - $oElement->setAttribute('src', 'javascript:false'); - } - - else if ('a' === $sTagNameLower && !empty($sLinkColor)) - { - $sStyles .= '; color: '.$sLinkColor; - } - - else if ('table' === $sTagNameLower && $oElement->hasAttribute('width')) - { - $aAttrsForRemove[] = 'width'; - $sWidth = $oElement->getAttribute('width'); - if (\ctype_digit($sWidth)) { - $sWidth .= 'px'; - } - $sStyles .= "; max-width:{$sWidth}"; - } - - if ($oElement->hasAttributes() && isset($oElement->attributes) && $oElement->attributes) - { - $aHtmlAllowedAttributes = isset(\MailSo\Config::$HtmlStrictAllowedAttributes) && - \is_array(\MailSo\Config::$HtmlStrictAllowedAttributes) && \count(\MailSo\Config::$HtmlStrictAllowedAttributes) ? - \MailSo\Config::$HtmlStrictAllowedAttributes : null; - - foreach ($oElement->attributes as $sAttrName => $oAttr) - { - if ($sAttrName && $oAttr) - { - $sAttrNameLower = \trim(\strtolower($sAttrName)); - if (($aHtmlAllowedAttributes && !\in_array($sAttrNameLower, $aHtmlAllowedAttributes)) - || 'on' === \substr($sAttrNameLower, 0, 2) -// || 'data-' === \substr($sAttrNameLower, 0, 5) -// || \strpos($sAttrNameLower, ':') - || \in_array($sAttrNameLower, array( - 'id', 'class', 'contenteditable', 'designmode', 'formaction', 'manifest', 'action', - 'data-bind', 'data-reactid', 'xmlns', 'srcset', - 'fscommand', 'seeksegmenttime' - ))) - { - $aAttrsForRemove[] = $sAttrName; - } - } - } - } - - if ($oElement->hasAttribute('href')) - { - $sHref = \trim($oElement->getAttribute('href')); - if (!\preg_match('/^([a-z]+):/i', $sHref) && '//' !== \substr($sHref, 0, 2)) - { - $oElement->setAttribute('data-x-broken-href', $sHref); - $oElement->setAttribute('href', 'javascript:false'); - } - - if ('a' === $sTagNameLower) - { - $oElement->setAttribute('rel', 'external nofollow noopener noreferrer'); - } - } - - $sLinkHref = \trim($oElement->getAttribute('xlink:href')); - if ($sLinkHref && !\preg_match('/^(http[s]?):/i', $sLinkHref) && '//' !== \substr($sLinkHref, 0, 2)) - { - $oElement->setAttribute('data-x-blocked-xlink-href', $sLinkHref); - $oElement->removeAttribute('xlink:href'); - } - - if (\in_array($sTagNameLower, array('a', 'form', 'area'))) - { - $oElement->setAttribute('target', '_blank'); - } - - if (\in_array($sTagNameLower, array('a', 'form', 'area', 'input', 'button', 'textarea'))) - { - $oElement->setAttribute('tabindex', '-1'); - } - - $skipStyle = false; - if ($bTryToDetectHiddenImages && 'img' === $sTagNameLower) - { - $sAlt = $oElement->hasAttribute('alt') - ? \trim($oElement->getAttribute('alt')) : ''; - - if ($oElement->hasAttribute('src') && '' === $sAlt) - { - $aH = array( - 'email.microsoftemail.com/open', - 'github.com/notifications/beacon/', - 'mandrillapp.com/track/open', - 'list-manage.com/track/open' - ); - - $sH = $oElement->hasAttribute('height') - ? \trim($oElement->getAttribute('height')) : ''; - -// $sW = $oElement->hasAttribute('width') -// ? \trim($oElement->getAttribute('width')) : ''; - - $sSrc = \trim($oElement->getAttribute('src')); - - $bC = \in_array($sH, array('1', '0', '1px', '0px')) || - \preg_match('/display:\\s*none|visibility:\\s*hidden|height:\\s*0/i', $sStyles); - - if (!$bC) - { - $sSrcLower = \strtolower($sSrc); - foreach ($aH as $sLine) - { - if (false !== \strpos($sSrcLower, $sLine)) - { - $bC = true; - break; - } - } - } - - if ($bC) - { - $skipStyle = true; - $oElement->setAttribute('style', 'display:none'); - $oElement->setAttribute('data-x-hidden-src', $sSrc); - $oElement->removeAttribute('src'); - } - } - } - - if ($oElement->hasAttribute('src')) - { - $sSrc = \trim($oElement->getAttribute('src')); - $oElement->removeAttribute('src'); - - if (\in_array($sSrc, $aContentLocationUrls)) - { - $oElement->setAttribute('data-x-src-location', $sSrc); - $aFoundContentLocationUrls[] = $sSrc; - } - else if ('cid:' === \strtolower(\substr($sSrc, 0, 4))) - { - $oElement->setAttribute('data-x-src-cid', \substr($sSrc, 4)); - $aFoundCIDs[] = \substr($sSrc, 4); - } - else - { - if (\preg_match('/^http[s]?:\/\//i', $sSrc) || '//' === \substr($sSrc, 0, 2)) - { - $oElement->setAttribute('data-x-src', $sSrc); - if ($fAdditionalExternalFilter) - { - $sCallResult = $fAdditionalExternalFilter($sSrc); - if (\strlen($sCallResult)) - { - $oElement->setAttribute('data-x-additional-src', $sCallResult); - } - } - - $bHasExternals = true; - } - else if ('data:image/' === \strtolower(\substr($sSrc, 0, 11))) - { - $oElement->setAttribute('src', $sSrc); - } - else - { - $oElement->setAttribute('data-x-broken-src', $sSrc); - } - } - } - - $sBackground = $oElement->hasAttribute('background') - ? \trim($oElement->getAttribute('background')) : ''; - $sBackgroundColor = $oElement->hasAttribute('bgcolor') - ? \trim($oElement->getAttribute('bgcolor')) : ''; - - if (!empty($sBackground) || !empty($sBackgroundColor)) - { - $aStyles = array(); - - if (!empty($sBackground)) - { - $aStyles[] = 'background-image: url(\''.$sBackground.'\')'; - $oElement->removeAttribute('background'); - } - - if (!empty($sBackgroundColor)) - { - $aStyles[] = 'background-color: '.$sBackgroundColor; - $oElement->removeAttribute('bgcolor'); - } - - $sStyles .= '; ' . \implode('; ', $aStyles); - } - - if ($sStyles && !$skipStyle) - { - $oElement->setAttribute('style', - static::ClearStyle($sStyles, $oElement, $bHasExternals, - $aFoundCIDs, $aContentLocationUrls, $aFoundContentLocationUrls, $fAdditionalExternalFilter)); - } - - foreach ($aAttrsForRemove as $sName) - { - @$oElement->removeAttribute($sName); - } - - if (\MailSo\Config::$HtmlStrictDebug && $aAttrsForRemove) - { - $oElement->setAttribute('data-removed-attrs', \implode(',', $aAttrsForRemove)); - } - } - - return static::GetTextFromDom($oDom, true); - } - /** * Used by DoSaveMessage() and DoSendMessage() */ diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php index 468e298f3..e1b15f240 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -145,7 +145,7 @@ trait Response * @return mixed */ private $aCheckableFolder = null; - private function responseObject($mResponse, string $sParent = '', array $aParameters = array()) + private function responseObject($mResponse, string $sParent = '') { if (!($mResponse instanceof \JsonSerializable)) { @@ -158,7 +158,7 @@ trait Response { foreach ($mResponse as $iKey => $oItem) { - $mResponse[$iKey] = $this->responseObject($oItem, $sParent, $aParameters); + $mResponse[$iKey] = $this->responseObject($oItem, $sParent); } } @@ -180,7 +180,7 @@ trait Response // \MailSo\Mime\EmailCollection foreach (['ReplyTo','From','To','Cc','Bcc','Sender','DeliveredTo','ReplyTo'] as $prop) { - $mResult[$prop] = $this->responseObject($mResult[$prop], $sParent, $aParameters); + $mResult[$prop] = $this->responseObject($mResult[$prop], $sParent); } $sSubject = $mResult['Subject']; @@ -202,56 +202,12 @@ trait Response if ('Message' === $sParent) { - $bHasExternals = false; - $aFoundCIDs = array(); - $aContentLocationUrls = array(); - $aFoundContentLocationUrls = array(); - - $oAttachments = /* @var \MailSo\Mail\AttachmentCollection */ $mResponse->Attachments(); - if ($oAttachments && 0 < $oAttachments->count()) - { - foreach ($oAttachments as /* @var \MailSo\Mail\Attachment */ $oAttachment) - { - if ($oAttachment) - { - $sContentLocation = $oAttachment->ContentLocation(); - if ($sContentLocation && \strlen($sContentLocation)) - { - $aContentLocationUrls[] = $sContentLocation; - } - } - } - } - $mResult['DraftInfo'] = $mResponse->DraftInfo(); $mResult['InReplyTo'] = $mResponse->InReplyTo(); $mResult['UnsubsribeLinks'] = $mResponse->UnsubsribeLinks(); $mResult['References'] = $mResponse->References(); - $fAdditionalExternalFilter = null; - if ($this->Config()->Get('labs', 'use_local_proxy_for_external_images', false)) - { - $fAdditionalExternalFilter = function ($sUrl) { - return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array( - 'Rnd' => \md5(\microtime(true)), - 'Token' => Utils::GetConnectionToken(), - 'Url' => $sUrl - )).'/'; - }; - } - - $sHtml = $mResponse->Html(); - $sHtml = \preg_replace_callback('/(]*>)([\s\S\r\n\t]*?)(<\/pre>)/mi', function ($aMatches) { - return \preg_replace('/[\r\n]+/', '
', $aMatches[1].\trim($aMatches[2]).$aMatches[3]); - }, $sHtml); - $mResult['Html'] = \strlen($sHtml) ? \MailSo\Base\HtmlUtils::ClearHtml( - $sHtml, $bHasExternals, $aFoundCIDs, $aContentLocationUrls, $aFoundContentLocationUrls, - $fAdditionalExternalFilter, !!$this->Config()->Get('labs', 'try_to_detect_hidden_images', false) - ) : ''; - unset($sHtml); - - $mResult['ExternalProxy'] = null !== $fAdditionalExternalFilter; - + $mResult['Html'] = $mResponse->Html(); $mResult['Plain'] = $mResponse->Plain(); // $this->GetCapa(false, Capa::OPEN_PGP) || $this->GetCapa(false, Capa::GNUPG) @@ -259,14 +215,7 @@ trait Response $mResult['PgpSigned'] = $mResponse->PgpSigned(); $mResult['PgpEncrypted'] = $mResponse->PgpEncrypted(); - $mResult['HasExternals'] = $bHasExternals; - $mResult['HasInternals'] = \count($aFoundCIDs) || \count($aFoundContentLocationUrls); - -// $mResult['FoundCIDs'] = $aFoundCIDs; - $mResult['Attachments'] = $this->responseObject($oAttachments, $sParent, \array_merge($aParameters, array( - 'FoundCIDs' => $aFoundCIDs, - 'FoundContentLocationUrls' => $aFoundContentLocationUrls - ))); + $mResult['Attachments'] = $this->responseObject($mResponse->Attachments(), $sParent); $mResult['ReadReceipt'] = $mResponse->ReadReceipt(); @@ -298,29 +247,11 @@ trait Response if ($mResponse instanceof \MailSo\Mail\Attachment) { $mResult = $mResponse->jsonSerialize(); - - $oAccount = $this->getAccountFromToken(); - - $aFoundCIDs = (isset($aParameters['FoundCIDs']) && \is_array($aParameters['FoundCIDs'])) - ? $aParameters['FoundCIDs'] : []; - - $aFoundContentLocationUrls = (isset($aParameters['FoundContentLocationUrls']) && \is_array($aParameters['FoundContentLocationUrls'])) - ? $aParameters['FoundContentLocationUrls'] : []; - - if ($aFoundContentLocationUrls) { - $aFoundCIDs = \array_merge($aFoundCIDs, $aFoundContentLocationUrls); - } - - // Hides valid inline attachments in message view 'attachments' section - $mResult['IsLinked'] = \in_array(\trim(\trim($mResponse->Cid()), '<>'), $aFoundCIDs) - || \in_array(\trim($mResponse->ContentLocation()), $aFoundContentLocationUrls); - $mResult['Framed'] = $this->isFileHasFramedPreview($mResult['FileName']); $mResult['IsThumbnail'] = $this->GetCapa(false, Capa::ATTACHMENT_THUMBNAILS) && $this->isFileHasThumbnail($mResult['FileName']); - $mResult['Download'] = Utils::EncodeKeyValuesQ(array( 'V' => APP_VERSION, - 'Account' => $oAccount->Hash(), + 'Account' => $this->getAccountFromToken()->Hash(), 'Folder' => $mResult['Folder'], 'Uid' => $mResult['Uid'], 'MimeIndex' => $mResult['MimeIndex'], @@ -368,7 +299,7 @@ trait Response if ($mResponse instanceof \MailSo\Base\Collection) { $mResult = $mResponse->jsonSerialize(); - $mResult['@Collection'] = $this->responseObject($mResult['@Collection'], $sParent, $aParameters); + $mResult['@Collection'] = $this->responseObject($mResult['@Collection'], $sParent); if ($mResponse instanceof \MailSo\Mail\EmailCollection) { return \array_slice($mResult['@Collection'], 0, 100); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index 177c3cb4e..55c218cc2 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -339,10 +339,17 @@ class ServiceActions if (!empty($sData) && $this->Config()->Get('labs', 'use_local_proxy_for_external_images', false)) { $this->oActions->verifyCacheByKey($sData); - $aData = Utils::DecodeKeyValuesQ($sData); + if (!\is_array($aData) && $this->oActions->GetAccount()) { + $aData = [ +// 'Rnd' => md5(\microtime(true)), + 'Token' => Utils::GetConnectionToken(), + 'Url' => \MailSo\Base\Utils::UrlSafeBase64Decode($sData) + ]; + } if (\is_array($aData) && !empty($aData['Token']) && !empty($aData['Url']) && $aData['Token'] === Utils::GetConnectionToken()) { + \header('X-Content-Location: '.$aData['Url']); $iCode = 404; $sContentType = ''; $mResult = $this->oHttp->GetUrlAsString($aData['Url'], 'SnappyMail External Proxy', $sContentType, $iCode); @@ -355,6 +362,8 @@ class ServiceActions $this->oActions->cacheByKey($sData); \header('Content-Type: '.$sContentType); + \header('Cache-Control: public'); + \header('Expires: '.\gmdate('D, j M Y H:i:s', 2592000 + \time()).' UTC'); echo $mResult; } } diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html index 595e8992e..77fb64f8d 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html @@ -36,7 +36,8 @@
-
+ +
-
-
+
  • - +
  • - +
  • -
  • - +
  • +
  • -
  • - +
  • +
  • - +
  • @@ -123,89 +124,83 @@
    -
    +
    - - ! - - + + ! + + ×
    - - -   - - -
    - () + + +
    + ()
    -
    - :  - +
    + : +
    -
    - :  - +
    + : +
    -
    - :  - +
    + : +
    -
    +
    - + - - + - + - + - + - + - + - + - + - + - + - + - + - +
    - -   - - + +
    - +   - () + ()
    %%
    -
    +
    -
    +
    @@ -218,11 +213,11 @@
    -
    -
    -
    • + data-bind="visible: showAttachmentControls() && message().hasAttachments()"> @@ -269,11 +264,11 @@
    -
    +
    -
    +
    @@ -283,5 +278,6 @@
    +
    diff --git a/snappymail/v/0.0.0/themes/SquaresDark/styles.css b/snappymail/v/0.0.0/themes/SquaresDark/styles.css index 772dc0b75..0651d7a2e 100644 --- a/snappymail/v/0.0.0/themes/SquaresDark/styles.css +++ b/snappymail/v/0.0.0/themes/SquaresDark/styles.css @@ -90,7 +90,7 @@ dialog header, dialog footer, .legend, #V-PopupsCompose .b-header .e-identity, -.messageView .messageItem { +.messageView #messageItem { color: var(--main-color); } @@ -101,17 +101,17 @@ dialog header, dialog footer, .g-ui-link, .messageView .messageItemHeader .informationShort a, -.messageView .messageItem .bodyText .b-text-part a { +.messageView #messageItem .bodyText .b-text-part a { color: #6AE; } .messageView .messageItemHeader, -.messageView .messageItem .pgpEncrypted, -.messageView .messageItem .pgpSigned, -.messageView .messageItem .readReceipt, -.messageView .messageItem .showImages, -.messageView .messageItem .attachmentsPlace, -.messageView .messageItem .attachmentsControls { +.messageView #messageItem .pgpEncrypted, +.messageView #messageItem .pgpSigned, +.messageView #messageItem .readReceipt, +.messageView #messageItem .showImages, +.messageView #messageItem .attachmentsPlace, +.messageView #messageItem .attachmentsControls { background-color: #222; border-bottom-color: #444; }