Moved the message HTML parsing from PHP to JavaScript

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

View file

@ -1,4 +1,6 @@
import { createElement } from 'Common/Globals';
import { createElement, SettingsGet } from 'Common/Globals';
import { forEachObjectEntry, pInt } from 'Common/Utils';
import { proxy } from 'Common/Links';
const
/*
@ -36,20 +38,303 @@ export const
encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
/**
* Clears the Message Html for viewing
* @param {string} text
* @returns {string}
*/
clearHtml = html => {
html = html.replace(/(<pre[^>]*>)([\s\S]*?)(<\/pre>)/gi, aMatches => {
return (aMatches[1] + aMatches[2].trim() + aMatches[3].trim()).replace(/\r?\n/g, '<br>');
});
clearHtml = (html, contentLocationUrls) => {
const debug = false, // Config()->Get('debug', 'enable', false);
useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
result = {
hasExternals: false,
foundCIDs: [],
foundContentLocationUrls: []
},
tpl = document.createElement('template');
tpl.innerHTML = html
.replace(/(<pre[^>]*>)([\s\S]*?)(<\/pre>)/gi, aMatches => {
return (aMatches[1] + aMatches[2].trim() + aMatches[3].trim()).replace(/\r?\n/g, '<br>');
})
// \MailSo\Base\HtmlUtils::ClearComments()
.replace(/<!--[\s\S]*?-->/g, '')
// \MailSo\Base\HtmlUtils::ClearTags()
// eslint-disable-next-line max-len
.replace(/<\/?(link|form|center|base|meta|bgsound|keygen|source|object|embed|applet|mocha|i?frame|frameset|video|audio|area|map)(\s[\s\S]*?)?>/gi, '')
// GetDomFromText
.replace('<o:p></o:p>', '')
.replace('<o:p>', '<span>')
.replace('</o:p>', '</span>')
// https://github.com/the-djmaze/snappymail/issues/187
.replace(/<a[^>]*>(((?!<\/a).)+<a\\s)/gi, '$1')
// \MailSo\Base\HtmlUtils::ClearFastTags
.replace(/<p[^>]*><\/p>/i, '')
.replace(/<!doctype[^>]*>/i, '')
.replace(/<\?xml [^>]*\?>/i, '')
.trim();
html = '';
// convert body attributes to CSS
const tasks = {
link: value => {
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
tpl.content.querySelectorAll('a').forEach(node => node.style.color = value)
}
},
text: (value, node) => node.style.color = value,
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
};
// if (static::Config()->Get('labs', 'strict_html_parser', true))
let allowedAttributes = [
// defaults
'name',
'dir', 'lang', 'style', 'title',
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
'border', 'bordercolor', 'charset', 'direction', 'language',
// a
'coords', 'download', 'hreflang', 'shape',
// body
'alink', 'bgproperties', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
'marginwidth', 'marginheight', 'offset',
// button,
'disabled', 'type', 'value',
// col
'align', 'valign',
// font
'color', 'face', 'size',
// form
'novalidate',
// hr
'noshade',
// img
'hspace', 'sizes', 'srcset', 'vspace', 'usemap',
// input, textarea
'checked', 'max', 'min', 'maxlength', 'multiple', 'pattern', 'placeholder', 'readonly',
'required', 'step', 'wrap',
// label
'for',
// meter
'low', 'high', 'optimum',
// ol
'reversed', 'start',
// option
'selected', 'label',
// table
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
// th
'abbr', 'scope',
// td
'axis', 'colspan', 'rowspan', 'headers', 'nowrap'
];
let disallowedAttributes = [
'id', 'class', 'contenteditable', 'designmode', 'formaction', 'manifest', 'action',
'data-bind', 'data-reactid', 'xmlns', 'srcset',
'fscommand', 'seeksegmenttime'
];
tpl.content.querySelectorAll('*').forEach(oElement => {
const name = oElement.tagName.toUpperCase(),
oStyle = oElement.style,
getAttribute = name => oElement.hasAttribute(name) ? oElement.getAttribute(name).trim() : '';
if (['HEAD','STYLE','SVG','SCRIPT','TITLE','INPUT','BUTTON','TEXTAREA','SELECT'].includes(name)
|| 'none' == oStyle.display
|| 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
// || ('0' === oStyle.opacity
) {
oElement.remove();
return;
}
if ('BODY' === name) {
forEachObjectEntry(tasks, (name, cb) => {
if (oElement.hasAttribute(name)) {
cb(getAttribute(name), oElement);
oElement.removeAttribute(name);
}
});
}
if ('TABLE' === name && oElement.hasAttribute('width')) {
let value = getAttribute('width');
oElement.removeAttribute('width');
oStyle.maxWidth = value + (/^[0-9]+$/.test(value) ? 'px' : '');
oStyle.removeProperty('width');
oStyle.removeProperty('min-width');
}
const aAttrsForRemove = [];
if (oElement.hasAttributes()) {
let i = oElement.attributes.length;
while (i--) {
let sAttrName = oElement.attributes[i].name.toLowerCase();
if (!allowedAttributes.includes(sAttrName)
|| 'on' === sAttrName.slice(0, 2)
|| 'form' === sAttrName.slice(0, 4)
// || 'data-' === sAttrName.slice(0, 5)
// || sAttrName.includes(':')
|| disallowedAttributes.includes(sAttrName))
{
oElement.removeAttribute(sAttrName);
aAttrsForRemove.push(sAttrName);
}
}
}
if (oElement.hasAttribute('href')) {
let sHref = getAttribute('href');
if (!/^([a-z]+):/i.test(sHref) && '//' !== sHref.slice(0, 2)) {
oElement.setAttribute('data-x-broken-href', sHref);
oElement.removeAttribute('href');
}
if ('A' === name) {
oElement.setAttribute('rel', 'external nofollow noopener noreferrer');
}
}
// SVG xlink:href
/*
if (oElement.hasAttribute('xlink:href')) {
oElement.removeAttribute('xlink:href');
}
*/
if ('A' === name) {
oElement.setAttribute('tabindex', '-1');
oElement.setAttribute('target', '_blank');
}
let skipStyle = false;
if (oElement.hasAttribute('src')) {
let sSrc = getAttribute('src');
oElement.removeAttribute('src');
if (detectHiddenImages
&& 'IMG' === name
&& (('' != getAttribute('height') && 2 > pInt(getAttribute('height')))
|| ('' != getAttribute('width') && 2 > pInt(getAttribute('width')))
|| [
'email.microsoftemail.com/open',
'github.com/notifications/beacon/',
'mandrillapp.com/track/open',
'list-manage.com/track/open'
].filter(uri => sSrc.toLowerCase().includes(uri)).length
)) {
skipStyle = true;
oElement.setAttribute('style', 'display:none');
oElement.setAttribute('data-x-hidden-src', sSrc);
}
else if (contentLocationUrls[sSrc])
{
oElement.setAttribute('data-x-src-location', sSrc);
result.foundContentLocationUrls.push(sSrc);
}
else if ('cid:' === sSrc.slice(0, 4))
{
oElement.setAttribute('data-x-src-cid', sSrc.slice(4));
result.foundCIDs.push(sSrc.slice(4));
}
else if (/^https?:\/\//i.test(sSrc) || '//' === sSrc.slice(0, 2))
{
oElement.setAttribute('data-x-src', useProxy ? proxy(sSrc) : sSrc);
result.hasExternals = true;
}
else if ('data:image/' === sSrc.slice(0, 11))
{
oElement.setAttribute('src', sSrc);
}
else
{
oElement.setAttribute('data-x-broken-src', sSrc);
}
}
if (oElement.hasAttribute('background')) {
let sBackground = getAttribute('background');
if (sBackground) {
oStyle.backgroundImage = 'url("' + sBackground + '")';
}
oElement.removeAttribute('background');
}
if (oElement.hasAttribute('bgcolor')) {
let sBackgroundColor = getAttribute('bgcolor');
if (sBackgroundColor) {
oStyle.backgroundColor = sBackgroundColor;
}
oElement.removeAttribute('bgcolor');
}
if (!skipStyle) {
/*
\MailSo\Base\HtmlUtils::ClearHtml(
$sHtml, $bHasExternals, $aFoundCIDs, $aContentLocationUrls, $aFoundContentLocationUrls,
$fAdditionalExternalFilter, !!$this->Config()->Get('labs', 'try_to_detect_hidden_images', false)
);
if ('fixed' === oStyle.position) {
oStyle.position = 'absolute';
}
*/
return html;
oStyle.removeProperty('behavior');
oStyle.removeProperty('cursor');
const urls = {
cid: [], // 'data-x-style-cid'
remote: [], // 'data-x-style-url'
broken: [] // 'data-x-broken-style-src'
};
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
if (oStyle[property]) {
let value = oStyle[property],
found = value.match(/url\s*\(([^)]+)\)/gi);
if (found) {
oStyle[property] = null;
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
let lowerUrl = found.toLowerCase();
if ('cid:' === lowerUrl.slice(0, 4)) {
found = found.slice(4);
urls.cid[property] = found
result.foundCIDs.push(found);
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
result.hasExternals = true;
urls.remote[property] = useProxy ? proxy(found) : found;
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
oStyle[property] = value;
} else {
urls.broken[property] = found;
}
}
}
});
// oStyle.removeProperty('background-image');
// oStyle.removeProperty('list-style-image');
if (urls.cid.length) {
oElement.setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
}
if (urls.remote.length) {
oElement.setAttribute('data-x-style-url', JSON.stringify(urls.remote));
}
if (urls.broken.length) {
oElement.setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
}
}
if (debug && aAttrsForRemove) {
oElement.setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
}
});
// return tpl.content.firstChild;
result.html = tpl.innerHTML;
return result;
},
// Removes background and color

