Moved the message HTML parsing from PHP to JavaScript

Now we can properly parse PGP/MIME HTML messages
This commit is contained in:
the-djmaze 2022-02-02 13:02:48 +01:00
parent 3615405aac
commit e265a0f1c1
13 changed files with 563 additions and 837 deletions

View file

@ -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(/(<pre[^>]*>)([\s\S]*?)(<\/pre>)/gi, aMatches => {
return (aMatches[1] + aMatches[2].trim() + aMatches[3].trim()).replace(/\r?\n/g, '<br>');
});
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(/(<pre[^>]*>)([\s\S]*?)(<\/pre>)/gi, aMatches => {
return (aMatches[1] + aMatches[2].trim() + aMatches[3].trim()).replace(/\r?\n/g, '<br>');
})
// \MailSo\Base\HtmlUtils::ClearComments()
.replace(/<!--[\s\S]*?-->/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('<o:p></o:p>', '')
.replace('<o:p>', '<span>')
.replace('</o:p>', '</span>')
// https://github.com/the-djmaze/snappymail/issues/187
.replace(/<a[^>]*>(((?!<\/a).)+<a\\s)/gi, '$1')
// \MailSo\Base\HtmlUtils::ClearFastTags
.replace(/<p[^>]*><\/p>/i, '')
.replace(/<!doctype[^>]*>/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

View file

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

View file

@ -49,6 +49,10 @@ export class AttachmentModel extends AbstractModel {
return attachment;
}
contentId() {
return this.cid.replace(/^<+|>+$/g, '');
}
/**
* @returns {boolean}
*/

View file

@ -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());
}
}

View file

@ -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<a href="mailto:$2">$2</a>');
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 + "')");
});
}
}

View file

@ -374,10 +374,6 @@ export const MessageUserStore = new class {
+ (message.isPgpEncrypted() ? ' openpgp-encrypted' : '')
+ '">'
+ '</div>');
body.rlHasImages = !!json.HasExternals;
message.hasImages(body.rlHasImages);
message.body = body;
if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
message.viewPlain();

View file

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

View file

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

View file

@ -66,16 +66,40 @@ abstract class HtmlUtils
$sText = \tidy_repair_string($sText, $tidyConfig, 'utf8');
}
$sHtmlAttrs = $sBodyAttrs = '';
$sText = \preg_replace(array(
'/<p[^>]*><\/p>/i',
'/<!doctype[^>]*>/i',
'/<\?xml [^>]*\?>/i'
), '', $sText);
$sText = static::ClearFastTags($sText);
$sHtmlAttrs = $sBodyAttrs = '';
$sText = static::ClearBodyAndHtmlTag($sText, $sHtmlAttrs, $sBodyAttrs);
$aMatch = array();
if (\preg_match('/<html([^>]+)>/im', $sText, $aMatch) && !empty($aMatch[1])) {
$sHtmlAttrs = $aMatch[1];
}
$aMatch = array();
if (\preg_match('/<body([^>]+)>/im', $sText, $aMatch) && !empty($aMatch[1])) {
$sBodyAttrs = $aMatch[1];
}
// $sText = \preg_replace('/^.*<body([^>]*)>/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('<?xml version="1.0" encoding="utf-8"?>'.
'<html '.$sHtmlAttrs.'><head>'.
'<html '.\trim($sHtmlAttrs).'><head>'.
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>'.
'<body '.$sBodyAttrs.'>'.\MailSo\Base\Utils::Utf8Clear($sText).'</body></html>');
'<body '.\trim($sBodyAttrs).'>'.\MailSo\Base\Utils::Utf8Clear($sText).'</body></html>');
$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('/<html([^>]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1]))
{
$sHtmlAttrs = $aMatch[1];
}
$aMatch = array();
if (\preg_match('/<body([^>]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1]))
{
$sBodyAttrs = $aMatch[1];
}
// $sHtml = \preg_replace('/^.*<body([^>]*)>/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[^>]*><\/p>/i',
'/<!doctype[^>]*>/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()
*/

View file

@ -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('/(<pre[^>]*>)([\s\S\r\n\t]*?)(<\/pre>)/mi', function ($aMatches) {
return \preg_replace('/[\r\n]+/', '<br />', $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);
}

View file

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

View file

@ -36,7 +36,8 @@
<div class="b-message-view-desc" data-i18n="MESSAGE/MESSAGE_VIEW_DESC" data-bind="visible: !message() && '' === messageError() && !hasCheckedMessages()">
</div>
<div class="b-message" data-bind="visible: message">
<!-- ko if: message -->
<div class="b-message" data-bind="i18nUpdate: message">
<div class="message-fixed-button-toolbar">
<div class="btn-group" style="margin-right: -8px">
<a class="btn btn-thin btn-transparent buttonReply fontastic"
@ -84,34 +85,34 @@
<li role="presentation">
<a href="#" tabindex="-1" data-bind="command: deleteWithoutMoveCommand" data-icon="🗑" data-i18n="MESSAGE_LIST/BUTTON_DELETE_WITHOUT_MOVE"></a>
</li>
<li class="dividerbar" role="presentation" data-bind="visible: message() && message().hasUnsubsribeLinks()">
<a target="_blank" href="#" tabindex="-1" data-bind="attr: { href: viewUnsubscribeLink }" data-icon="✖" data-i18n="MESSAGE/BUTTON_UNSUBSCRIBE"></a>
<li class="dividerbar" role="presentation" data-bind="visible: message().hasUnsubsribeLinks()">
<a target="_blank" href="#" tabindex="-1" data-bind="attr: { href: message().getFirstUnsubsribeLink() }" data-icon="✖" data-i18n="MESSAGE/BUTTON_UNSUBSCRIBE"></a>
</li>
</div>
<div data-bind="visible: allowMessageActions" class="dividerbar">
<div data-bind="visible: allowMessageActions, with: message" class="dividerbar">
<li role="presentation">
<a href="#" tabindex="-1" data-bind="click: function () { message() && message().printMessage(); } " data-icon="🖨" data-i18n="MESSAGE/MENU_PRINT"></a>
<a href="#" tabindex="-1" data-bind="click: printMessage" data-icon="🖨" data-i18n="MESSAGE/MENU_PRINT"></a>
</li>
<li role="presentation">
<a href="#" tabindex="-1" data-bind="click: function () { message() && message().viewPopupMessage(); }">
<a href="#" tabindex="-1" data-bind="click: popupMessage">
<i class="icon-popup"></i>
<span data-i18n="MESSAGE/BUTTON_IN_NEW_WINDOW"></span>
</a>
</li>
<li role="presentation" data-bind="visible: message() && message().html() && !message().isHtml()">
<a href="#" tabindex="-1" data-bind="click: function () { message().viewHtml(); }" data-icon="👁" data-i18n="MESSAGE/HTML_VIEW"></a>
<li role="presentation" data-bind="visible: html() && !isHtml()">
<a href="#" tabindex="-1" data-bind="click: viewHtml" data-icon="👁" data-i18n="MESSAGE/HTML_VIEW"></a>
</li>
<li role="presentation" data-bind="visible: message() && message().plain() && message().isHtml()">
<a href="#" tabindex="-1" data-bind="click: function () { message().viewPlain(); }" data-icon="👁" data-i18n="MESSAGE/PLAIN_VIEW"></a>
<li role="presentation" data-bind="visible: plain() && isHtml()">
<a href="#" tabindex="-1" data-bind="click: viewPlain" data-icon="👁" data-i18n="MESSAGE/PLAIN_VIEW"></a>
</li>
<li class="dividerbar" role="presentation">
<a target="_blank" href="#" tabindex="-1" data-bind="attr: { href: viewViewLink }">
<a target="_blank" href="#" tabindex="-1" data-bind="attr: { href: viewLink() }">
<i class="icon-file-code"></i>
<span data-i18n="MESSAGE/MENU_VIEW_ORIGINAL"></span>
</a>
</li>
<li role="presentation">
<a target="_blank" href="#" tabindex="-1" data-bind="attr: { href: viewDownloadLink }" data-icon="📥" data-i18n="MESSAGE/MENU_DOWNLOAD_ORIGINAL"></a>
<a target="_blank" href="#" tabindex="-1" data-bind="attr: { href: downloadLink() }" data-icon="📥" data-i18n="MESSAGE/MENU_DOWNLOAD_ORIGINAL"></a>
</li>
</div>
</ul>
@ -123,89 +124,83 @@
</div>
</div>
<div class="messageItemHeader" data-bind="css: {'emptySubject': '' === viewSubject()}">
<div class="messageItemHeader" data-bind="css: {'emptySubject': '' === message().subject()}">
<div class="subjectParent">
<span class="infoParent g-ui-user-select-none fontastic" data-bind="click: function() { showFullInfo(!showFullInfo()); }"></span>
<span class="flagParent g-ui-user-select-none flagOff fontastic" data-bind="text: viewIsFlagged() ? '★' : '☆', css: {'flagOn': viewIsFlagged, 'flagOff': !viewIsFlagged()}"></span>
<b style="color: red; margin-right: 5px" data-bind="visible: viewIsImportant">!</b>
<span class="subject" data-bind="hidden: !viewSubject(), text: viewSubject, title: viewSubject, event: { 'dblclick': toggleFullScreen }"></span>
<span class="emptySubjectText" data-i18n="MESSAGE/EMPTY_SUBJECT_TEXT" data-bind="hidden: viewSubject(), event: { 'dblclick': toggleFullScreen }"></span>
<span class="flagParent g-ui-user-select-none flagOff fontastic" data-bind="text: message().isFlagged() ? '★' : '☆', css: {'flagOn': message().isFlagged(), 'flagOff': !message().isFlagged()}"></span>
<b style="color: red; margin-right: 5px" data-bind="visible: message().isImportant()">!</b>
<span class="subject" data-bind="hidden: !message().subject(), text: message().subject, title: message().subject, event: { 'dblclick': toggleFullScreen }"></span>
<span class="emptySubjectText" data-i18n="MESSAGE/EMPTY_SUBJECT_TEXT" data-bind="hidden: message().subject(), event: { 'dblclick': toggleFullScreen }"></span>
<a href="#" class="close" data-bind="command: closeMessageCommand" style="margin-top: -8px;">×</a>
</div>
<div data-bind="hidden: showFullInfo(), event: { 'dblclick': toggleFullScreen }">
<div class="informationShort">
<span class="from" data-bind="html: viewFromShort, title: viewFrom"></span>
<span data-bind="visible: viewFromDkimVisibility">
&nbsp;
<i data-bind="css: viewFromDkimStatusIconClass, title: viewFromDkimStatusTitle"></i>
</span>
<div data-bind="visible: 0 < viewTimeStamp()">
(<time class="date" data-moment-format="FULL" data-bind="moment: viewTimeStamp"></time>)
<span class="from" data-bind="html: viewFromShort, title: message().fromToLine()"></span>
<i data-bind="visible: viewFromDkimVisibility, css: viewFromDkimStatusIconClass, title: viewFromDkimStatusTitle"></i>
<div data-bind="visible: 0 < message().dateTimeStampInUTC()">
(<time class="date" data-moment-format="FULL" data-bind="moment: message().dateTimeStampInUTC()"></time>)
</div>
</div>
<div class="informationShortWrp">
<div class="informationShort" data-bind="visible: viewTo">
<span data-i18n="GLOBAL/TO"></span>:&nbsp;
<span data-bind="text: viewTo"></span>
<div class="informationShort" data-bind="visible: message().toToLine()">
<span data-i18n="GLOBAL/TO"></span>:
<span data-bind="text: message().toToLine()"></span>
</div>
<div class="informationShort" data-bind="visible: viewCc">
<span data-i18n="GLOBAL/CC"></span>:&nbsp;
<span data-bind="text: viewCc"></span>
<div class="informationShort" data-bind="visible: message().ccToLine()">
<span data-i18n="GLOBAL/CC"></span>:
<span data-bind="text: message().ccToLine()"></span>
</div>
</div>
<div class="informationShort" data-bind="visible: viewSpamStatus()">
<span data-i18n="MESSAGE/SPAM_SCORE"></span>:&nbsp;
<meter min="0" max="100" optimum="0" low="33" high="66" data-bind="value: viewSpamScore, title: viewSpamStatus"></meter>
<div class="informationShort" data-bind="visible: message().spamResult()">
<span data-i18n="MESSAGE/SPAM_SCORE"></span>:
<meter min="0" max="100" optimum="0" low="33" high="66" data-bind="value: message().spamScore(), title: message().spamStatus()"></meter>
</div>
</div>
<div class="informationFull" data-bind="visible: showFullInfo()">
<div class="informationFull" data-bind="visible: showFullInfo(), with: message">
<table>
<tr data-bind="visible: '' !== viewFrom()">
<tr data-bind="visible: fromToLine()">
<td data-i18n="GLOBAL/FROM"></td>
<td><span data-bind="text: viewFrom, title: viewFrom"></span>
<span data-bind="visible: viewFromDkimVisibility">
&nbsp;
<i data-bind="css: viewFromDkimStatusIconClass, title: viewFromDkimStatusTitle"></i>
</span>
<td><span data-bind="text: fromToLine(), title: fromToLine()"></span>
<i data-bind="visible: $parent.viewFromDkimVisibility, css: $parent.viewFromDkimStatusIconClass, title: $parent.viewFromDkimStatusTitle"></i>
</td>
</tr>
<tr data-bind="visible: '' !== viewTo()">
<tr data-bind="visible: toToLine()">
<td data-i18n="GLOBAL/TO"></td>
<td data-bind="text: viewTo, title: viewTo"></td>
<td data-bind="text: toToLine(), title: toToLine()"></td>
</tr>
<tr data-bind="visible: '' !== viewCc()">
<tr data-bind="visible: ccToLine()">
<td data-i18n="GLOBAL/CC"></td>
<td data-bind="text: viewCc, title: viewCc"></td>
<td data-bind="text: ccToLine(), title: ccToLine()"></td>
</tr>
<tr data-bind="visible: '' !== viewBcc()">
<tr data-bind="visible: bccToLine()">
<td data-i18n="GLOBAL/BCC"></td>
<td data-bind="text: viewBcc, title: viewBcc"></td>
<td data-bind="text: bccToLine(), title: bccToLine()"></td>
</tr>
<tr data-bind="visible: '' !== viewReplyTo()">
<tr data-bind="visible: replyToToLine()">
<td data-i18n="GLOBAL/REPLY_TO"></td>
<td data-bind="text: viewReplyTo, title: viewReplyTo"></td>
<td data-bind="text: replyToToLine(), title: replyToToLine()"></td>
</tr>
<tr data-bind="visible: 0 < viewTimeStamp()">
<tr data-bind="visible: dateTimeStampInUTC">
<td data-i18n="MESSAGE/LABEL_DATE"></td>
<td>
<time data-moment-format="FULL" data-bind="moment: viewTimeStamp"></time>
<time data-moment-format="FULL" data-bind="moment: dateTimeStampInUTC()"></time>
&nbsp;
(<time data-moment-format="FROMNOW" data-bind="moment: viewTimeStamp"></time>)
(<time data-moment-format="FROMNOW" data-bind="moment: dateTimeStampInUTC()"></time>)
</td>
</tr>
<tr data-bind="visible: 0 < viewSpamScore()">
<tr data-bind="visible: 0 < spamScore()">
<td data-i18n="MESSAGE/SPAM_SCORE"></td>
<td><span data-bind="text: viewSpamScore, title: viewSpamStatus"></span>%</td>
<td><span data-bind="text: spamScore(), title: spamStatus()"></span>%</td>
</tr>
<tr data-bind="visible: '' !== viewFrom()">
<tr data-bind="visible: friendlySize()">
<td data-i18n="POPUPS_FILTER/SELECT_FIELD_SIZE"></td>
<td class="size" data-bind="text: viewSize"></td>
<td class="size" data-bind="text: friendlySize()"></td>
</tr>
</table>
</div>
<div class="hasVirus" data-bind="visible: hasVirus()" data-i18n="MESSAGE/HAS_VIRUS_WARNING"></div>
<div class="hasVirus" data-bind="visible: message().hasVirus()" data-i18n="MESSAGE/HAS_VIRUS_WARNING"></div>
</div>
<div class="messageItem" data-bind="css: viewLineAsCss()">
<div id="messageItem" data-bind="css: message().lineAsCss()">
<div tabindex="0" data-bind="hasfocus: messageDomFocused">
<span class="buttonFull" data-bind="click: toggleFullScreen">
@ -218,11 +213,11 @@
<div data-bind="visible: !messageLoadingThrottle()">
<div class="bodySubHeader">
<div class="showImages" data-bind="visible: message() && message().hasImages(), click: showImages"
<div class="showImages" data-bind="visible: message().hasImages(), click: showImages"
data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></div>
<div class="readReceipt" data-bind="visible: message() && !isDraftOrSentFolder() && '' !== message().readReceipt() && !message().isReadReceipt(), click: readReceipt"
<div class="readReceipt" data-bind="visible: !isDraftOrSentFolder() && '' !== message().readReceipt() && !message().isReadReceipt(), click: readReceipt"
data-icon="✉" data-i18n="MESSAGE/BUTTON_NOTIFY_READ_RECEIPT"></div>
<div class="attachmentsPlace" data-bind="visible: message() && message().attachments().hasVisible(),
<div class="attachmentsPlace" data-bind="visible: message().hasAttachments(),
css: {'selection-mode' : showAttachmentControls, 'unselectedAttachmentsError': highlightUnselectedAttachments}">
<ul class="attachmentList" data-bind="foreach: message() ? message().attachments() : []">
<li class="attachmentItem" draggable="true"
@ -254,7 +249,7 @@
</div>
<div class="attachmentsControls"
data-bind="visible: showAttachmentControls() && message() && message().attachments().hasVisible()">
data-bind="visible: showAttachmentControls() && message().hasAttachments()">
<span data-bind="visible: downloadAsZipAllowed">
<i class="fontastic iconcolor-red" data-bind="visible: downloadAsZipError"></i>
@ -269,11 +264,11 @@
</div>
</div>
<div class="openpgp-control encrypted" data-bind="visible: pgpEncrypted">
<div class="openpgp-control encrypted" data-bind="visible: message().pgpEncrypted() || message().isPgpEncrypted()">
<span data-icon="🔒" data-i18n="MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC"></span>
<button class="btn" data-bind="visible: pgpSupported, click: pgpDecrypt" data-i18n="OPENPGP/BUTTON_DECRYPT"></button>
</div>
<div class="openpgp-control signed" data-bind="visible: pgpSigned">
<div class="openpgp-control signed" data-bind="visible: message().pgpSigned()">
<span data-icon="✍" data-i18n="MESSAGE/PGP_SIGNED_MESSAGE_DESC"></span>
<button class="btn" data-bind="visible: pgpSupported, click: pgpVerify" data-i18n="MESSAGE/BUTTON_PGP_VERIFY"></button>
</div>
@ -283,5 +278,6 @@
</div>
</div>
</div>
<!-- /ko -->
</div>
</div>

View file

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