From 3fc5216841168f7dec350805000a5de84fc88eb4 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 4 Feb 2022 13:40:59 +0100 Subject: [PATCH] Improved Composer handling with PGP messages --- dev/Common/Html.js | 81 +++++++++++-------------- dev/External/SquireUI.js | 4 +- dev/Model/Message.js | 11 +--- dev/Stores/User/Pgp.js | 9 ++- dev/View/Popup/Compose.js | 123 +++++++++++++++++--------------------- 5 files changed, 103 insertions(+), 125 deletions(-) diff --git a/dev/Common/Html.js b/dev/Common/Html.js index 4cbe5b29a..0ecd2092f 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -376,60 +376,57 @@ export const ); }, - convertDivs = (...args) => { - let divText = 1 < args.length ? args[1].trim() : ''; - if (divText.length) { - divText = '\n' + divText.replace(/]*>([\s\S\r\n]*)<\/div>/gim, convertDivs).trim() + '\n'; - } - - return divText; - }, - convertPre = (...args) => 1 < args.length - ? args[1] - .toString() - .replace(/[\n]/gm, '
') - .replace(/[\r]/gm, '') + ? args[1].toString().replace(/\n/g, '
') : '', + fixAttibuteValue = (...args) => (1 < args.length ? args[1] + encodeHtml(args[2]) : ''), convertLinks = (...args) => (1 < args.length ? args[1].trim() : ''); + html = html + .replace(/\r?\n/, '') + .replace(/]*>([\s\S]*?)<\/pre>/gim, convertPre) + .replace(/\s+/gm, ' '); + + while (/<(div|tr)[\s>]/i.test(html)) { + html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n'); + } + while (/<\/(div|tr)[\s>]/i.test(html)) { + html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n'); + } + + while (/<(ul|ol|p|h\d)[\s>]/i.test(html)) { + html = html.replace(/\n*<(ul|ol|p|h\d)(\s[\s\S]*?)?>\n*/gi, '\n\n'); + } + while (/<\/(ul|ol|p|h\d)[\s>]/i.test(html)) { + html = html.replace(/\n*<\/(ul|ol|p|h\d)(\s[\s\S]*?)?>\n*/gi, '\n\n'); + } + tpl.innerHTML = html - .replace(/]*><\/p>/gi, '') - .replace(/]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre) - .replace(/[\s]+/gm, ' ') .replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue) .replace(/]*>/gim, '\n') - .replace(/<\/h[\d]>/gi, '\n') - .replace(/<\/p>/gi, '\n\n') - .replace(/]*>/gim, '\n') - .replace(/<\/ul>/gi, '\n') .replace(/]*>/gim, ' * ') .replace(/<\/li>/gi, '\n') - .replace(/<\/td>/gi, '\n') - .replace(/<\/tr>/gi, '\n') - .replace(/]*>/gim, '\n_______________________________\n\n') - .replace(/]*>([\s\S\r\n]*)<\/div>/gim, convertDivs) + .replace(/\n*/gi, '\t') + .replace(/<\/t[dh](\s[\s\S]*?)?>/gi, '\n') + .replace(/\n*\n*/gi, '\n\n' + '⎯'.repeat(64) + '\n\n') .replace(/]*>/gim, '\n__bq__start__\n') .replace(/<\/blockquote>/gim, '\n__bq__end__\n') - .replace(/]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks) - .replace(/<\/div>/gi, '\n') + .replace(/]*>([\s\S]*?)<\/a>/gim, convertLinks) .replace(/ /gi, ' ') .replace(/"/gi, '"') - .replace(/<[^>]*>/gm, ''); + .replace(//gi, '\n') + .replace(/<[\s\S]+?>/g, ''); text = tpl.content.textContent; if (text) { text = text - .replace(/\n[ \t]+/gm, '\n') - .replace(/[\n]{3,}/gm, '\n\n') + .replace(/\n{3,}/gm, '\n\n') .replace(/>/gi, '>') .replace(/</gi, '<') - .replace(/&/gi, '&') - // wordwrap max line length 100 - .match(/.{1,100}(\s|$)|\S+?(\s|$)/g).join('\n'); + .replace(/&/gi, '&'); } while (0 < --limit) { @@ -506,9 +503,9 @@ export const .replace(/&/g, '&') .replace(/>/g, '>') .replace(/') - .replace(/[\s]*~~~\/blockquote~~~/g, '') - .replace(/\n/g, '
'); + .replace(/~~~blockquote~~~\s*/g, '
') + .replace(/\s*~~~\/blockquote~~~/g, '
') + .replace(/\n/g, '
'); }; export class HtmlEditor { @@ -600,31 +597,25 @@ export class HtmlEditor { * @param {boolean=} wrapIsHtml = false * @returns {string} */ - getData(wrapIsHtml = false) { + getData() { let result = ''; if (this.editor) { try { if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) { result = this.editor.__plain.getRawData(); } else { - result = wrapIsHtml - ? '
' + - this.editor.getData() + - '
' - : this.editor.getData(); + result = this.editor.getData(); } } catch (e) {} // eslint-disable-line no-empty } - return result; } /** - * @param {boolean=} wrapIsHtml = false * @returns {string} */ - getDataWithHtmlMark(wrapIsHtml = false) { - return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml); + getDataWithHtmlMark() { + return (this.isHtml() ? ':HTML:' : '') + this.getData(); } modeWysiwyg() { diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js index 2c474f670..8e6cc1f83 100644 --- a/dev/External/SquireUI.js +++ b/dev/External/SquireUI.js @@ -35,7 +35,7 @@ const addLinks: true // allow_smart_html_links */ sanitizeToDOMFragment: (html, isPaste/*, squire*/) => { - tpl.innerHTML = html + tpl.innerHTML = (html||'') .replace(/<\/?(BODY|HTML)[^>]*>/gi,'') .replace(//g,'') .replace(/]*>\s*<\/span>/gi,'') @@ -104,7 +104,7 @@ const } if (!skipInsert) { - signature = (isHtml ? '

' : "\n\n") + signature + (isHtml ? '' : ''); + signature = isHtml ? `

${signature}

` : `\n\n${signature}\n\n`; text = insertBefore ? signature + text : text + signature; diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 455a0bf34..a91690d58 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -616,23 +616,16 @@ export class MessageModel extends AbstractModel { bodyAsHTML() { // if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) { if (this.body && !this.body.querySelector('iframe')) { - let clone = this.body.cloneNode(true), - attr = 'data-html-editor-font-wrapper'; + let clone = this.body.cloneNode(true); clone.querySelectorAll('blockquote.rl-bq-switcher').forEach( node => node.classList.remove('rl-bq-switcher','hidden-bq') ); clone.querySelectorAll('.rlBlockquoteSwitcher').forEach( node => node.remove() ); - clone.querySelectorAll('['+attr+']').forEach( - node => node.removeAttribute(attr) - ); return clone.innerHTML; } - if (this.isPgpEncrypted()) { - return this.html() || plainToHtml(this.plain()); - } - return ''; + return this.html() || plainToHtml(this.plain()); } /** diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js index a43756177..0d4489e4d 100644 --- a/dev/Stores/User/Pgp.js +++ b/dev/Stores/User/Pgp.js @@ -67,6 +67,13 @@ export const PgpUserStore = new class { return !!(OpenPGPUserStore.isSupported() || GnuPGUserStore.isSupported() || window.mailvelope); } + /** + * @returns {boolean} + */ + isEncrypted(text) { + return 0 === text.trim().indexOf('-----BEGIN PGP MESSAGE-----'); + } + async mailvelopeHasPublicKeyForEmails(recipients, all) { const keyring = this.mailvelopeKeyring, @@ -129,7 +136,7 @@ export const PgpUserStore = new class { const sender = message.from[0].email, armoredText = message.plain(); - if (!armoredText.includes('-----BEGIN PGP MESSAGE-----')) { + if (!this.isEncrypted(armoredText)) { return; } diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index bcfa1cbca..69b8d749c 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -364,7 +364,7 @@ class ComposePopupView extends AbstractViewPopup { sign = !draft && this.pgpSign() && this.canPgpSign(), encrypt = this.pgpEncrypt() && this.canPgpEncrypt(), TextIsHtml = this.oEditor.isHtml(), - Text = this.oEditor.getData(true); + Text = this.oEditor.getData(); if (TextIsHtml) { let l; do { @@ -708,36 +708,27 @@ class ComposePopupView extends AbstractViewPopup { } } - convertSignature(signature) { - let fromLine = this.oLastMessage ? this.emailArrayToStringLineHelper(this.oLastMessage.from, true) : ''; - if (fromLine) { - signature = signature.replace(/{{FROM-FULL}}/g, fromLine); - - if (!fromLine.includes(' ') && 0 < fromLine.indexOf('@')) { - fromLine = fromLine.replace(/@\S+/, ''); - } - - signature = signature.replace(/{{FROM}}/g, fromLine); - } - - return signature - .replace(/\r/g, '') - .replace(/\s{1,2}?{{FROM}}/g, '') - .replace(/\s{1,2}?{{FROM-FULL}}/g, '') - .replace(/{{DATE}}/g, new Date().format('LLLL')) - .replace(/{{TIME}}/g, new Date().format('LT')) - .replace(/{{MOMENT:[^}]+}}/g, ''); - } - setSignatureFromIdentity(identity) { if (identity) { this.editor(editor => { - let signature = identity.signature(), - isHtml = signature && ':HTML:' === signature.slice(0, 6); - - editor.setSignature( - this.convertSignature(isHtml ? signature.slice(6) : signature), - isHtml, !!identity.signatureInsertBefore()); + let signature = identity.signature() || '', + isHtml = ':HTML:' === signature.slice(0, 6), + fromLine = this.oLastMessage ? this.emailArrayToStringLineHelper(this.oLastMessage.from, true) : ''; + if (fromLine) { + signature = signature.replace(/{{FROM-FULL}}/g, fromLine); + if (!fromLine.includes(' ') && 0 < fromLine.indexOf('@')) { + fromLine = fromLine.replace(/@\S+/, ''); + } + signature = signature.replace(/{{FROM}}/g, fromLine); + } + signature = (isHtml ? signature.slice(6) : signature) + .replace(/\r/g, '') + .replace(/\s{1,2}?{{FROM}}/g, '') + .replace(/\s{1,2}?{{FROM-FULL}}/g, '') + .replace(/{{DATE}}/g, new Date().format('LLLL')) + .replace(/{{TIME}}/g, new Date().format('LT')) + .replace(/{{MOMENT:[^}]+}}/g, ''); + editor.setSignature(signature, isHtml, !!identity.signatureInsertBefore()); }); } } @@ -834,7 +825,6 @@ class ComposePopupView extends AbstractViewPopup { sDate = '', sSubject = '', sText = '', - sReplyTitle = '', identity = null, aDraftInfo = null, message = null; @@ -882,7 +872,6 @@ class ComposePopupView extends AbstractViewPopup { sDate = timestampToString(message.dateTimeStampInUTC(), 'FULL'); sSubject = message.subject(); aDraftInfo = message.aDraftInfo; - sText = message.bodyAsHTML(); let resplyAllParts = null; switch (lineComposeType) { @@ -960,65 +949,63 @@ class ComposePopupView extends AbstractViewPopup { // no default } + sText = message.bodyAsHTML(); + let encrypted; + switch (lineComposeType) { case ComposeType.Reply: case ComposeType.ReplyAll: sFrom = message.fromToLine(false, true); - sReplyTitle = i18n('COMPOSE/REPLY_MESSAGE_TITLE', { - DATETIME: sDate, - EMAIL: sFrom - }); - - sText = sText.replace(/]+>/g, '').replace(/]+><\/a>/g, '').trim(); - sText = '

' + sReplyTitle + ':

' + sText + '
'; - + sText = '

' + i18n('COMPOSE/REPLY_MESSAGE_TITLE', { DATETIME: sDate, EMAIL: sFrom }) + + ':

' + + sText.replace(/]+>/g, '').replace(/]+><\/a>/g, '').trim() + + '
'; break; case ComposeType.Forward: sFrom = message.fromToLine(false, true); sTo = message.toToLine(false, true); sCc = message.ccToLine(false, true); - sText = - '

' + - i18n('COMPOSE/FORWARD_MESSAGE_TOP_TITLE') + - '
' + - i18n('GLOBAL/FROM') + - ': ' + - sFrom + - '
' + - i18n('GLOBAL/TO') + - ': ' + - sTo + - (sCc.length ? '
' + i18n('GLOBAL/CC') + ': ' + sCc : '') + - '
' + - i18n('COMPOSE/FORWARD_MESSAGE_TOP_SENT') + - ': ' + - encodeHtml(sDate) + - '
' + - i18n('GLOBAL/SUBJECT') + - ': ' + - encodeHtml(sSubject) + - '

' + - sText.trim() + - '

'; + sText = '

' + i18n('COMPOSE/FORWARD_MESSAGE_TOP_TITLE') + '

' + + i18n('GLOBAL/FROM') + ': ' + sFrom + + '
' + + i18n('GLOBAL/TO') + ': ' + sTo + + (sCc.length ? '
' + i18n('GLOBAL/CC') + ': ' + sCc : '') + + '
' + + i18n('COMPOSE/FORWARD_MESSAGE_TOP_SENT') + + ': ' + + encodeHtml(sDate) + + '
' + + i18n('GLOBAL/SUBJECT') + + ': ' + + encodeHtml(sSubject) + + '

' + + sText.trim() + + '
'; break; case ComposeType.ForwardAsAttachment: sText = ''; break; - // no default + default: + encrypted = PgpUserStore.isEncrypted(sText); + if (encrypted) { + sText = message.plain(); + } } this.editor(editor => { - editor.setHtml(sText); + encrypted || editor.setHtml(sText); - if ( - EditorDefaultType.PlainForced === SettingsUserStore.editorDefaultType() || - (!message.isHtml() && EditorDefaultType.HtmlForced !== SettingsUserStore.editorDefaultType()) + if (encrypted + || EditorDefaultType.PlainForced === SettingsUserStore.editorDefaultType() + || (!message.isHtml() && EditorDefaultType.HtmlForced !== SettingsUserStore.editorDefaultType()) ) { editor.modePlain(); } + !encrypted || editor.setPlain(sText); + if (identity && ComposeType.Draft !== lineComposeType && ComposeType.EditAsNew !== lineComposeType) { this.setSignatureFromIdentity(identity); } @@ -1507,8 +1494,8 @@ class ComposePopupView extends AbstractViewPopup { * The iframe will be injected into the container identified by selector. * https://mailvelope.github.io/mailvelope/Editor.html */ - let text = this.oEditor.getData(true), - encrypted = text.includes('-----BEGIN PGP MESSAGE-----'), + let text = this.oEditor.getData(), + encrypted = PgpUserStore.isEncrypted(text), size = SettingsGet('PhpUploadSizes')['post_max_size'], quota = pInt(size); switch (size.slice(-1)) {