View file

@ -47,6 +47,16 @@ export const
attachmentDownload = (download, customSpecSuffix) =>
serverRequestRaw('Download', download, customSpecSuffix),
proxy = url =>
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
/*
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
'Rnd' => \md5(\microtime(true)),
'Token' => Utils::GetConnectionToken(),
'Url' => $sUrl
)).'/';
*/
/**
* @param {string} type
* @returns {string}

View file

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

View file

@ -32,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
* @returns {*}
*/
findByCid(cid) {
let regex = /^<+|>+$/g, cidc = cid.replace(regex, '');
return this.find(item => cid === item.cid || cidc === item.cid || cidc === item.cid.replace(regex, ''));
cid = cid.replace(/^<+|>+$/g, '');
return this.find(item => cid === item.contentId());
}
}

View file

@ -4,7 +4,7 @@ import { MessagePriority } from 'Common/EnumsUser';
import { i18n } from 'Common/Translator';
import { doc } from 'Common/Globals';
import { encodeHtml, removeColors, plainToHtml } from 'Common/Html';
import { encodeHtml, removeColors, plainToHtml, clearHtml } from 'Common/Html';
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw } from 'Common/Links';
@ -82,6 +82,7 @@ export class MessageModel extends AbstractModel {
isHtml: false,
hasImages: false,
hasExternals: false,
hasAttachments: false,
pgpSigned: null,
pgpEncrypted: null,
@ -121,7 +122,6 @@ export class MessageModel extends AbstractModel {
this.uid = 0;
this.hash = '';
this.requestHash = '';
this.externalProxy = false;
this.emails = [];
this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel;
@ -160,6 +160,7 @@ export class MessageModel extends AbstractModel {
this.isHtml(false);
this.hasImages(false);
this.hasExternals(false);
this.hasAttachments(false);
this.attachments(new AttachmentCollectionModel);
this.pgpSigned(null);
@ -178,6 +179,11 @@ export class MessageModel extends AbstractModel {
this.hasFlaggedSubMessage(false);
}
spamStatus() {
let spam = this.spamResult();
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
}
/**
* @param {Array} properties
* @returns {Array}
@ -393,7 +399,27 @@ export class MessageModel extends AbstractModel {
html = removeColors(html);
}
body.innerHTML = html;
const contentLocationUrls = {},
oAttachments = this.attachments();
oAttachments.forEach(oAttachment => {
if (oAttachment.cid && oAttachment.contentLocation) {
contentLocationUrls[oAttachment.contentId()] = oAttachment.contentLocation;
}
});
let result = clearHtml(html, contentLocationUrls);
this.hasExternals(result.hasExternals);
// this.hasInternals = result.foundCIDs.length || result.foundContentLocationUrls.length;
this.hasImages(body.rlHasImages = !!result.hasExternals);
// Hide valid inline attachments in message view 'attachments' section
oAttachments.forEach(oAttachment => {
oAttachment.isLinked = result.foundCIDs.includes(oAttachment.contentId())
|| result.foundContentLocationUrls.includes(oAttachment.contentLocation)
});
this.hasAttachments(oAttachments.hasVisible());
body.innerHTML = result.html;
body.classList.toggle('html', 1);
body.classList.toggle('plain', 0);
@ -422,12 +448,12 @@ export class MessageModel extends AbstractModel {
el.src = attachment.linkPreview();
}
} else if (data.xStyleCid) {
const name = data.xStyleCidName,
attachment = findAttachmentByCid(data.xStyleCid);
if (attachment && attachment.linkPreview && name) {
el.setAttribute('style', name + ": url('" + attachment.linkPreview() + "');"
+ (el.getAttribute('style') || ''));
}
forEachObjectEntry(JSON.parse(data.xStyleCid), (name, cid) => {
const attachment = findAttachmentByCid(cid);
if (attachment && attachment.linkPreview && name) {
el.style[name] = "url('" + attachment.linkPreview() + "')";
}
});
}
});
@ -453,7 +479,7 @@ export class MessageModel extends AbstractModel {
.replace(email, '$1<a href="mailto:$2">$2</a>');
this.isHtml(false);
this.hasImages(this.hasExternals());
this.hasImages(false);
this.initView();
return true;
}
@ -474,9 +500,6 @@ export class MessageModel extends AbstractModel {
});
}
/**
* @param {boolean=} print = false
*/
viewPopupMessage(print) {
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.ccToLine(false),
@ -503,6 +526,13 @@ export class MessageModel extends AbstractModel {
}
}
/**
* @param {boolean=} print = false
*/
popupMessage() {
this.viewPopupMessage(false);
}
printMessage() {
this.viewPopupMessage(true);
}
@ -539,7 +569,7 @@ export class MessageModel extends AbstractModel {
this.priority(message.priority());
this.hasExternals(message.hasExternals());
this.externalProxy = message.externalProxy;
this.hasAttachments(message.hasAttachments());
this.emails = message.emails;
@ -573,7 +603,7 @@ export class MessageModel extends AbstractModel {
this.hasImages(false);
body.rlHasImages = false;
let attr = this.externalProxy ? 'data-x-additional-src' : 'data-x-src';
let attr = 'data-x-src';
body.querySelectorAll('[' + attr + ']').forEach(node => {
if (node.matches('img')) {
node.loading = 'lazy';
@ -581,11 +611,8 @@ export class MessageModel extends AbstractModel {
node.src = node.getAttribute(attr);
});
attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url';
body.querySelectorAll('[' + attr + ']').forEach(node => {
node.setAttribute('style', ((node.getAttribute('style')||'')
+ ';' + node.getAttribute(attr))
.replace(/^[;\s]+/,''));
body.querySelectorAll('[data-x-style-url]').forEach(node => {
forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
});
}
}

View file

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

View file

@ -174,7 +174,7 @@ html.rl-no-preview-pane {
}
}
.messageItem {
#messageItem {
color: #000;
height: 100%;
@ -532,7 +532,7 @@ html.rl-message-fullscreen {
}
}
html:not(.rl-mobile) .messageItem {
html:not(.rl-mobile) #messageItem {
.buttonFull {
display: none !important;
}

View file

@ -14,7 +14,16 @@ import {
MessageSetAction
} from 'Common/EnumsUser';
import { $htmlCL, leftPanelDisabled, keyScopeReal, moveAction, Settings, getFullscreenElement, exitFullscreen } from 'Common/Globals';
import {
elementById,
$htmlCL,
leftPanelDisabled,
keyScopeReal,
moveAction,
Settings,
getFullscreenElement,
exitFullscreen
} from 'Common/Globals';
import { arrayLength, inFocus } from 'Common/Utils';
import { mailToHelper, showMessageComposer, initFullscreen } from 'Common/UtilsUser';
@ -43,54 +52,58 @@ import { AbstractViewRight } from 'Knoin/AbstractViews';
import { PgpUserStore } from 'Stores/User/Pgp';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
const currentMessage = () => MessageUserStore.message();
import PostalMime from '../../../../vendors/postal-mime/src/postal-mime.js';
import { AttachmentModel } from 'Model/Attachment';
const mimeToMessage = (data, message) => {
// TODO: Check multipart/signed
const headers = data.split(/\r?\n\r?\n/)[0];
if (/Content-Type:.+; boundary=/.test(headers)) {
// https://github.com/postalsys/postal-mime
(new PostalMime).parse(data).then(result => {
let html = result.html,
regex = /^<+|>+$/g;
result.attachments.forEach(data => {
let attachment = new AttachmentModel;
attachment.mimeType = data.mimeType;
attachment.fileName = data.filename;
attachment.content = data.content; // ArrayBuffer
attachment.cid = data.contentId || '';
// Parse inline attachments from result.attachments
if (attachment.cid) {
if (html) {
let cid = 'cid:' + attachment.cid.replace(regex, ''),
b64 = 'data:' + data.mimeType + ';base64,' + btoa(String.fromCharCode(...new Uint8Array(data.content)));
html = html
.replace('src="' + cid + '"', 'src="' + b64 + '"')
.replace("src='" + cid + "'", "src='" + b64 + "'");
const
oMessageScrollerDom = () => elementById('messageItem') || {},
currentMessage = () => MessageUserStore.message(),
mimeToMessage = (data, message) => {
// TODO: Check multipart/signed
const headers = data.split(/\r?\n\r?\n/)[0];
if (/Content-Type:.+; boundary=/.test(headers)) {
// https://github.com/postalsys/postal-mime
(new PostalMime).parse(data).then(result => {
let html = result.html,
regex = /^<+|>+$/g;
result.attachments.forEach(data => {
let attachment = new AttachmentModel;
attachment.mimeType = data.mimeType;
attachment.fileName = data.filename;
attachment.content = data.content; // ArrayBuffer
attachment.cid = data.contentId || '';
// Parse inline attachments from result.attachments
if (attachment.cid) {
if (html) {
let cid = 'cid:' + attachment.cid.replace(regex, ''),
b64 = 'data:' + data.mimeType + ';base64,' + btoa(String.fromCharCode(...new Uint8Array(data.content)));
html = html
.replace('src="' + cid + '"', 'src="' + b64 + '"')
.replace("src='" + cid + "'", "src='" + b64 + "'");
}
}
}
// data.disposition
// data.related = true;
message.attachments.push(attachment);
message.attachments.push(attachment);
});
message.hasAttachments(message.attachments.hasVisible());
// result.headers;
// TODO: strip script tags and all other security that PHP also does
message.plain(result.text || '');
if (html) {
message.html(html.replace(/<\/?script[\s\S]*?>/gi, '') || '');
message.viewHtml();
} else {
message.viewPlain();
}
});
// result.headers;
// TODO: strip script tags and all other security that PHP also does
message.plain(result.text || '');
if (html) {
message.html(html.replace(/<\/?script[\s\S]*?>/gi, '') || '');
message.viewHtml();
} else {
message.viewPlain();
}
});
return;
}
message.plain(data);
message.viewPlain();
};
return;
}
message.plain(data);
message.viewPlain();
};
export class MailMessageView extends AbstractViewRight {
constructor() {
@ -112,14 +125,17 @@ export class MailMessageView extends AbstractViewRight {
}
}, this.messageVisibility);
this.oMessageScrollerDom = null;
this.addObservables({
showAttachmentControls: false,
downloadAsZipLoading: false,
lastReplyAction_: '',
showFullInfo: '1' === Local.get(ClientSideKeyName.MessageHeaderFullInfo),
moreDropdownTrigger: false
moreDropdownTrigger: false,
// viewer
viewFromShort: '',
viewFromDkimData: ['none', ''],
viewToShort: ''
});
this.moveAction = moveAction;
@ -161,32 +177,7 @@ export class MailMessageView extends AbstractViewRight {
this.notSpamCommand = createCommandActionHelper(FolderType.NotSpam, true);
// viewer
this.viewFolder = '';
this.viewUid = '';
this.viewHash = '';
this.addObservables({
viewSubject: '',
viewFromShort: '',
viewFromDkimData: ['none', ''],
viewToShort: '',
viewFrom: '',
viewTo: '',
viewCc: '',
viewBcc: '',
viewReplyTo: '',
viewTimeStamp: 0,
viewSize: '',
viewSpamScore: 0,
viewSpamStatus: '',
viewLineAsCss: '',
viewViewLink: '',
viewUnsubscribeLink: '',
viewDownloadLink: '',
viewIsImportant: false,
viewIsFlagged: false,
hasVirus: null
});
this.addComputables({
allowAttachmentControls: () => this.attachmentsActions.length && Settings.capa(Capa.AttachmentsActions),
@ -228,9 +219,6 @@ export class MailMessageView extends AbstractViewRight {
return '';
},
pgpSigned: () => currentMessage() && !!currentMessage().pgpSigned(),
pgpEncrypted: () => currentMessage()
&& !!(currentMessage().pgpEncrypted() || currentMessage().isPgpEncrypted()),
pgpSupported: () => currentMessage() && PgpUserStore.isSupported(),
messageListOrViewLoading:
@ -258,36 +246,13 @@ export class MailMessageView extends AbstractViewRight {
this.scrollMessageToTop();
}
let spam = message.spamResult();
this.viewFolder = message.folder;
this.viewUid = message.uid;
this.viewHash = message.hash;
this.viewSubject(message.subject());
this.viewFromShort(message.fromToLine(true, true));
this.viewFromDkimData(message.fromDkimData());
this.viewToShort(message.toToLine(true, true));
this.viewFrom(message.fromToLine());
this.viewTo(message.toToLine());
this.viewCc(message.ccToLine());
this.viewBcc(message.bccToLine());
this.viewReplyTo(message.replyToToLine());
this.viewTimeStamp(message.dateTimeStampInUTC());
this.viewSize(message.friendlySize());
this.viewSpamScore(message.spamScore());
this.viewSpamStatus(spam ? i18n(message.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '');
this.viewLineAsCss(message.lineAsCss());
this.viewViewLink(message.viewLink());
this.viewUnsubscribeLink(message.getFirstUnsubsribeLink());
this.viewDownloadLink(message.downloadLink());
this.viewIsImportant(message.isImportant());
this.viewIsFlagged(message.isFlagged());
this.hasVirus(message.hasVirus());
} else {
MessageUserStore.selectorMessageSelected(null);
this.viewFolder = '';
this.viewUid = '';
this.viewHash = '';
this.scrollMessageToTop();
@ -303,11 +268,6 @@ export class MailMessageView extends AbstractViewRight {
}
});
MessageUserStore.messageViewTrigger.subscribe(() => {
const message = currentMessage();
this.viewIsFlagged(message ? message.isFlagged() : false);
});
this.lastReplyAction(Local.get(ClientSideKeyName.LastReplyAction) || ComposeType.Reply);
addEventListener('mailbox.message-view.toggle-full-screen', () => this.toggleFullScreen());
@ -357,8 +317,6 @@ export class MailMessageView extends AbstractViewRight {
}
onBuild(dom) {
this.oMessageScrollerDom = dom.querySelector('.messageItem');
this.fullScreenMode.subscribe(value =>
value && currentMessage() && AppUserStore.focusedState(Scope.MessageView));
@ -533,7 +491,7 @@ export class MailMessageView extends AbstractViewRight {
// change focused state
shortcuts.add('arrowleft', '', Scope.MessageView, () => {
if (!this.fullScreenMode() && currentMessage() && SettingsUserStore.usePreviewPane()
&& !this.oMessageScrollerDom.scrollLeft) {
&& !oMessageScrollerDom().scrollLeft) {
AppUserStore.focusedState(Scope.MessageList);
return false;
}
@ -606,11 +564,11 @@ export class MailMessageView extends AbstractViewRight {
}
scrollMessageToTop() {
this.oMessageScrollerDom.scrollTop = (50 < this.oMessageScrollerDom.scrollTop) ? 50 : 0;
oMessageScrollerDom().scrollTop = (50 < oMessageScrollerDom().scrollTop) ? 50 : 0;
}
scrollMessageToLeft() {
this.oMessageScrollerDom.scrollLeft = 0;
oMessageScrollerDom().scrollLeft = 0;
}
downloadAsZip() {
@ -637,7 +595,7 @@ export class MailMessageView extends AbstractViewRight {
* @returns {void}
*/
showImages() {
currentMessage() && currentMessage().showExternalImages();
currentMessage().showExternalImages();
}
/**
@ -654,7 +612,7 @@ export class MailMessageView extends AbstractViewRight {
*/
readReceipt() {
let oMessage = currentMessage()
if (oMessage && oMessage.readReceipt()) {
if (oMessage.readReceipt()) {
Remote.request('SendReadReceiptMessage', null, {
MessageFolder: oMessage.folder,
MessageUid: oMessage.uid,
@ -672,41 +630,39 @@ export class MailMessageView extends AbstractViewRight {
}
}
pgpDecrypt(self) {
const message = self.message();
message && PgpUserStore.decrypt(message).then(result => {
pgpDecrypt() {
const oMessage = currentMessage();
PgpUserStore.decrypt(oMessage).then(result => {
if (result && result.data) {
mimeToMessage(result.data, message);
mimeToMessage(result.data, oMessage);
}
});
}
pgpVerify(self) {
if (self.pgpSigned()) {
const message = self.message(),
sender = message && message.from[0].email;
PgpUserStore.hasPublicKeyForEmails([message.from[0].email]).then(mode => {
pgpVerify() {
const oMessage = currentMessage();
if (oMessage.pgpSigned()) {
const sender = oMessage.from[0].email;
PgpUserStore.hasPublicKeyForEmails([oMessage.from[0].email]).then(mode => {
if ('gnupg' === mode) {
let params = message.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
if (params) {
params.Folder = message.folder;
params.Uid = message.uid;
rl.app.Remote.post('MessagePgpVerify', null, params)
.then(data => {
// TODO
console.dir(data);
})
.catch(error => {
// TODO
console.dir(error);
});
}
let params = oMessage.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
params.Folder = oMessage.folder;
params.Uid = oMessage.uid;
rl.app.Remote.post('MessagePgpVerify', null, params)
.then(data => {
// TODO
console.dir(data);
})
.catch(error => {
// TODO
console.dir(error);
});
} else if ('openpgp' === mode) {
const publicKey = OpenPGPUserStore.getPublicKeyFor(sender);
OpenPGPUserStore.verify(message.plain(), null/*detachedSignature*/, publicKey).then(result => {
OpenPGPUserStore.verify(oMessage.plain(), null/*detachedSignature*/, publicKey).then(result => {
if (result) {
message.plain(result.data);
message.viewPlain();
oMessage.plain(result.data);
oMessage.viewPlain();
console.dir({signatures:result.signatures});
}
/*
@ -714,7 +670,7 @@ export class MailMessageView extends AbstractViewRight {
i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
});
message.getText()
oMessage.getText()
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds