mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-30 20:49:20 +03: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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue