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 @@
-