From da92e8bf615b100f254563107c4ce9f64fc72f68 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 17 Dec 2020 00:16:00 +0200 Subject: [PATCH 01/17] Extract inline images into attachments for plain rich text messages --- .../formatters/plain-mail-msg-formatter.ts | 45 +++++++++++++++++-- extension/js/common/core/mime.ts | 28 ++++++------ 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts index 544f08861f4..304bf230cd5 100644 --- a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts @@ -2,10 +2,15 @@ 'use strict'; +import * as DOMPurify from 'dompurify'; + +import { Attachment } from '../../../../js/common/core/attachment.js'; import { BaseMailFormatter } from './base-mail-formatter.js'; +import { Buf } from '../../../../js/common/core/buf.js'; +import { Dict } from '../../../../js/common/core/common.js'; import { NewMsgData, SendBtnTexts } from '../compose-types.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; -import { SendableMsgBody } from '../../../../js/common/core/mime.js'; +import { SendableMsgBody, Mime } from '../../../../js/common/core/mime.js'; export class PlainMsgMailFormatter extends BaseMailFormatter { @@ -14,9 +19,43 @@ export class PlainMsgMailFormatter extends BaseMailFormatter { const attachments = this.isDraft ? [] : await this.view.attsModule.attach.collectAtts(); const body: SendableMsgBody = { 'text/plain': newMsg.plaintext }; if (this.richtext) { - body['text/html'] = newMsg.plainhtml; + const { htmlWithInlineImages, imgAttachments } = this.extractInlineImagesToAttachments(newMsg.plainhtml); + attachments.push(...imgAttachments); + body['text/html'] = htmlWithInlineImages; } - return await SendableMsg.createPlain(this.acctEmail, this.headers(newMsg), body, attachments); + return SendableMsg.createPlain(this.acctEmail, this.headers(newMsg), body, attachments); + } + + public extractInlineImagesToAttachments = (html: string) => { + const imgAttachments: Attachment[] = []; + DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (!node) { + return; + } + if ('src' in node) { + const img: Element = node; + const src = img.getAttribute('src') as string; + const { mimeType, data } = this.parseInlineImageSrc(src); + const imgAttachment = new Attachment({ name: img.getAttribute('name') || '', type: mimeType, data: Buf.fromBase64Str(data), inline: true }); + const imgAttNode = Mime.createAttNode(imgAttachment); + const imgAttachmentId: string = imgAttNode._headers.find((header: Dict) => header.key === 'X-Attachment-Id').value; + console.log('imgAttachmentId', imgAttachmentId); + img.setAttribute('src', `cid:${imgAttachmentId}`); + imgAttachments.push(imgAttachment); + } + }); + const htmlWithInlineImages = DOMPurify.sanitize(html); + DOMPurify.removeAllHooks(); + return { htmlWithInlineImages, imgAttachments }; } + private parseInlineImageSrc = (src: string) => { + let mimeType; + let data = ''; + const matches = src.match(/data:(image\/\w+);base64,(.*)/); + if (matches) { + [, mimeType, data] = matches; + } + return { mimeType, data }; + } } diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index bd4f7bb5642..ba3b13001ee 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -271,6 +271,20 @@ export class Mime { return pgpMimeSigned; } + public static createAttNode = (attachment: Attachment): any => { // todo: MimeBuilder types + const type = `${attachment.type}; name="${attachment.name}"`; + const id = `f_${Str.sloppyRandom(30)}@flowcrypt`; + const header: Dict = {}; + if (attachment.contentDescription) { + header['Content-Description'] = attachment.contentDescription; + } + header['Content-Disposition'] = attachment.inline ? 'inline' : 'attachment'; + header['X-Attachment-Id'] = id; + header['Content-ID'] = `<${id}>`; + header['Content-Transfer-Encoding'] = 'base64'; + return new MimeBuilder(type, { filename: attachment.name }).setHeader(header).setContent(attachment.getData()); // tslint:disable-line:no-unsafe-any + } + private static headerGetAddress = (parsedMimeMsg: MimeContent, headersNames: Array) => { const result: { to: string[], cc: string[], bcc: string[] } = { to: [], cc: [], bcc: [] }; let from: string | undefined; @@ -310,20 +324,6 @@ export class Mime { return undefined; } - private static createAttNode = (attachment: Attachment): any => { // todo: MimeBuilder types - const type = `${attachment.type}; name="${attachment.name}"`; - const id = `f_${Str.sloppyRandom(30)}@flowcrypt`; - const header: Dict = {}; - if (attachment.contentDescription) { - header['Content-Description'] = attachment.contentDescription; - } - header['Content-Disposition'] = attachment.inline ? 'inline' : 'attachment'; - header['X-Attachment-Id'] = id; - header['Content-ID'] = `<${id}>`; - header['Content-Transfer-Encoding'] = 'base64'; - return new MimeBuilder(type, { filename: attachment.name }).setHeader(header).setContent(attachment.getData()); // tslint:disable-line:no-unsafe-any - } - private static getNodeType = (node: MimeParserNode, type: 'value' | 'initial' = 'value') => { if (node.headers['content-type'] && node.headers['content-type'][0]) { return node.headers['content-type'][0][type]; From fb314cbd4d36d82bff88ab7d1c470598a76ee26f Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 17 Dec 2020 02:09:26 +0200 Subject: [PATCH 02/17] Move extractInlineImagesToAttachments() method from PlainMsgMailFormatter to Mime --- .../compose-send-btn-popover-module.ts | 2 +- .../formatters/plain-mail-msg-formatter.ts | 44 +------------------ extension/js/common/core/mime.ts | 40 ++++++++++++++++- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts index c3d75429d4a..4979fc7bf46 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts @@ -29,7 +29,7 @@ export class ComposeSendBtnPopoverModule extends ViewModule { for (const key of Object.keys(popoverItems)) { const popoverOpt = key as PopoverOpt; if (popoverOpt === 'richtext' && !this.view.debug && Catch.browser().name !== 'firefox') { - continue; // richtext not deployed to Chrome yet, for now only allow firefox (and also in automated tests which set debug===true) + // continue; // richtext not deployed to Chrome yet, for now only allow firefox (and also in automated tests which set debug===true) } const item = popoverItems[popoverOpt]; const elem = $(` diff --git a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts index 304bf230cd5..56f87369a4d 100644 --- a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts @@ -2,15 +2,10 @@ 'use strict'; -import * as DOMPurify from 'dompurify'; - -import { Attachment } from '../../../../js/common/core/attachment.js'; import { BaseMailFormatter } from './base-mail-formatter.js'; -import { Buf } from '../../../../js/common/core/buf.js'; -import { Dict } from '../../../../js/common/core/common.js'; import { NewMsgData, SendBtnTexts } from '../compose-types.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; -import { SendableMsgBody, Mime } from '../../../../js/common/core/mime.js'; +import { SendableMsgBody } from '../../../../js/common/core/mime.js'; export class PlainMsgMailFormatter extends BaseMailFormatter { @@ -19,43 +14,8 @@ export class PlainMsgMailFormatter extends BaseMailFormatter { const attachments = this.isDraft ? [] : await this.view.attsModule.attach.collectAtts(); const body: SendableMsgBody = { 'text/plain': newMsg.plaintext }; if (this.richtext) { - const { htmlWithInlineImages, imgAttachments } = this.extractInlineImagesToAttachments(newMsg.plainhtml); - attachments.push(...imgAttachments); - body['text/html'] = htmlWithInlineImages; + body['text/html'] = newMsg.plainhtml; } return SendableMsg.createPlain(this.acctEmail, this.headers(newMsg), body, attachments); } - - public extractInlineImagesToAttachments = (html: string) => { - const imgAttachments: Attachment[] = []; - DOMPurify.addHook('afterSanitizeAttributes', (node) => { - if (!node) { - return; - } - if ('src' in node) { - const img: Element = node; - const src = img.getAttribute('src') as string; - const { mimeType, data } = this.parseInlineImageSrc(src); - const imgAttachment = new Attachment({ name: img.getAttribute('name') || '', type: mimeType, data: Buf.fromBase64Str(data), inline: true }); - const imgAttNode = Mime.createAttNode(imgAttachment); - const imgAttachmentId: string = imgAttNode._headers.find((header: Dict) => header.key === 'X-Attachment-Id').value; - console.log('imgAttachmentId', imgAttachmentId); - img.setAttribute('src', `cid:${imgAttachmentId}`); - imgAttachments.push(imgAttachment); - } - }); - const htmlWithInlineImages = DOMPurify.sanitize(html); - DOMPurify.removeAllHooks(); - return { htmlWithInlineImages, imgAttachments }; - } - - private parseInlineImageSrc = (src: string) => { - let mimeType; - let data = ''; - const matches = src.match(/data:(image\/\w+);base64,(.*)/); - if (matches) { - [, mimeType, data] = matches; - } - return { mimeType, data }; - } } diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index ba3b13001ee..747b72c8c42 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -2,6 +2,8 @@ 'use strict'; +import * as DOMPurify from 'dompurify'; + import { Dict, Str } from './common.js'; import { requireIso88592, requireMimeBuilder, requireMimeParser } from '../platform/require.js'; @@ -208,7 +210,11 @@ export class Mime { } else { contentNode = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any for (const type of Object.keys(body)) { - contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!.toString())); // already present, that's why part of for loop + let content = body[type]!.toString(); + if (type === 'text/html') { + content = Mime.extractInlineImagesToAttachments(content, rootNode); + } + contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, content)); // already present, that's why part of for loop } } rootNode.appendChild(contentNode); // tslint:disable-line:no-unsafe-any @@ -403,4 +409,36 @@ export class Mime { return node; } + private static extractInlineImagesToAttachments = (html: string, rootNode: any) => { + const imgAttNodes: any[] = []; + DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (!node) { + return; + } + if ('src' in node) { + const img: Element = node; + const src = img.getAttribute('src') as string; + const { mimeType, data } = Mime.parseInlineImageSrc(src); + const imgAttachment = new Attachment({ name: img.getAttribute('name') || '', type: mimeType, data: Buf.fromBase64Str(data), inline: true }); + const imgAttNode = Mime.createAttNode(imgAttachment); + rootNode.appendChild(imgAttNode); // tslint:disable-line:no-unsafe-any + const imgAttachmentId: string = imgAttNode._headers.find((header: Dict) => header.key === 'X-Attachment-Id').value; + img.setAttribute('src', `cid:${imgAttachmentId}`); + imgAttNodes.push(imgAttNode); + } + }); + const htmlWithInlineImages = DOMPurify.sanitize(html); + DOMPurify.removeAllHooks(); + return htmlWithInlineImages; + } + + private static parseInlineImageSrc = (src: string) => { + let mimeType; + let data = ''; + const matches = src.match(/data:(image\/\w+);base64,(.*)/); + if (matches) { + [, mimeType, data] = matches; + } + return { mimeType, data }; + } } From a8534c67f044d4c0e9337d23fe1307e690e4327d Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 17 Dec 2020 02:16:09 +0200 Subject: [PATCH 03/17] clean up --- .../compose-send-btn-popover-module.ts | 2 +- .../formatters/plain-mail-msg-formatter.ts | 2 +- extension/js/common/core/mime.ts | 28 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts index 4979fc7bf46..c3d75429d4a 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-popover-module.ts @@ -29,7 +29,7 @@ export class ComposeSendBtnPopoverModule extends ViewModule { for (const key of Object.keys(popoverItems)) { const popoverOpt = key as PopoverOpt; if (popoverOpt === 'richtext' && !this.view.debug && Catch.browser().name !== 'firefox') { - // continue; // richtext not deployed to Chrome yet, for now only allow firefox (and also in automated tests which set debug===true) + continue; // richtext not deployed to Chrome yet, for now only allow firefox (and also in automated tests which set debug===true) } const item = popoverItems[popoverOpt]; const elem = $(` diff --git a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts index 56f87369a4d..e8f6337f918 100644 --- a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts @@ -16,6 +16,6 @@ export class PlainMsgMailFormatter extends BaseMailFormatter { if (this.richtext) { body['text/html'] = newMsg.plainhtml; } - return SendableMsg.createPlain(this.acctEmail, this.headers(newMsg), body, attachments); + return await SendableMsg.createPlain(this.acctEmail, this.headers(newMsg), body, attachments); } } diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 747b72c8c42..cb6422646e3 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -277,20 +277,6 @@ export class Mime { return pgpMimeSigned; } - public static createAttNode = (attachment: Attachment): any => { // todo: MimeBuilder types - const type = `${attachment.type}; name="${attachment.name}"`; - const id = `f_${Str.sloppyRandom(30)}@flowcrypt`; - const header: Dict = {}; - if (attachment.contentDescription) { - header['Content-Description'] = attachment.contentDescription; - } - header['Content-Disposition'] = attachment.inline ? 'inline' : 'attachment'; - header['X-Attachment-Id'] = id; - header['Content-ID'] = `<${id}>`; - header['Content-Transfer-Encoding'] = 'base64'; - return new MimeBuilder(type, { filename: attachment.name }).setHeader(header).setContent(attachment.getData()); // tslint:disable-line:no-unsafe-any - } - private static headerGetAddress = (parsedMimeMsg: MimeContent, headersNames: Array) => { const result: { to: string[], cc: string[], bcc: string[] } = { to: [], cc: [], bcc: [] }; let from: string | undefined; @@ -330,6 +316,20 @@ export class Mime { return undefined; } + private static createAttNode = (attachment: Attachment): any => { // todo: MimeBuilder types + const type = `${attachment.type}; name="${attachment.name}"`; + const id = `f_${Str.sloppyRandom(30)}@flowcrypt`; + const header: Dict = {}; + if (attachment.contentDescription) { + header['Content-Description'] = attachment.contentDescription; + } + header['Content-Disposition'] = attachment.inline ? 'inline' : 'attachment'; + header['X-Attachment-Id'] = id; + header['Content-ID'] = `<${id}>`; + header['Content-Transfer-Encoding'] = 'base64'; + return new MimeBuilder(type, { filename: attachment.name }).setHeader(header).setContent(attachment.getData()); // tslint:disable-line:no-unsafe-any + } + private static getNodeType = (node: MimeParserNode, type: 'value' | 'initial' = 'value') => { if (node.headers['content-type'] && node.headers['content-type'][0]) { return node.headers['content-type'][0][type]; From 2a505ec2abe98666899e2ea255d60735732f1bd2 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 17 Dec 2020 12:03:46 +0200 Subject: [PATCH 04/17] use MimeNode.getHeader() --- .../compose-modules/formatters/plain-mail-msg-formatter.ts | 1 + extension/js/common/core/mime.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts index e8f6337f918..544f08861f4 100644 --- a/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/plain-mail-msg-formatter.ts @@ -18,4 +18,5 @@ export class PlainMsgMailFormatter extends BaseMailFormatter { } return await SendableMsg.createPlain(this.acctEmail, this.headers(newMsg), body, attachments); } + } diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index cb6422646e3..2b970a7cbb8 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -422,7 +422,7 @@ export class Mime { const imgAttachment = new Attachment({ name: img.getAttribute('name') || '', type: mimeType, data: Buf.fromBase64Str(data), inline: true }); const imgAttNode = Mime.createAttNode(imgAttachment); rootNode.appendChild(imgAttNode); // tslint:disable-line:no-unsafe-any - const imgAttachmentId: string = imgAttNode._headers.find((header: Dict) => header.key === 'X-Attachment-Id').value; + const imgAttachmentId = imgAttNode.getHeader('X-Attachment-Id'); // tslint:disable-line:no-unsafe-any img.setAttribute('src', `cid:${imgAttachmentId}`); imgAttNodes.push(imgAttNode); } From 2dd1e0bce9cdee9700a51f67e4d9546ad310577f Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 17 Dec 2020 15:35:37 +0200 Subject: [PATCH 05/17] fix displayImageSrcLinkAsImg(), do not show a.type.indexOf('image/') === 0 && a.cid === `<${contentId}>`)[0]; if (content) { - img.src = `data:${a.type};base64,${content.getData().toBase64Str()}`; + img.src = `data:${content.type};base64,${content.getData().toBase64Str()}`; Xss.replaceElementDANGEROUSLY(a, img.outerHTML); // xss-safe-value - img.outerHTML was built using dom node api } else { Xss.replaceElementDANGEROUSLY(a, Xss.escape(`[broken link: ${a.href}]`)); // xss-escaped diff --git a/extension/js/common/platform/xss.ts b/extension/js/common/platform/xss.ts index e88271c761a..11d93ccb6c8 100644 --- a/extension/js/common/platform/xss.ts +++ b/extension/js/common/platform/xss.ts @@ -98,14 +98,14 @@ export class Xss { a.href = src; a.className = 'image_src_link'; a.target = '_blank'; - a.innerText = (title || 'show image') + (src.startsWith('data:image/') ? '' : ' (remote)'); + a.innerText = (title || 'show image') + (src.startsWith('data:image/') || src.startsWith('cid:') ? '' : ' (remote)'); const heightWidth = `height: ${img.clientHeight ? `${Number(img.clientHeight)}px` : 'auto'}; width: ${img.clientWidth ? `${Number(img.clientWidth)}px` : 'auto'};max-width:98%;`; a.setAttribute('style', `text-decoration: none; background: #FAFAFA; padding: 4px; border: 1px dotted #CACACA; display: inline-block; ${heightWidth}`); Xss.replaceElementDANGEROUSLY(img, a.outerHTML); // xss-safe-value - "a" was build using dom node api } } if ('target' in node) { // open links in new window - (node as Element).setAttribute('target','_blank'); + (node as Element).setAttribute('target', '_blank'); // prevents https://www.owasp.org/index.php/Reverse_Tabnabbing (node as Element).setAttribute('rel', 'noopener noreferrer'); } From f68b85f42d4064391a4664f2af2c0949777ab6b9 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Sat, 19 Dec 2020 02:55:57 +0200 Subject: [PATCH 06/17] extract inline images only when sending plain rich-text messages --- .../js/common/api/email-provider/sendable-msg.ts | 2 +- extension/js/common/core/mime.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index d9464874d6c..1452ee4a94e 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -144,7 +144,7 @@ export class SendableMsg { if (this.body['encrypted/buf']) { this.body = { 'text/plain': this.body['encrypted/buf'].toString() }; } - return await Mime.encode(this.body, this.headers, this.attachments, this.type); + return await Mime.encode(this.body, this.headers, this.attachments, this.type, true); } } diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 2b970a7cbb8..dada5ab5cec 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -197,7 +197,13 @@ export class Mime { }); } - public static encode = async (body: SendableMsgBody, headers: RichHeaders, attachments: Attachment[] = [], type?: MimeEncodeType): Promise => { + public static encode = async ( + body: SendableMsgBody, + headers: RichHeaders, + attachments: Attachment[] = [], + type?: MimeEncodeType, + extractInlineImagesToAttachments?: boolean + ): Promise => { const rootContentType = type !== 'pgpMimeEncrypted' ? 'multipart/mixed' : `multipart/encrypted; protocol="application/pgp-encrypted";`; const rootNode = new MimeBuilder(rootContentType, { includeBccInHeader: true }); // tslint:disable-line:no-unsafe-any for (const key of Object.keys(headers)) { @@ -210,11 +216,11 @@ export class Mime { } else { contentNode = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any for (const type of Object.keys(body)) { - let content = body[type]!.toString(); - if (type === 'text/html') { + let content = body[type]!.toString(); // already present, that's why part of for loop + if (extractInlineImagesToAttachments && type === 'text/html') { content = Mime.extractInlineImagesToAttachments(content, rootNode); } - contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, content)); // already present, that's why part of for loop + contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, content)); } } rootNode.appendChild(contentNode); // tslint:disable-line:no-unsafe-any From f896c368fbf8f32bcf6c98f14c9c47a5ded73966 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Sat, 19 Dec 2020 03:08:47 +0200 Subject: [PATCH 07/17] revert unnecessary addition --- extension/js/common/platform/xss.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/platform/xss.ts b/extension/js/common/platform/xss.ts index 11d93ccb6c8..2a4332e2db5 100644 --- a/extension/js/common/platform/xss.ts +++ b/extension/js/common/platform/xss.ts @@ -98,7 +98,7 @@ export class Xss { a.href = src; a.className = 'image_src_link'; a.target = '_blank'; - a.innerText = (title || 'show image') + (src.startsWith('data:image/') || src.startsWith('cid:') ? '' : ' (remote)'); + a.innerText = (title || 'show image') + (src.startsWith('data:image/') ? '' : ' (remote)'); const heightWidth = `height: ${img.clientHeight ? `${Number(img.clientHeight)}px` : 'auto'}; width: ${img.clientWidth ? `${Number(img.clientWidth)}px` : 'auto'};max-width:98%;`; a.setAttribute('style', `text-decoration: none; background: #FAFAFA; padding: 4px; border: 1px dotted #CACACA; display: inline-block; ${heightWidth}`); Xss.replaceElementDANGEROUSLY(img, a.outerHTML); // xss-safe-value - "a" was build using dom node api From 6e3bdbd4a7bd4f8bcf1a94dd93c36ded6a03f85c Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Sat, 19 Dec 2020 03:26:58 +0200 Subject: [PATCH 08/17] get rid of the expensive regex --- extension/js/common/core/mime.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index dada5ab5cec..2a3f1dd1a09 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -441,9 +441,10 @@ export class Mime { private static parseInlineImageSrc = (src: string) => { let mimeType; let data = ''; - const matches = src.match(/data:(image\/\w+);base64,(.*)/); - if (matches) { - [, mimeType, data] = matches; + const parts = src.split(/[:;,]/); + if (parts.length === 4 && parts[0] === 'data' && parts[1].match(/^image\/\w+/) && parts[2] === 'base64') { + mimeType = parts[1]; + data = parts[3]; } return { mimeType, data }; } From 247521b56b117206a1c3b8dc270efc61ac4ab135 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Sat, 19 Dec 2020 04:01:09 +0200 Subject: [PATCH 09/17] add test --- test/source/tests/compose.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 634cadf89c1..3df17631e39 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -713,6 +713,10 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await sendImgAndVerifyPresentInSentMsg(t, browser, 'sign'); })); + ava.default('compose - sending and rendering plain message with image ', testWithBrowser('compatibility', async (t, browser) => { + await sendImgAndVerifyPresentInSentMsg(t, browser, 'plain'); + })); + ava.default('compose - sending and rendering message with U+10000 code points', testWithBrowser('compatibility', async (t, browser) => { const rainbow = '\ud83c\udf08'; await sendTextAndVerifyPresentInSentMsg(t, browser, rainbow, { sign: true, encrypt: false }); @@ -1030,10 +1034,11 @@ const pastePublicKeyManually = async (composeFrame: ControllableFrame, inboxPage await inboxPage.waitTillGone('@dialog-add-pubkey'); }; -const sendImgAndVerifyPresentInSentMsg = async (t: AvaContext, browser: BrowserHandle, sendingType: 'encrypt' | 'sign') => { +const sendImgAndVerifyPresentInSentMsg = async (t: AvaContext, browser: BrowserHandle, sendingType: 'encrypt' | 'sign' | 'plain') => { // send a message with image in it const imgBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAnElEQVR42u3RAQ0AAAgDIE1u9FvDOahAVzLFGS1ECEKEIEQIQoQgRIgQIQgRghAhCBGCECEIQYgQhAhBiBCECEEIQoQgRAhChCBECEIQIgQhQhAiBCFCEIIQIQgRghAhCBGCEIQIQYgQhAhBiBCEIEQIQoQgRAhChCAEIUIQIgQhQhAiBCEIEYIQIQgRghAhCBEiRAhChCBECEK+W3uw+TnWoJc/AAAAAElFTkSuQmCC'; - const subject = `Test Sending ${sendingType === 'sign' ? 'Signed' : 'Encrypted'} Message With Image ${Util.lousyRandom()}`; + const sendingTypeForHumans = sendingType === 'encrypt' ? 'Encrypted' : (sendingType === 'sign' ? 'Signed' : 'Plain'); + const subject = `Test Sending ${sendingTypeForHumans} Message With Image ${Util.lousyRandom()}`; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, subject, { richtext: true, sign: sendingType === 'sign', encrypt: sendingType === 'encrypt' }); // the following is a temporary hack - currently not able to directly paste an image with puppeteer @@ -1042,6 +1047,10 @@ const sendImgAndVerifyPresentInSentMsg = async (t: AvaContext, browser: BrowserH await ComposePageRecipe.sendAndClose(composePage); // get sent msg id from mock const sentMsg = new GoogleData('flowcrypt.compatibility@gmail.com').getMessageBySubject(subject)!; + if (sendingType === 'plain') { + expect(sentMsg.payload?.body?.data).to.match(/This is an automated puppeteer test: Test Sending Plain Message With Image/); + return; + } let url = `chrome/dev/ci_pgp_host_page.htm?frameId=none&msgId=${encodeURIComponent(sentMsg.id)}&senderEmail=flowcrypt.compatibility%40gmail.com&isOutgoing=___cu_false___&acctEmail=flowcrypt.compatibility%40gmail.com`; if (sendingType === 'sign') { url += '&signature=___cu_true___'; From 01790f64c6ae050f71e3588ff25b378b3388e75b Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Sun, 20 Dec 2020 00:31:47 +0200 Subject: [PATCH 10/17] take parsedMail.html into consideration in storeSentMessage --- test/source/mock/google/google-data.ts | 3 ++- test/source/tests/compose.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/source/mock/google/google-data.ts b/test/source/mock/google/google-data.ts index dafa247ee68..545bd56e42d 100644 --- a/test/source/mock/google/google-data.ts +++ b/test/source/mock/google/google-data.ts @@ -186,7 +186,8 @@ export class GoogleData { } let body: GmailMsg$payload$body; if (parsedMail.text) { - body = { data: parsedMail.text, size: parsedMail.text.length }; + const data = parsedMail.html || parsedMail.text; + body = { data, size: data.length }; } else if (bodyContentAtt) { body = { attachmentId: bodyContentAtt.id, size: bodyContentAtt.size }; } else { diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 3df17631e39..cc8d712cf28 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -705,15 +705,15 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te expect(await composePage.readHtml('@input-body')).to.include('
مرحبا
'); })); - ava.default('compose - sending and rendering encrypted message with image ', testWithBrowser('compatibility', async (t, browser) => { + ava.default('compose - sending and rendering encrypted message with image', testWithBrowser('compatibility', async (t, browser) => { await sendImgAndVerifyPresentInSentMsg(t, browser, 'encrypt'); })); - ava.default('compose - sending and rendering signed message with image ', testWithBrowser('compatibility', async (t, browser) => { + ava.default('compose - sending and rendering signed message with image', testWithBrowser('compatibility', async (t, browser) => { await sendImgAndVerifyPresentInSentMsg(t, browser, 'sign'); })); - ava.default('compose - sending and rendering plain message with image ', testWithBrowser('compatibility', async (t, browser) => { + ava.default('compose - sending and rendering plain message with image', testWithBrowser('compatibility', async (t, browser) => { await sendImgAndVerifyPresentInSentMsg(t, browser, 'plain'); })); From e0176f8a0bd852783f69f57a300070b883068d21 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Sun, 20 Dec 2020 02:41:50 +0200 Subject: [PATCH 11/17] improve if --- test/source/mock/google/google-data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/source/mock/google/google-data.ts b/test/source/mock/google/google-data.ts index 545bd56e42d..a4f3d814bcf 100644 --- a/test/source/mock/google/google-data.ts +++ b/test/source/mock/google/google-data.ts @@ -185,9 +185,9 @@ export class GoogleData { } } let body: GmailMsg$payload$body; - if (parsedMail.text) { - const data = parsedMail.html || parsedMail.text; - body = { data, size: data.length }; + const htmlOrText = parsedMail.html || parsedMail.text; + if (htmlOrText) { + body = { data: htmlOrText, size: htmlOrText.length }; } else if (bodyContentAtt) { body = { attachmentId: bodyContentAtt.id, size: bodyContentAtt.size }; } else { From 501193d91da6894b0433bed2bb4ac12410916058 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Wed, 23 Dec 2020 19:45:27 +0200 Subject: [PATCH 12/17] move extractInlineImagesToAttachments() to ComposeSendBtnModule --- .../compose-send-btn-module.ts | 41 +++++++++++++++ .../common/api/email-provider/sendable-msg.ts | 2 +- extension/js/common/core/attachment.ts | 5 ++ extension/js/common/core/mime.ts | 52 ++----------------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts index 0beb214be20..d6a41ca1c14 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts @@ -2,9 +2,12 @@ 'use strict'; +import * as DOMPurify from 'dompurify'; + import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Attachment } from '../../../js/common/core/attachment.js'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; +import { Buf } from '../../../js/common/core/buf.js'; import { Catch } from '../../../js/common/platform/catch.js'; import { ComposeSendBtnPopoverModule } from './compose-send-btn-popover-module.js'; import { GeneralMailFormatter } from './formatters/general-mail-formatter.js'; @@ -138,12 +141,50 @@ export class ComposeSendBtnModule extends ViewModule { a.type = 'application/octet-stream'; // so that Enigmail+Thunderbird does not attempt to display without decrypting } } + if (choices.richtext && !choices.encrypt && !choices.sign) { // extract inline images of plain rich-text messages (#3256) + const { htmlWithInlineImages, imgAttachments } = this.extractInlineImagesToAttachments(msg.body['text/html'] as string); + msg.body['text/html'] = htmlWithInlineImages; + msg.attachments.push(...imgAttachments); + } if (this.view.myPubkeyModule.shouldAttach()) { msg.attachments.push(Attachment.keyinfoAsPubkeyAtt(senderKi)); } await this.addNamesToMsg(msg); } + private extractInlineImagesToAttachments = (html: string) => { + const imgAttachments: Attachment[] = []; + DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (!node) { + return; + } + if ('src' in node) { + const img: Element = node; + const src = img.getAttribute('src') as string; + const { mimeType, data } = this.parseInlineImageSrc(src); + const imgAttachment = new Attachment({ name: img.getAttribute('name') || '', type: mimeType, data: Buf.fromBase64Str(data), inline: true }); + imgAttachment.id = Attachment.attachmentId(); + img.setAttribute('src', `cid:${imgAttachment.id}`); + imgAttachments.push(imgAttachment); + } + }); + const htmlWithInlineImages = DOMPurify.sanitize(html); + DOMPurify.removeAllHooks(); + return { htmlWithInlineImages, imgAttachments }; + } + + private parseInlineImageSrc = (src: string) => { + let mimeType; + let data = ''; + const parts = src.split(/[:;,]/); + if (parts.length === 4 && parts[0] === 'data' && parts[1].match(/^image\/\w+/) && parts[2] === 'base64') { + mimeType = parts[1]; + data = parts[3]; + } + return { mimeType, data }; + } + + private doSendMsg = async (msg: SendableMsg) => { // if this is a password-encrypted message, then we've already shown progress for uploading to backend // and this requests represents second half of uploadable effort. Else this represents all (no previous heavy requests) diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index 1452ee4a94e..d9464874d6c 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -144,7 +144,7 @@ export class SendableMsg { if (this.body['encrypted/buf']) { this.body = { 'text/plain': this.body['encrypted/buf'].toString() }; } - return await Mime.encode(this.body, this.headers, this.attachments, this.type, true); + return await Mime.encode(this.body, this.headers, this.attachments, this.type); } } diff --git a/extension/js/common/core/attachment.ts b/extension/js/common/core/attachment.ts index 3894cbc90be..0a4d702113a 100644 --- a/extension/js/common/core/attachment.ts +++ b/extension/js/common/core/attachment.ts @@ -3,6 +3,7 @@ 'use strict'; import { Buf } from './buf.js'; +import { Str } from './common.js'; type Attachment$treatAs = "publicKey" | 'privateKey' | "encryptedMsg" | "hidden" | "signature" | "encryptedFile" | "plainFile"; export type AttachmentMeta = { @@ -43,6 +44,10 @@ export class Attachment { return trimmed.replace(/[\u0000\u002f\u005c]/g, '_').replace(/__+/g, '_'); } + public static attachmentId = (): string => { + return `f_${Str.sloppyRandom(30)}@flowcrypt`; + } + constructor({ data, type, name, length, url, inline, id, msgId, treatAs, cid, contentDescription }: AttachmentMeta) { if (typeof data === 'undefined' && typeof url === 'undefined' && typeof id === 'undefined') { throw new Error('Attachment: one of data|url|id has to be set'); diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 2a3f1dd1a09..bdc9dca0f1c 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -2,8 +2,6 @@ 'use strict'; -import * as DOMPurify from 'dompurify'; - import { Dict, Str } from './common.js'; import { requireIso88592, requireMimeBuilder, requireMimeParser } from '../platform/require.js'; @@ -197,13 +195,7 @@ export class Mime { }); } - public static encode = async ( - body: SendableMsgBody, - headers: RichHeaders, - attachments: Attachment[] = [], - type?: MimeEncodeType, - extractInlineImagesToAttachments?: boolean - ): Promise => { + public static encode = async (body: SendableMsgBody, headers: RichHeaders, attachments: Attachment[] = [], type?: MimeEncodeType): Promise => { const rootContentType = type !== 'pgpMimeEncrypted' ? 'multipart/mixed' : `multipart/encrypted; protocol="application/pgp-encrypted";`; const rootNode = new MimeBuilder(rootContentType, { includeBccInHeader: true }); // tslint:disable-line:no-unsafe-any for (const key of Object.keys(headers)) { @@ -216,11 +208,7 @@ export class Mime { } else { contentNode = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any for (const type of Object.keys(body)) { - let content = body[type]!.toString(); // already present, that's why part of for loop - if (extractInlineImagesToAttachments && type === 'text/html') { - content = Mime.extractInlineImagesToAttachments(content, rootNode); - } - contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, content)); + contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!.toString())); // already present, that's why part of for loop } } rootNode.appendChild(contentNode); // tslint:disable-line:no-unsafe-any @@ -324,7 +312,7 @@ export class Mime { private static createAttNode = (attachment: Attachment): any => { // todo: MimeBuilder types const type = `${attachment.type}; name="${attachment.name}"`; - const id = `f_${Str.sloppyRandom(30)}@flowcrypt`; + const id = attachment.id || Attachment.attachmentId(); const header: Dict = {}; if (attachment.contentDescription) { header['Content-Description'] = attachment.contentDescription; @@ -414,38 +402,4 @@ export class Mime { } return node; } - - private static extractInlineImagesToAttachments = (html: string, rootNode: any) => { - const imgAttNodes: any[] = []; - DOMPurify.addHook('afterSanitizeAttributes', (node) => { - if (!node) { - return; - } - if ('src' in node) { - const img: Element = node; - const src = img.getAttribute('src') as string; - const { mimeType, data } = Mime.parseInlineImageSrc(src); - const imgAttachment = new Attachment({ name: img.getAttribute('name') || '', type: mimeType, data: Buf.fromBase64Str(data), inline: true }); - const imgAttNode = Mime.createAttNode(imgAttachment); - rootNode.appendChild(imgAttNode); // tslint:disable-line:no-unsafe-any - const imgAttachmentId = imgAttNode.getHeader('X-Attachment-Id'); // tslint:disable-line:no-unsafe-any - img.setAttribute('src', `cid:${imgAttachmentId}`); - imgAttNodes.push(imgAttNode); - } - }); - const htmlWithInlineImages = DOMPurify.sanitize(html); - DOMPurify.removeAllHooks(); - return htmlWithInlineImages; - } - - private static parseInlineImageSrc = (src: string) => { - let mimeType; - let data = ''; - const parts = src.split(/[:;,]/); - if (parts.length === 4 && parts[0] === 'data' && parts[1].match(/^image\/\w+/) && parts[2] === 'base64') { - mimeType = parts[1]; - data = parts[3]; - } - return { mimeType, data }; - } } From 7fcaae4fc3dce8043432aa1c3ccfc5db51096d18 Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 24 Dec 2020 00:44:18 +0200 Subject: [PATCH 13/17] finalyze, keepCidLinks: true --- .../compose-modules/compose-send-btn-module.ts | 11 ++++++++--- .../pgp_block_modules/pgp-block-render-module.ts | 2 +- extension/js/common/core/mime.ts | 3 ++- extension/js/common/platform/xss.ts | 2 +- test/source/util/parse.ts | 4 +++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts index d6a41ca1c14..869fe6a8084 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts @@ -162,9 +162,14 @@ export class ComposeSendBtnModule extends ViewModule { const img: Element = node; const src = img.getAttribute('src') as string; const { mimeType, data } = this.parseInlineImageSrc(src); - const imgAttachment = new Attachment({ name: img.getAttribute('name') || '', type: mimeType, data: Buf.fromBase64Str(data), inline: true }); - imgAttachment.id = Attachment.attachmentId(); - img.setAttribute('src', `cid:${imgAttachment.id}`); + const imgAttachment = new Attachment({ + cid: Attachment.attachmentId(), + name: img.getAttribute('name') || '', + type: mimeType, + data: Buf.fromBase64Str(data), + inline: true + }); + img.setAttribute('src', `cid:${imgAttachment.cid}`); imgAttachments.push(imgAttachment); } }); diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts index 9058e208793..edcd8300ff9 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts @@ -135,7 +135,7 @@ export class PgpBlockViewRenderModule { const contentId = a.href.replace(/^cid:/g, ''); const content = this.view.attachmentsModule.includedAtts.filter(a => a.type.indexOf('image/') === 0 && a.cid === `<${contentId}>`)[0]; if (content) { - img.src = `data:${content.type};base64,${content.getData().toBase64Str()}`; + img.src = `data:${a.type};base64,${content.getData().toBase64Str()}`; Xss.replaceElementDANGEROUSLY(a, img.outerHTML); // xss-safe-value - img.outerHTML was built using dom node api } else { Xss.replaceElementDANGEROUSLY(a, Xss.escape(`[broken link: ${a.href}]`)); // xss-escaped diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index bdc9dca0f1c..3294e2e04c5 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -312,7 +312,7 @@ export class Mime { private static createAttNode = (attachment: Attachment): any => { // todo: MimeBuilder types const type = `${attachment.type}; name="${attachment.name}"`; - const id = attachment.id || Attachment.attachmentId(); + const id = attachment.cid || Attachment.attachmentId(); const header: Dict = {}; if (attachment.contentDescription) { header['Content-Description'] = attachment.contentDescription; @@ -402,4 +402,5 @@ export class Mime { } return node; } + } diff --git a/extension/js/common/platform/xss.ts b/extension/js/common/platform/xss.ts index 2a4332e2db5..e88271c761a 100644 --- a/extension/js/common/platform/xss.ts +++ b/extension/js/common/platform/xss.ts @@ -105,7 +105,7 @@ export class Xss { } } if ('target' in node) { // open links in new window - (node as Element).setAttribute('target', '_blank'); + (node as Element).setAttribute('target','_blank'); // prevents https://www.owasp.org/index.php/Reverse_Tabnabbing (node as Element).setAttribute('rel', 'noopener noreferrer'); } diff --git a/test/source/util/parse.ts b/test/source/util/parse.ts index ddd9c682c99..9b27ca9d289 100644 --- a/test/source/util/parse.ts +++ b/test/source/util/parse.ts @@ -32,7 +32,9 @@ const strictParse = async (source: string): Promise => { }; const convertBase64ToMimeMsg = async (base64: string) => { - return await simpleParser(new Buffer(Buf.fromBase64Str(base64))); + return await simpleParser(new Buffer(Buf.fromBase64Str(base64)), { + keepCidLinks: true // #3256 + }); }; export default { strictParse, convertBase64ToMimeMsg }; From ba058b8e97b1b5490e9b5a1eabf45c6902ea441c Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 24 Dec 2020 00:51:31 +0200 Subject: [PATCH 14/17] better naming: htmlWithInlineImages -> htmlWithCidImages --- .../elements/compose-modules/compose-send-btn-module.ts | 8 ++++---- test/source/util/parse.ts | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts index 869fe6a8084..37bf645e17a 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts @@ -142,8 +142,8 @@ export class ComposeSendBtnModule extends ViewModule { } } if (choices.richtext && !choices.encrypt && !choices.sign) { // extract inline images of plain rich-text messages (#3256) - const { htmlWithInlineImages, imgAttachments } = this.extractInlineImagesToAttachments(msg.body['text/html'] as string); - msg.body['text/html'] = htmlWithInlineImages; + const { htmlWithCidImages, imgAttachments } = this.extractInlineImagesToAttachments(msg.body['text/html'] as string); + msg.body['text/html'] = htmlWithCidImages; msg.attachments.push(...imgAttachments); } if (this.view.myPubkeyModule.shouldAttach()) { @@ -173,9 +173,9 @@ export class ComposeSendBtnModule extends ViewModule { imgAttachments.push(imgAttachment); } }); - const htmlWithInlineImages = DOMPurify.sanitize(html); + const htmlWithCidImages = DOMPurify.sanitize(html); DOMPurify.removeAllHooks(); - return { htmlWithInlineImages, imgAttachments }; + return { htmlWithCidImages, imgAttachments }; } private parseInlineImageSrc = (src: string) => { diff --git a/test/source/util/parse.ts b/test/source/util/parse.ts index 9b27ca9d289..da54e44eb05 100644 --- a/test/source/util/parse.ts +++ b/test/source/util/parse.ts @@ -32,9 +32,7 @@ const strictParse = async (source: string): Promise => { }; const convertBase64ToMimeMsg = async (base64: string) => { - return await simpleParser(new Buffer(Buf.fromBase64Str(base64)), { - keepCidLinks: true // #3256 - }); + return await simpleParser(new Buffer(Buf.fromBase64Str(base64)), { keepCidLinks: true /* #3256 */ }); }; export default { strictParse, convertBase64ToMimeMsg }; From a85da677960528ac52e78d7556725f53d722c12a Mon Sep 17 00:00:00 2001 From: Limon Monte Date: Thu, 24 Dec 2020 01:42:55 +0200 Subject: [PATCH 15/17] fix the 'send pwd encrypted msg & check on flowcrypt site' test --- test/source/tests/flaky.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/tests/flaky.ts b/test/source/tests/flaky.ts index b7805568eb1..a76513660a7 100644 --- a/test/source/tests/flaky.ts +++ b/test/source/tests/flaky.ts @@ -120,7 +120,7 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test await fileInput!.uploadFile('test/samples/small.txt'); await ComposePageRecipe.sendAndClose(composePage, { password: msgPwd }); const msg = new GoogleData('flowcrypt.compatibility@gmail.com').getMessageBySubject(subject)!; - const webDecryptUrl = msg.payload!.body!.data!.match(/https:\/\/flowcrypt.com\/[a-z0-9A-Z]+/g)![0]; + const webDecryptUrl = msg.payload!.body!.data!.replace(///g, '/').match(/https:\/\/flowcrypt.com\/[a-z0-9A-Z]+/g)![0]; // while this test runs on a mock, it forwards the message/upload call to real backend - see `fwdToRealBackend` // that's why we are able to test the message on real flowcrypt.com/api and web. const webDecryptPage = await browser.newPage(t, webDecryptUrl); From d491c8fd3ba5caed4fce4edff146445ad1d6a8aa Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 31 Dec 2020 15:52:27 +0000 Subject: [PATCH 16/17] slight style update --- .../elements/compose-modules/compose-send-btn-module.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts index 37bf645e17a..c273834303a 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts @@ -141,8 +141,10 @@ export class ComposeSendBtnModule extends ViewModule { a.type = 'application/octet-stream'; // so that Enigmail+Thunderbird does not attempt to display without decrypting } } - if (choices.richtext && !choices.encrypt && !choices.sign) { // extract inline images of plain rich-text messages (#3256) - const { htmlWithCidImages, imgAttachments } = this.extractInlineImagesToAttachments(msg.body['text/html'] as string); + if (choices.richtext && !choices.encrypt && !choices.sign && msg.body['text/html']) { + // extract inline images of plain rich-text messages (#3256) + // todo - also apply to rich text signed-only messages + const { htmlWithCidImages, imgAttachments } = this.extractInlineImagesToAttachments(msg.body['text/html']); msg.body['text/html'] = htmlWithCidImages; msg.attachments.push(...imgAttachments); } From 5008408cad94e84be29f86f10582a4378c3cbafe Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 31 Dec 2020 16:06:38 +0000 Subject: [PATCH 17/17] added a todo --- test/source/tests/compose.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index cc8d712cf28..5880878c255 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1050,6 +1050,8 @@ const sendImgAndVerifyPresentInSentMsg = async (t: AvaContext, browser: BrowserH if (sendingType === 'plain') { expect(sentMsg.payload?.body?.data).to.match(/This is an automated puppeteer test: Test Sending Plain Message With Image/); return; + // todo - this test case is a stop-gap. We need to implement rendering of such messages below, + // then let test plain messages with images in them (referenced by cid) just like other types of messages below } let url = `chrome/dev/ci_pgp_host_page.htm?frameId=none&msgId=${encodeURIComponent(sentMsg.id)}&senderEmail=flowcrypt.compatibility%40gmail.com&isOutgoing=___cu_false___&acctEmail=flowcrypt.compatibility%40gmail.com`; if (sendingType === 'sign') {