mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-12-29 11:01:34 +08:00
Moved the message HTML parsing from PHP to JavaScript
Now we can properly parse PGP/MIME HTML messages
This commit is contained in:
parent
3615405aac
commit
e265a0f1c1
13 changed files with 563 additions and 837 deletions
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -49,6 +49,10 @@ export class AttachmentModel extends AbstractModel {
|
|||
return attachment;
|
||||
}
|
||||
|
||||
contentId() {
|
||||
return this.cid.replace(/^<+|>+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + "')");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
||||
<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>:
|
||||
<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>:
|
||||
<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>:
|
||||
<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">
|
||||
|
||||
<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>
|
||||
|
||||
(<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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue