From f5e09bc2db6cfae553461a22c4207fff6d94210f Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Wed, 5 Apr 2023 18:06:12 +0100
Subject: [PATCH 001/147] (wip) move in the direction of common renderer
---
.../compose-modules/compose-draft-module.ts | 4 +-
.../inbox-active-thread-module.ts | 47 ++++--------
.../common/api/email-provider/gmail/gmail.ts | 2 +-
extension/js/common/core/mime.ts | 49 +++++--------
extension/js/common/ui/message-renderer.ts | 71 +++++++++++++++++++
.../webmail/gmail-element-replacer.ts | 50 ++++++++++---
6 files changed, 146 insertions(+), 77 deletions(-)
create mode 100644 extension/js/common/ui/message-renderer.ts
diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts
index f19d498382e..2039a12c000 100644
--- a/extension/chrome/elements/compose-modules/compose-draft-module.ts
+++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts
@@ -11,7 +11,7 @@ import { Ui } from '../../../js/common/browser/ui.js';
import { Buf } from '../../../js/common/core/buf.js';
import { Str, Url } from '../../../js/common/core/common.js';
import { DecryptErrTypes, MsgUtil } from '../../../js/common/core/crypto/pgp/msg-util.js';
-import { Mime, MimeContent, MimeProccesedMsg } from '../../../js/common/core/mime.js';
+import { Mime, MimeContentWithHeaders, MimeProccesedMsg } from '../../../js/common/core/mime.js';
import { MsgBlockParser } from '../../../js/common/core/msg-block-parser.js';
import { Catch } from '../../../js/common/platform/catch.js';
import { GlobalStore } from '../../../js/common/platform/store/global-store.js';
@@ -293,7 +293,7 @@ export class ComposeDraftModule extends ViewModule {
}
};
- private fillAndRenderDraftHeaders = async (decoded: MimeContent) => {
+ private fillAndRenderDraftHeaders = async (decoded: MimeContentWithHeaders) => {
this.view.recipientsModule.addRecipientsAndShowPreview({ to: decoded.to, cc: decoded.cc, bcc: decoded.bcc });
if (decoded.from) {
this.view.S.now('input_from').val(decoded.from);
diff --git a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
index 9c0e3d655ca..c6c720d6e28 100644
--- a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
+++ b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
@@ -3,9 +3,9 @@
'use strict';
import { Bm, BrowserMsg } from '../../../../js/common/browser/browser-msg.js';
-import { FactoryReplyParams, XssSafeFactory } from '../../../../js/common/xss-safe-factory.js';
+import { FactoryReplyParams } from '../../../../js/common/xss-safe-factory.js';
import { GmailParser, GmailRes } from '../../../../js/common/api/email-provider/gmail/gmail-parser.js';
-import { Str, Url, UrlParams } from '../../../../js/common/core/common.js';
+import { Url, UrlParams } from '../../../../js/common/core/common.js';
import { ApiErr } from '../../../../js/common/api/shared/api-error.js';
import { BrowserMsgCommonHandlers } from '../../../../js/common/browser/browser-msg-common-handlers.js';
@@ -13,7 +13,7 @@ import { Buf } from '../../../../js/common/core/buf.js';
import { Catch } from '../../../../js/common/platform/catch.js';
import { InboxView } from '../inbox.js';
import { Lang } from '../../../../js/common/lang.js';
-import { Mime } from '../../../../js/common/core/mime.js';
+import { MessageRenderer } from '../../../../js/common/ui/message-renderer.js';
import { Ui } from '../../../../js/common/browser/ui.js';
import { ViewModule } from '../../../../js/common/view-module.js';
import { Xss } from '../../../../js/common/platform/xss.js';
@@ -105,40 +105,19 @@ export class InboxActiveThreadModule extends ViewModule {
private renderMsg = async (message: GmailRes.GmailMsg) => {
const htmlId = this.replyMsgId(message.id);
- const from = GmailParser.findHeader(message, 'from') || 'unknown';
+ const from = GmailParser.findHeader(message, 'from');
try {
const { raw } = await this.view.gmail.msgGet(message.id, 'raw');
- const mimeMsg = Buf.fromBase64UrlStr(raw!); // eslint-disable-line @typescript-eslint/no-non-null-assertion
- const { blocks, headers } = await Mime.process(mimeMsg);
- let r = '';
- let renderedAttachments = '';
- for (const block of blocks) {
- if (block.type === 'encryptedMsg' || block.type === 'publicKey' || block.type === 'privateKey' || block.type === 'signedMsg') {
- this.threadHasPgpBlock = true;
- }
- if (r) {
- r += '
';
- }
- if (['encryptedAttachment', 'plainAttachment'].includes(block.type)) {
- renderedAttachments += XssSafeFactory.renderableMsgBlock(
- this.view.factory,
- block,
- message.id,
- from,
- this.view.storage.sendAs && !!this.view.storage.sendAs[from]
- );
- } else if (this.view.showOriginal) {
- r += Xss.escape(Str.with(block.content)).replace(/\n/g, '
');
- } else {
- r += XssSafeFactory.renderableMsgBlock(this.view.factory, block, message.id, from, this.view.storage.sendAs && !!this.view.storage.sendAs[from]);
- }
- }
- if (renderedAttachments) {
- r += `${renderedAttachments}
`;
- }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const { blocks, headers } = await MessageRenderer.processMessageFromRaw(raw!);
+ // todo: review the meaning of threadHasPgpBlock
+ this.threadHasPgpBlock ||= blocks.some(block => ['encryptedMsg', 'publicKey', 'privateKey', 'signedMsg'].includes(block.type));
+ // todo: take `from` from the processedMessage?
const exportBtn = this.debugEmails.includes(this.view.acctEmail) ? 'download api export' : '';
- r =
- `` + r;
+ const r =
+ `` + MessageRenderer.renderMsg({ from, blocks }, this.view.factory, this.view.showOriginal, message.id, this.view.storage.sendAs);
$('.thread').append(this.wrapMsg(htmlId, r)); // xss-safe-factory
if (exportBtn) {
$('.action-export').on(
diff --git a/extension/js/common/api/email-provider/gmail/gmail.ts b/extension/js/common/api/email-provider/gmail/gmail.ts
index 75cad4a34a0..62e3c4383fd 100644
--- a/extension/js/common/api/email-provider/gmail/gmail.ts
+++ b/extension/js/common/api/email-provider/gmail/gmail.ts
@@ -295,7 +295,7 @@ export class Gmail extends EmailProviderApi implements EmailProviderInterface {
await this.apiGmailLoopThroughEmailsToCompileContacts(needles, gmailQuery, chunkedCb);
};
- /**
+ /** @deprecated should not be used by pgp_block frames
* Extracts the encrypted message from gmail api. Sometimes it's sent as a text, sometimes html, sometimes attachments in various forms.
* As MsgBlockParser detects incomplete encryptedMsg etc. and they get through, we're handling them too
*/
diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts
index ed3e2c1c94a..6fb75fb52f8 100644
--- a/extension/js/common/core/mime.ts
+++ b/extension/js/common/core/mime.ts
@@ -23,16 +23,19 @@ const Iso88592 = requireIso88592();
type AddressHeader = { address: string; name: string };
type MimeContentHeader = string | AddressHeader[];
export type MimeContent = {
- headers: Dict;
attachments: Attachment[];
rawSignedContent?: string;
subject?: string;
html?: string;
text?: string;
- from?: string;
+};
+
+export type MimeContentWithHeaders = MimeContent & {
+ headers: Dict;
to: string[];
cc: string[];
bcc: string[];
+ from?: string;
};
export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | 'smimeEncrypted' | 'smimeSigned' | undefined;
@@ -46,12 +49,14 @@ export type SendableMsgBody = {
};
/* eslint-enable @typescript-eslint/naming-convention */
export type MimeProccesedMsg = {
- rawSignedContent: string | undefined;
- headers: Dict;
+ rawSignedContent: string | undefined; // undefined if format was 'full'
blocks: MsgBlock[];
- from: string | undefined;
- to: string[];
};
+
+export type MimeProccesedFromRawMsg = MimeProccesedMsg & {
+ headers: Dict;
+};
+
type SendingType = 'to' | 'cc' | 'bcc';
export class Mime {
@@ -127,17 +132,17 @@ export class Mime {
}
}
return {
- headers: decoded.headers,
blocks,
- from: decoded.from,
- to: decoded.to,
rawSignedContent: decoded.rawSignedContent,
};
};
- public static process = async (mimeMsg: Uint8Array): Promise => {
+ public static process = async (mimeMsg: Uint8Array): Promise => {
const decoded = await Mime.decode(mimeMsg);
- return Mime.processDecoded(decoded);
+ return {
+ headers: decoded.headers,
+ ...Mime.processDecoded(decoded),
+ };
};
public static isPlainImgAttachment = (b: MsgBlock) => {
@@ -149,12 +154,6 @@ export class Mime {
);
};
- public static replyHeaders = (parsedMimeMsg: MimeContent) => {
- const msgId = String(parsedMimeMsg.headers['message-id'] || '');
- const refs = String(parsedMimeMsg.headers['in-reply-to'] || '');
- return { 'in-reply-to': msgId, references: refs + ' ' + msgId };
- };
-
public static resemblesMsg = (msg: Uint8Array | string) => {
const chunk = (typeof msg === 'string' ? msg.substring(0, 3000) : new Buf(msg.slice(0, 3000)).toUtfStr('ignore')).toLowerCase().replace(/\r\n/g, '\n');
const headers = chunk.split('\n\n')[0];
@@ -177,18 +176,8 @@ export class Mime {
return contentType.index === 0;
};
- public static decode = async (mimeMsg: Uint8Array | string): Promise => {
- let mimeContent: MimeContent = {
- attachments: [],
- headers: {},
- subject: undefined,
- text: undefined,
- html: undefined,
- from: undefined,
- to: [],
- cc: [],
- bcc: [],
- };
+ public static decode = async (mimeMsg: Uint8Array | string): Promise => {
+ let mimeContent: MimeContentWithHeaders;
const parser = new MimeParser();
const leafNodes: { [key: string]: MimeParserNode } = {};
parser.onbody = (node: MimeParserNode) => {
@@ -338,7 +327,7 @@ export class Mime {
return pgpMimeSigned;
};
- private static headerGetAddress = (parsedMimeMsg: MimeContent, headersNames: Array) => {
+ private static headerGetAddress = (parsedMimeMsg: MimeContentWithHeaders, headersNames: Array) => {
const result: { to: string[]; cc: string[]; bcc: string[] } = { to: [], cc: [], bcc: [] };
let from: string | undefined;
const getHdrValAsArr = (hdr: MimeContentHeader) =>
diff --git a/extension/js/common/ui/message-renderer.ts b/extension/js/common/ui/message-renderer.ts
new file mode 100644
index 00000000000..50980ae33ee
--- /dev/null
+++ b/extension/js/common/ui/message-renderer.ts
@@ -0,0 +1,71 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+import { GmailParser, GmailRes } from '../api/email-provider/gmail/gmail-parser.js';
+import { Buf } from '../core/buf.js';
+import { Dict, Str } from '../core/common.js';
+import { Mime, MimeContent, MimeProccesedMsg } from '../core/mime.js';
+import { MsgBlock } from '../core/msg-block.js';
+import { SendAsAlias } from '../platform/store/acct-store.js';
+import { Xss } from '../platform/xss.js';
+import { XssSafeFactory } from '../xss-safe-factory.js';
+
+export type ProccesedMsg = MimeProccesedMsg & {
+ from?: string;
+};
+
+export class MessageRenderer {
+ public static renderMsg = (
+ { from, blocks }: { blocks: MsgBlock[]; from?: string },
+ factory: XssSafeFactory,
+ showOriginal: boolean,
+ msgId: string, // todo: will be removed
+ sendAs?: Dict
+ ) => {
+ const isOutgoing = Boolean(from && !!sendAs?.[from]);
+ let r = '';
+ let renderedAttachments = '';
+ for (const block of blocks) {
+ if (r) {
+ r += '
';
+ }
+ if (['encryptedAttachment', 'plainAttachment'].includes(block.type)) {
+ renderedAttachments += XssSafeFactory.renderableMsgBlock(factory, block, msgId, from || 'unknown', isOutgoing);
+ } else if (showOriginal) {
+ r += Xss.escape(Str.with(block.content)).replace(/\n/g, '
');
+ } else {
+ r += XssSafeFactory.renderableMsgBlock(factory, block, msgId, from || 'unknown', isOutgoing);
+ }
+ }
+ if (renderedAttachments) {
+ r += `${renderedAttachments}
`;
+ }
+ return r;
+ };
+
+ public static process = async (gmailMsg: GmailRes.GmailMsg): Promise => {
+ const processedMsg = gmailMsg.raw ? await MessageRenderer.processMessageFromRaw(gmailMsg.raw) : MessageRenderer.processMessageFromFull(gmailMsg);
+ return { from: GmailParser.findHeader(gmailMsg, 'from'), ...processedMsg };
+ };
+
+ public static processMessageFromRaw = async (raw: string) => {
+ const mimeMsg = Buf.fromBase64UrlStr(raw);
+ return await Mime.process(mimeMsg);
+ };
+
+ private static processMessageFromFull = (gmailMsg: GmailRes.GmailMsg) => {
+ const bodies = GmailParser.findBodies(gmailMsg);
+ const attachments = GmailParser.findAttachments(gmailMsg);
+ const text = bodies['text/plain'] ? Buf.fromBase64UrlStr(bodies['text/plain']).toUtfStr() : undefined;
+ // todo: do we need to strip?
+ const html = bodies['text/html'] ? Xss.htmlSanitizeAndStripAllTags(Buf.fromBase64UrlStr(bodies['text/html']).toUtfStr(), '\n') : undefined;
+ // reconstructed MIME content
+ const mimeContent: MimeContent = {
+ text,
+ html,
+ attachments,
+ };
+ return Mime.processDecoded(mimeContent);
+ };
+}
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 0898db14854..30bb81d2b8a 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -25,16 +25,22 @@ import { SendAsAlias } from '../../common/platform/store/acct-store.js';
// todo: can we somehow define a purely relay class for ContactStore to clearly show that crypto-libraries are not loaded and can't be used?
import { ContactStore } from '../../common/platform/store/contact-store.js';
import { Buf } from '../../common/core/buf.js';
-import { MsgBlockParser } from '../../common/core/msg-block-parser.js';
+import { MessageRenderer, ProccesedMsg } from '../../common/ui/message-renderer.js';
type JQueryEl = JQuery;
+interface MessageCacheEntry {
+ full: Promise;
+ processedFull?: ProccesedMsg;
+}
+
export class GmailElementReplacer implements WebmailElementReplacer {
private debug = false;
private gmail: Gmail;
private recipientHasPgpCache: Dict = {};
private sendAs: Dict;
+ private messages: Dict = {};
private factory: XssSafeFactory;
private clientConfiguration: ClientConfiguration;
private pubLookup: PubLookup;
@@ -152,8 +158,29 @@ export class GmailElementReplacer implements WebmailElementReplacer {
}
};
+ private msgGetCached = (msgId: string): MessageCacheEntry => {
+ // todo: retries? exceptions?
+ let msgDownload = this.messages[msgId];
+ if (!msgDownload) {
+ this.messages[msgId] = { full: this.gmail.msgGet(msgId, 'full') };
+ msgDownload = this.messages[msgId];
+ }
+ return msgDownload;
+ };
+
+ private msgGetProcessed = async (msgId: string): Promise => {
+ // todo: retries? exceptions?
+ const msgDownload = this.msgGetCached(msgId);
+ if (msgDownload.processedFull) {
+ return msgDownload.processedFull;
+ }
+ const msg = await msgDownload.full;
+ msgDownload.processedFull = await MessageRenderer.process(msg);
+ return msgDownload.processedFull;
+ };
+
private everything = () => {
- this.replaceArmoredBlocks();
+ this.replaceArmoredBlocks().catch(Catch.reportErr);
this.replaceAttachments().catch(Catch.reportErr);
this.replaceComposeDraftLinks();
this.replaceConvoBtns();
@@ -163,7 +190,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
this.renderLocalDrafts().catch(Catch.reportErr);
};
- private replaceArmoredBlocks = () => {
+ private replaceArmoredBlocks = async () => {
const emailsContainingPgpBlock = $(this.sel.msgOuter).find(this.sel.msgInnerContainingPgp).not('.evaluated');
for (const emailContainer of emailsContainingPgpBlock) {
if (this.debug) {
@@ -173,14 +200,12 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (this.debug) {
console.debug('replaceArmoredBlocks() for of emailsContainingPgpBlock -> emailContainer added evaluated');
}
- const senderEmail = this.getSenderEmail(emailContainer);
- const isOutgoing = !!this.sendAs[senderEmail];
const msgId = this.determineMsgId(emailContainer);
- const { blocks } = MsgBlockParser.detectBlocks(emailContainer.innerText);
+ const { blocks, from } = await this.msgGetProcessed(msgId);
if (blocks.length === 1 && blocks[0].type === 'plainText') {
// only has single block which is plain text
} else {
- const replacementXssSafe = XssSafeFactory.renderableMsgBlocks(this.factory, blocks, msgId, senderEmail, isOutgoing);
+ const replacementXssSafe = MessageRenderer.renderMsg({ blocks, from }, this.factory, false, msgId, this.sendAs);
$(this.sel.translatePrompt).hide();
if (this.debug) {
console.debug('replaceArmoredBlocks() for of emailsContainingPgpBlock -> emailContainer replacing');
@@ -390,7 +415,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (this.debug) {
console.debug('processNewPgpAttachments() -> msgGet may take some time');
}
- const msg = await this.gmail.msgGet(msgId, 'full'); // todo: cache or thoroughly refactor in #5022
+ const msg = await this.msgGetCached(msgId).full;
if (this.debug) {
console.debug('processNewPgpAttachments() -> msgGet done -> processAttachments', msg);
}
@@ -423,7 +448,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (this.debug) {
console.debug('processAttachments()', attachmentMetas);
}
- let msgEl = this.getMsgBodyEl(msgId); // not a constant because sometimes elements get replaced, then returned by the function that replaced them
+ const msgEl = this.getMsgBodyEl(msgId); // not a constant because sometimes elements get replaced, then returned by the function that replaced them
const isBodyEmpty = msgEl.text() === '' || msgEl.text() === '\n';
const senderEmail = this.getSenderEmail(msgEl);
const isOutgoing = !!this.sendAs[senderEmail];
@@ -444,6 +469,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (treatAs !== 'plainFile') {
this.hideAttachment(attachmentSel, attachmentsContainerInner);
nRenderedAttachments--;
+ /*
if (treatAs === 'encryptedFile') {
// actual encrypted attachment - show it
attachmentsContainerInner.prepend(this.factory.embeddedAttachment(a, true)); // xss-safe-factory
@@ -483,7 +509,9 @@ export class GmailElementReplacer implements WebmailElementReplacer {
const embeddedSignedMsgXssSafe = this.factory.embeddedMsg('signedMsg', '', msgId, false, senderEmail, true);
msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
}
+ */
} else if (treatAs === 'plainFile' && a.name.substr(-4) === '.asc') {
+ // todo:
// normal looking attachment ending with .asc
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const data = await this.gmail.attachmentGetChunk(msgId, a.id!); // .id is present when fetched from api
@@ -579,6 +607,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
return nRenderedAttachments;
};
+ /* todo:
private renderBackupFromFile = async (
attachmentMeta: Attachment,
attachmentsContainerInner: JQueryEl,
@@ -598,7 +627,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'append', this.factory.embeddedBackup(downloadedAttachment.data.toUtfStr())); // xss-safe-factory
return nRenderedAttachments;
};
-
+ */
private filterAttachments = (potentialMatches: JQueryEl | HTMLElement, regExp: RegExp) => {
return $(potentialMatches)
.filter('span.aZo:visible, span.a5r:visible')
@@ -683,6 +712,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
}
};
+ // todo: should we use it?
private getSenderEmail = (msgEl: HTMLElement | JQueryEl) => {
return ($(msgEl).closest('.gs').find('span.gD').attr('email') || '').toLowerCase();
};
From 73082cf2ff864efc5c58e1251e12aa41390161ca Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Fri, 7 Apr 2023 09:26:57 +0100
Subject: [PATCH 002/147] fix
---
.../compose-modules/compose-draft-module.ts | 4 +-
.../compose-modules/compose-quote-module.ts | 10 +-
.../inbox-active-thread-module.ts | 17 +++-
extension/js/common/core/mime.ts | 91 +++++++++++++------
.../js/common/{ui => }/message-renderer.ts | 47 ++++++----
.../webmail/gmail-element-replacer.ts | 30 +++---
6 files changed, 126 insertions(+), 73 deletions(-)
rename extension/js/common/{ui => }/message-renderer.ts (57%)
diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts
index 2039a12c000..1ecfc3a0852 100644
--- a/extension/chrome/elements/compose-modules/compose-draft-module.ts
+++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts
@@ -304,13 +304,13 @@ export class ComposeDraftModule extends ViewModule {
};
private decryptAndRenderDraft = async (encrypted: MimeProccesedMsg): Promise => {
- const rawBlock = encrypted.blocks.find(b => ['encryptedMsg', 'signedMsg', 'pkcs7'].includes(b.type));
+ const rawBlock = encrypted.blocks.find(b => ['encryptedMsg', 'signedMsg', 'pkcs7'].includes(b.block.type));
if (!rawBlock) {
return await this.abortAndRenderReplyMsgComposeTableIfIsReplyBox('!rawBlock');
}
const decrypted = await MsgUtil.decryptMessage({
kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail),
- encryptedData: rawBlock.content,
+ encryptedData: rawBlock.block.content,
verificationPubs: [],
});
if (!decrypted.success) {
diff --git a/extension/chrome/elements/compose-modules/compose-quote-module.ts b/extension/chrome/elements/compose-modules/compose-quote-module.ts
index eee74fa4f3b..442ca3284ca 100644
--- a/extension/chrome/elements/compose-modules/compose-quote-module.ts
+++ b/extension/chrome/elements/compose-modules/compose-quote-module.ts
@@ -104,14 +104,14 @@ export class ComposeQuoteModule extends ViewModule {
decryptedBlockTypes.push('decryptedAttachment');
}
const readableBlocks: MsgBlock[] = [];
- for (const block of message.blocks.filter(b => readableBlockTypes.includes(b.type))) {
- if (['encryptedMsg', 'signedMsg'].includes(block.type)) {
+ for (const block of message.blocks.filter(b => readableBlockTypes.includes(b.block.type))) {
+ if (['encryptedMsg', 'signedMsg'].includes(block.block.type)) {
this.setQuoteLoaderProgress('decrypting...');
- const decrypted = await this.decryptMessage(block.content);
+ const decrypted = await this.decryptMessage(block.block.content);
const msgBlocks = await MsgBlockParser.fmtDecryptedAsSanitizedHtmlBlocks(Buf.fromUtfStr(decrypted));
readableBlocks.push(...msgBlocks.blocks.filter(b => decryptedBlockTypes.includes(b.type)));
} else {
- readableBlocks.push(block);
+ readableBlocks.push(block.block);
}
}
const decryptedAndFormatedContent: string[] = [];
@@ -154,7 +154,7 @@ export class ComposeQuoteModule extends ViewModule {
return {
headers,
text: decryptedAndFormatedContent.join('\n'),
- isOnlySigned: !!(decoded.rawSignedContent || (message.blocks.length > 0 && message.blocks[0].type === 'signedMsg')),
+ isOnlySigned: !!(decoded.rawSignedContent || (message.blocks.length > 0 && message.blocks[0].block.type === 'signedMsg')),
decryptedFiles,
};
} catch (e) {
diff --git a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
index c6c720d6e28..5c28c09b15c 100644
--- a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
+++ b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
@@ -13,7 +13,7 @@ import { Buf } from '../../../../js/common/core/buf.js';
import { Catch } from '../../../../js/common/platform/catch.js';
import { InboxView } from '../inbox.js';
import { Lang } from '../../../../js/common/lang.js';
-import { MessageRenderer } from '../../../../js/common/ui/message-renderer.js';
+import { MessageRenderer } from '../../../../js/common/message-renderer.js';
import { Ui } from '../../../../js/common/browser/ui.js';
import { ViewModule } from '../../../../js/common/view-module.js';
import { Xss } from '../../../../js/common/platform/xss.js';
@@ -111,13 +111,24 @@ export class InboxActiveThreadModule extends ViewModule {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { blocks, headers } = await MessageRenderer.processMessageFromRaw(raw!);
// todo: review the meaning of threadHasPgpBlock
- this.threadHasPgpBlock ||= blocks.some(block => ['encryptedMsg', 'publicKey', 'privateKey', 'signedMsg'].includes(block.type));
+ this.threadHasPgpBlock ||= blocks.some(block => ['encryptedMsg', 'publicKey', 'privateKey', 'signedMsg'].includes(block.block.type));
// todo: take `from` from the processedMessage?
+ const { renderedXssSafe, renderedAttachments } = MessageRenderer.renderMsg(
+ { from, blocks },
+ this.view.factory,
+ this.view.showOriginal,
+ message.id,
+ this.view.storage.sendAs
+ );
const exportBtn = this.debugEmails.includes(this.view.acctEmail) ? 'download api export' : '';
const r =
`` + MessageRenderer.renderMsg({ from, blocks }, this.view.factory, this.view.showOriginal, message.id, this.view.storage.sendAs);
+ } ${exportBtn}
` +
+ renderedXssSafe +
+ (renderedAttachments.length
+ ? `${renderedAttachments.map(a => a.renderedBlock).join('')}
`
+ : '');
$('.thread').append(this.wrapMsg(htmlId, r)); // xss-safe-factory
if (exportBtn) {
$('.action-export').on(
diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts
index 6fb75fb52f8..1f66e6160f6 100644
--- a/extension/js/common/core/mime.ts
+++ b/extension/js/common/core/mime.ts
@@ -50,7 +50,7 @@ export type SendableMsgBody = {
/* eslint-enable @typescript-eslint/naming-convention */
export type MimeProccesedMsg = {
rawSignedContent: string | undefined; // undefined if format was 'full'
- blocks: MsgBlock[];
+ blocks: { block: MsgBlock; file?: Attachment }[]; // may be many blocks per file
};
export type MimeProccesedFromRawMsg = MimeProccesedMsg & {
@@ -61,22 +61,25 @@ type SendingType = 'to' | 'cc' | 'bcc';
export class Mime {
public static processDecoded = (decoded: MimeContent): MimeProccesedMsg => {
- const blocks: MsgBlock[] = [];
+ const blocksFromBody: MsgBlock[] = [];
if (decoded.text) {
const blocksFromTextPart = MsgBlockParser.detectBlocks(Str.normalize(decoded.text)).blocks;
// if there are some encryption-related blocks found in the text section, which we can use, and not look at the html section
if (blocksFromTextPart.find(b => ['pkcs7', 'encryptedMsg', 'signedMsg', 'publicKey', 'privateKey'].includes(b.type))) {
- blocks.push(...blocksFromTextPart); // because the html most likely containt the same thing, just harder to parse pgp sections cause it's html
+ blocksFromBody.push(...blocksFromTextPart); // because the html most likely containt the same thing, just harder to parse pgp sections cause it's html
} else if (decoded.html) {
// if no pgp blocks found in text part and there is html part, prefer html
- blocks.push(MsgBlock.fromContent('plainHtml', decoded.html));
+ blocksFromBody.push(MsgBlock.fromContent('plainHtml', decoded.html));
} else {
// else if no html and just a plain text message, use that
- blocks.push(...blocksFromTextPart);
+ blocksFromBody.push(...blocksFromTextPart);
}
} else if (decoded.html) {
- blocks.push(MsgBlock.fromContent('plainHtml', decoded.html));
+ blocksFromBody.push(MsgBlock.fromContent('plainHtml', decoded.html));
}
+ const blocks: { block: MsgBlock; file?: Attachment }[] = blocksFromBody.map(block => {
+ return { block };
+ });
const signatureAttachments: Attachment[] = [];
for (const file of decoded.attachments) {
const isBodyEmpty = decoded.text === '' || decoded.text === '\n';
@@ -84,51 +87,69 @@ export class Mime {
if (treatAs === 'encryptedMsg') {
const armored = PgpArmor.clip(file.getData().toUtfStr());
if (armored) {
- blocks.push(MsgBlock.fromContent('encryptedMsg', armored));
+ blocks.push({ block: MsgBlock.fromContent('encryptedMsg', armored), file });
}
} else if (treatAs === 'signature') {
signatureAttachments.push(file);
} else if (treatAs === 'publicKey') {
- blocks.push(...MsgBlockParser.detectBlocks(file.getData().toUtfStr()).blocks);
+ blocks.push(
+ ...MsgBlockParser.detectBlocks(file.getData().toUtfStr()).blocks.map(block => {
+ return { block, file }; // todo: test when more than one
+ })
+ );
} else if (treatAs === 'privateKey') {
- blocks.push(...MsgBlockParser.detectBlocks(file.getData().toUtfStr()).blocks);
- } else if (treatAs === 'encryptedFile') {
blocks.push(
- MsgBlock.fromAttachment('encryptedAttachment', '', {
+ ...MsgBlockParser.detectBlocks(file.getData().toUtfStr()).blocks.map(block => {
+ return { block, file }; // todo: test when more than one
+ })
+ );
+ } else if (treatAs === 'encryptedFile') {
+ blocks.push({
+ // todo: fromAttachment return complete record?
+ block: MsgBlock.fromAttachment('encryptedAttachment', '', {
name: file.name,
type: file.type,
length: file.getData().length,
data: file.getData(),
- })
- );
+ }),
+ file,
+ });
} else if (treatAs === 'plainFile') {
- blocks.push(
- MsgBlock.fromAttachment('plainAttachment', '', {
+ blocks.push({
+ block: MsgBlock.fromAttachment('plainAttachment', '', {
name: file.name,
type: file.type,
length: file.getData().length,
data: file.getData(),
inline: file.inline,
cid: file.cid,
- })
- );
+ }),
+ file,
+ });
}
}
if (signatureAttachments.length) {
// todo: if multiple signatures, figure out which fits what
- const signature = signatureAttachments[0].getData().toUtfStr();
- for (const block of blocks) {
- if (block.type === 'plainText') {
- block.type = 'signedText';
- block.signature = signature;
- } else if (block.type === 'plainHtml') {
- block.type = 'signedHtml';
- block.signature = signature;
+ const signatureAttachment = signatureAttachments[0];
+ // todo: data may not be present
+ if (signatureAttachment.hasData()) {
+ const signature = signatureAttachment.getData().toUtfStr();
+ for (const block of blocks) {
+ if (block.block.type === 'plainText') {
+ block.block.type = 'signedText';
+ block.block.signature = signature;
+ } else if (block.block.type === 'plainHtml') {
+ block.block.type = 'signedHtml';
+ block.block.signature = signature;
+ }
}
- }
- if (!blocks.find(block => ['plainText', 'plainHtml', 'signedMsg', 'signedHtml', 'signedText'].includes(block.type))) {
- // signed an empty message
- blocks.push(new MsgBlock('signedMsg', '', true, signature));
+ if (!blocks.find(block => ['plainText', 'plainHtml', 'signedMsg', 'signedHtml', 'signedText'].includes(block.block.type))) {
+ // signed an empty message
+ blocks.push({ block: new MsgBlock('signedMsg', '', true, signature), file: signatureAttachment });
+ }
+ } else {
+ // debugger;
+ throw new Error('No data.');
}
}
return {
@@ -177,7 +198,17 @@ export class Mime {
};
public static decode = async (mimeMsg: Uint8Array | string): Promise => {
- let mimeContent: MimeContentWithHeaders;
+ let mimeContent: MimeContentWithHeaders = {
+ attachments: [],
+ headers: {},
+ subject: undefined,
+ text: undefined,
+ html: undefined,
+ from: undefined,
+ to: [],
+ cc: [],
+ bcc: [],
+ };
const parser = new MimeParser();
const leafNodes: { [key: string]: MimeParserNode } = {};
parser.onbody = (node: MimeParserNode) => {
diff --git a/extension/js/common/ui/message-renderer.ts b/extension/js/common/message-renderer.ts
similarity index 57%
rename from extension/js/common/ui/message-renderer.ts
rename to extension/js/common/message-renderer.ts
index 50980ae33ee..f60a25bde9e 100644
--- a/extension/js/common/ui/message-renderer.ts
+++ b/extension/js/common/message-renderer.ts
@@ -2,22 +2,28 @@
'use strict';
-import { GmailParser, GmailRes } from '../api/email-provider/gmail/gmail-parser.js';
-import { Buf } from '../core/buf.js';
-import { Dict, Str } from '../core/common.js';
-import { Mime, MimeContent, MimeProccesedMsg } from '../core/mime.js';
-import { MsgBlock } from '../core/msg-block.js';
-import { SendAsAlias } from '../platform/store/acct-store.js';
-import { Xss } from '../platform/xss.js';
-import { XssSafeFactory } from '../xss-safe-factory.js';
+import { GmailParser, GmailRes } from './api/email-provider/gmail/gmail-parser.js';
+import { Attachment } from './core/attachment.js';
+import { Buf } from './core/buf.js';
+import { Dict, Str } from './core/common.js';
+import { Mime, MimeContent, MimeProccesedMsg } from './core/mime.js';
+import { MsgBlock } from './core/msg-block.js';
+import { SendAsAlias } from './platform/store/acct-store.js';
+import { Xss } from './platform/xss.js';
+import { XssSafeFactory } from './xss-safe-factory.js';
export type ProccesedMsg = MimeProccesedMsg & {
from?: string;
};
+export type RenderedAttachment = {
+ renderedBlock: string;
+ file: Attachment;
+};
+
export class MessageRenderer {
public static renderMsg = (
- { from, blocks }: { blocks: MsgBlock[]; from?: string },
+ { from, blocks }: { blocks: { block: MsgBlock; file?: Attachment }[]; from?: string },
factory: XssSafeFactory,
showOriginal: boolean,
msgId: string, // todo: will be removed
@@ -25,23 +31,28 @@ export class MessageRenderer {
) => {
const isOutgoing = Boolean(from && !!sendAs?.[from]);
let r = '';
- let renderedAttachments = '';
+ const renderedAttachments: RenderedAttachment[] = [];
for (const block of blocks) {
if (r) {
r += '
';
}
- if (['encryptedAttachment', 'plainAttachment'].includes(block.type)) {
- renderedAttachments += XssSafeFactory.renderableMsgBlock(factory, block, msgId, from || 'unknown', isOutgoing);
+ // todo: we don't need types, just source property?
+ if (['encryptedAttachment', 'plainAttachment'].includes(block.block.type) && !block.file) {
+ // debugger;
+ throw new Error('Unexpected!');
+ }
+ if (block.file) {
+ renderedAttachments.push({
+ renderedBlock: XssSafeFactory.renderableMsgBlock(factory, block.block, msgId, from || 'unknown', isOutgoing),
+ file: block.file,
+ });
} else if (showOriginal) {
- r += Xss.escape(Str.with(block.content)).replace(/\n/g, '
');
+ r += Xss.escape(Str.with(block.block.content)).replace(/\n/g, '
');
} else {
- r += XssSafeFactory.renderableMsgBlock(factory, block, msgId, from || 'unknown', isOutgoing);
+ r += XssSafeFactory.renderableMsgBlock(factory, block.block, msgId, from || 'unknown', isOutgoing);
}
}
- if (renderedAttachments) {
- r += `${renderedAttachments}
`;
- }
- return r;
+ return { renderedXssSafe: r, renderedAttachments };
};
public static process = async (gmailMsg: GmailRes.GmailMsg): Promise => {
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 30bb81d2b8a..ec7614481ab 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -25,13 +25,13 @@ import { SendAsAlias } from '../../common/platform/store/acct-store.js';
// todo: can we somehow define a purely relay class for ContactStore to clearly show that crypto-libraries are not loaded and can't be used?
import { ContactStore } from '../../common/platform/store/contact-store.js';
import { Buf } from '../../common/core/buf.js';
-import { MessageRenderer, ProccesedMsg } from '../../common/ui/message-renderer.js';
+import { MessageRenderer, RenderedAttachment } from '../../common/message-renderer.js';
type JQueryEl = JQuery;
interface MessageCacheEntry {
full: Promise;
- processedFull?: ProccesedMsg;
+ processedFull?: { renderedXssSafe?: string; renderedAttachments: RenderedAttachment[] };
}
export class GmailElementReplacer implements WebmailElementReplacer {
@@ -168,14 +168,20 @@ export class GmailElementReplacer implements WebmailElementReplacer {
return msgDownload;
};
- private msgGetProcessed = async (msgId: string): Promise => {
+ private msgGetProcessed = async (msgId: string): Promise<{ renderedXssSafe?: string; renderedAttachments: RenderedAttachment[] }> => {
// todo: retries? exceptions?
const msgDownload = this.msgGetCached(msgId);
if (msgDownload.processedFull) {
return msgDownload.processedFull;
}
const msg = await msgDownload.full;
- msgDownload.processedFull = await MessageRenderer.process(msg);
+ const { blocks, from } = await MessageRenderer.process(msg);
+ if (blocks.length === 1 && blocks[0].block.type === 'plainText') {
+ // only has single block which is plain text
+ msgDownload.processedFull = { renderedAttachments: [] };
+ } else {
+ msgDownload.processedFull = MessageRenderer.renderMsg({ blocks, from }, this.factory, false, msgId, this.sendAs);
+ }
return msgDownload.processedFull;
};
@@ -201,16 +207,13 @@ export class GmailElementReplacer implements WebmailElementReplacer {
console.debug('replaceArmoredBlocks() for of emailsContainingPgpBlock -> emailContainer added evaluated');
}
const msgId = this.determineMsgId(emailContainer);
- const { blocks, from } = await this.msgGetProcessed(msgId);
- if (blocks.length === 1 && blocks[0].type === 'plainText') {
- // only has single block which is plain text
- } else {
- const replacementXssSafe = MessageRenderer.renderMsg({ blocks, from }, this.factory, false, msgId, this.sendAs);
+ const { renderedXssSafe } = await this.msgGetProcessed(msgId);
+ if (renderedXssSafe) {
$(this.sel.translatePrompt).hide();
if (this.debug) {
console.debug('replaceArmoredBlocks() for of emailsContainingPgpBlock -> emailContainer replacing');
}
- this.updateMsgBodyEl_DANGEROUSLY(emailContainer, 'set', replacementXssSafe); // xss-safe-factory: replace_blocks is XSS safe
+ this.updateMsgBodyEl_DANGEROUSLY(emailContainer, 'set', renderedXssSafe); // xss-safe-factory: replace_blocks is XSS safe
if (this.debug) {
console.debug('replaceArmoredBlocks() for of emailsContainingPgpBlock -> emailContainer replaced');
}
@@ -448,7 +451,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (this.debug) {
console.debug('processAttachments()', attachmentMetas);
}
- const msgEl = this.getMsgBodyEl(msgId); // not a constant because sometimes elements get replaced, then returned by the function that replaced them
+ let msgEl = this.getMsgBodyEl(msgId); // not a constant because sometimes elements get replaced, then returned by the function that replaced them
const isBodyEmpty = msgEl.text() === '' || msgEl.text() === '\n';
const senderEmail = this.getSenderEmail(msgEl);
const isOutgoing = !!this.sendAs[senderEmail];
@@ -469,7 +472,6 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (treatAs !== 'plainFile') {
this.hideAttachment(attachmentSel, attachmentsContainerInner);
nRenderedAttachments--;
- /*
if (treatAs === 'encryptedFile') {
// actual encrypted attachment - show it
attachmentsContainerInner.prepend(this.factory.embeddedAttachment(a, true)); // xss-safe-factory
@@ -509,7 +511,6 @@ export class GmailElementReplacer implements WebmailElementReplacer {
const embeddedSignedMsgXssSafe = this.factory.embeddedMsg('signedMsg', '', msgId, false, senderEmail, true);
msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
}
- */
} else if (treatAs === 'plainFile' && a.name.substr(-4) === '.asc') {
// todo:
// normal looking attachment ending with .asc
@@ -607,7 +608,6 @@ export class GmailElementReplacer implements WebmailElementReplacer {
return nRenderedAttachments;
};
- /* todo:
private renderBackupFromFile = async (
attachmentMeta: Attachment,
attachmentsContainerInner: JQueryEl,
@@ -627,7 +627,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'append', this.factory.embeddedBackup(downloadedAttachment.data.toUtfStr())); // xss-safe-factory
return nRenderedAttachments;
};
- */
+
private filterAttachments = (potentialMatches: JQueryEl | HTMLElement, regExp: RegExp) => {
return $(potentialMatches)
.filter('span.aZo:visible, span.a5r:visible')
From e91bc1b7e5881ffb4924469bdfd57d06ab5ee0d6 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sat, 8 Apr 2023 08:30:14 +0100
Subject: [PATCH 003/147] fix
---
.../inbox-active-thread-module.ts | 10 ++++++----
extension/js/common/core/mime.ts | 9 ++++++---
extension/js/common/message-renderer.ts | 18 +++++++-----------
.../webmail/gmail-element-replacer.ts | 12 +++++++-----
test/source/tests/decrypt.ts | 15 +++++++++++++--
5 files changed, 39 insertions(+), 25 deletions(-)
diff --git a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
index 5c28c09b15c..f1e1b0e8f72 100644
--- a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
+++ b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
@@ -3,7 +3,7 @@
'use strict';
import { Bm, BrowserMsg } from '../../../../js/common/browser/browser-msg.js';
-import { FactoryReplyParams } from '../../../../js/common/xss-safe-factory.js';
+import { FactoryReplyParams, XssSafeFactory } from '../../../../js/common/xss-safe-factory.js';
import { GmailParser, GmailRes } from '../../../../js/common/api/email-provider/gmail/gmail-parser.js';
import { Url, UrlParams } from '../../../../js/common/core/common.js';
@@ -113,7 +113,7 @@ export class InboxActiveThreadModule extends ViewModule {
// todo: review the meaning of threadHasPgpBlock
this.threadHasPgpBlock ||= blocks.some(block => ['encryptedMsg', 'publicKey', 'privateKey', 'signedMsg'].includes(block.block.type));
// todo: take `from` from the processedMessage?
- const { renderedXssSafe, renderedAttachments } = MessageRenderer.renderMsg(
+ const { renderedXssSafe, attachmentBlocks, isOutgoing } = MessageRenderer.renderMsg(
{ from, blocks },
this.view.factory,
this.view.showOriginal,
@@ -126,8 +126,10 @@ export class InboxActiveThreadModule extends ViewModule {
headers.date
} ${exportBtn}` +
renderedXssSafe +
- (renderedAttachments.length
- ? `${renderedAttachments.map(a => a.renderedBlock).join('')}
`
+ (attachmentBlocks.length // todo: we always have data on this page (for now), as we download 'raw'
+ ? `${attachmentBlocks
+ .map(block => XssSafeFactory.renderableMsgBlock(this.view.factory, block.block, message.id, from || 'unknown', isOutgoing))
+ .join('')}
`
: '');
$('.thread').append(this.wrapMsg(htmlId, r)); // xss-safe-factory
if (exportBtn) {
diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts
index 1f66e6160f6..f1d85420125 100644
--- a/extension/js/common/core/mime.ts
+++ b/extension/js/common/core/mime.ts
@@ -109,8 +109,10 @@ export class Mime {
block: MsgBlock.fromAttachment('encryptedAttachment', '', {
name: file.name,
type: file.type,
- length: file.getData().length,
- data: file.getData(),
+ length: file.hasData() ? file.getData().length : undefined,
+ data: file.hasData() ? file.getData() : undefined,
+ id: file.id, // todo:
+ cid: file.cid, // todo:
}),
file,
});
@@ -122,7 +124,8 @@ export class Mime {
length: file.getData().length,
data: file.getData(),
inline: file.inline,
- cid: file.cid,
+ id: file.id, // todo:
+ cid: file.cid, // todo:
}),
file,
});
diff --git a/extension/js/common/message-renderer.ts b/extension/js/common/message-renderer.ts
index f60a25bde9e..867b5715bf0 100644
--- a/extension/js/common/message-renderer.ts
+++ b/extension/js/common/message-renderer.ts
@@ -16,9 +16,9 @@ export type ProccesedMsg = MimeProccesedMsg & {
from?: string;
};
-export type RenderedAttachment = {
- renderedBlock: string;
- file: Attachment;
+export type AttachmentBlock = {
+ block: MsgBlock;
+ file: Attachment; // todo: only need id in MsgBlock's AttachmentMeta?
};
export class MessageRenderer {
@@ -31,28 +31,24 @@ export class MessageRenderer {
) => {
const isOutgoing = Boolean(from && !!sendAs?.[from]);
let r = '';
- const renderedAttachments: RenderedAttachment[] = [];
+ const attachmentBlocks: AttachmentBlock[] = [];
for (const block of blocks) {
if (r) {
r += '
';
}
- // todo: we don't need types, just source property?
if (['encryptedAttachment', 'plainAttachment'].includes(block.block.type) && !block.file) {
// debugger;
throw new Error('Unexpected!');
}
- if (block.file) {
- renderedAttachments.push({
- renderedBlock: XssSafeFactory.renderableMsgBlock(factory, block.block, msgId, from || 'unknown', isOutgoing),
- file: block.file,
- });
+ if (block.file && ['encryptedAttachment', 'plainAttachment'].includes(block.block.type)) {
+ attachmentBlocks.push({ block: block.block, file: block.file });
} else if (showOriginal) {
r += Xss.escape(Str.with(block.block.content)).replace(/\n/g, '
');
} else {
r += XssSafeFactory.renderableMsgBlock(factory, block.block, msgId, from || 'unknown', isOutgoing);
}
}
- return { renderedXssSafe: r, renderedAttachments };
+ return { renderedXssSafe: r, attachmentBlocks, isOutgoing };
};
public static process = async (gmailMsg: GmailRes.GmailMsg): Promise => {
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index ec7614481ab..7cd2e359306 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -25,13 +25,15 @@ import { SendAsAlias } from '../../common/platform/store/acct-store.js';
// todo: can we somehow define a purely relay class for ContactStore to clearly show that crypto-libraries are not loaded and can't be used?
import { ContactStore } from '../../common/platform/store/contact-store.js';
import { Buf } from '../../common/core/buf.js';
-import { MessageRenderer, RenderedAttachment } from '../../common/message-renderer.js';
+import { MessageRenderer, AttachmentBlock } from '../../common/message-renderer.js';
type JQueryEl = JQuery;
+type ProcessedMessage = { renderedXssSafe?: string; attachmentBlocks: AttachmentBlock[] };
+
interface MessageCacheEntry {
full: Promise;
- processedFull?: { renderedXssSafe?: string; renderedAttachments: RenderedAttachment[] };
+ processedFull?: ProcessedMessage;
}
export class GmailElementReplacer implements WebmailElementReplacer {
@@ -168,7 +170,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
return msgDownload;
};
- private msgGetProcessed = async (msgId: string): Promise<{ renderedXssSafe?: string; renderedAttachments: RenderedAttachment[] }> => {
+ private msgGetProcessed = async (msgId: string): Promise => {
// todo: retries? exceptions?
const msgDownload = this.msgGetCached(msgId);
if (msgDownload.processedFull) {
@@ -178,7 +180,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
const { blocks, from } = await MessageRenderer.process(msg);
if (blocks.length === 1 && blocks[0].block.type === 'plainText') {
// only has single block which is plain text
- msgDownload.processedFull = { renderedAttachments: [] };
+ msgDownload.processedFull = { attachmentBlocks: [] };
} else {
msgDownload.processedFull = MessageRenderer.renderMsg({ blocks, from }, this.factory, false, msgId, this.sendAs);
}
@@ -418,7 +420,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (this.debug) {
console.debug('processNewPgpAttachments() -> msgGet may take some time');
}
- const msg = await this.msgGetCached(msgId).full;
+ const msg = await this.msgGetCached(msgId).full; // todo: msgGetProcessed
if (this.debug) {
console.debug('processNewPgpAttachments() -> msgGet done -> processAttachments', msg);
}
diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts
index 2135d7c9dc4..5ee4625c07d 100644
--- a/test/source/tests/decrypt.ts
+++ b/test/source/tests/decrypt.ts
@@ -1000,14 +1000,25 @@ XZ8r4OC6sguP/yozWlkG+7dDxsgKQVBENeG6Lw==
const extraAuthHeaders = { Authorization: `Bearer ${accessToken}` }; // eslint-disable-line @typescript-eslint/naming-convention
const gmailPage = await browser.newPage(t, `${t.urls?.mockGmailUrl()}/1866867cfdb8b61e`, undefined, extraAuthHeaders);
await gmailPage.waitAll('iframe');
- const pgpBlock = await gmailPage.getFrame(['pgp_block.htm']);
+ const pgpBlocks = await Promise.all((await gmailPage.getFramesUrls(['pgp_block.htm'])).map(url => gmailPage.getFrame([url])));
+ expect(pgpBlocks.length).to.equal(3);
+ await BrowserRecipe.pgpBlockCheck(t, pgpBlocks[0], {
+ content: ['this is message 3 for flowcrypt issue 4342'],
+ encryption: 'not encrypted',
+ signature: 'signed',
+ });
// should re-fetch the correct text/plain text with signature
- await BrowserRecipe.pgpBlockCheck(t, pgpBlock, {
+ await BrowserRecipe.pgpBlockCheck(t, pgpBlocks[1], {
content: ['this is message 1 for flowcrypt issue 4342'],
unexpectedContent: ['this is message 1 CORRUPTED for flowcrypt issue 4342'],
encryption: 'not encrypted',
signature: 'signed',
});
+ await BrowserRecipe.pgpBlockCheck(t, pgpBlocks[2], {
+ content: ['this is message 2 for flowcrypt issue 4342'],
+ encryption: 'not encrypted',
+ signature: 'signed',
+ });
await gmailPage.close();
})
);
From 928fbbfb23997049719b9b766fc24cd2f96a3d56 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Wed, 12 Apr 2023 08:21:53 +0100
Subject: [PATCH 004/147] (wip) better attachment recognition
---
.../inbox-active-thread-module.ts | 16 +-
extension/js/common/browser/browser-msg.ts | 8 +-
extension/js/common/core/attachment.ts | 24 +-
.../js/common/core/crypto/pgp/msg-util.ts | 8 +-
extension/js/common/core/mime.ts | 67 +++--
extension/js/common/message-renderer.ts | 32 +--
.../webmail/gmail-element-replacer.ts | 271 +++++++++++-------
7 files changed, 252 insertions(+), 174 deletions(-)
diff --git a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
index f1e1b0e8f72..ea9ea417a4c 100644
--- a/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
+++ b/extension/chrome/settings/inbox/inbox-modules/inbox-active-thread-module.ts
@@ -13,12 +13,13 @@ import { Buf } from '../../../../js/common/core/buf.js';
import { Catch } from '../../../../js/common/platform/catch.js';
import { InboxView } from '../inbox.js';
import { Lang } from '../../../../js/common/lang.js';
-import { MessageRenderer } from '../../../../js/common/message-renderer.js';
+import { AttachmentBlock, MessageRenderer } from '../../../../js/common/message-renderer.js';
import { Ui } from '../../../../js/common/browser/ui.js';
import { ViewModule } from '../../../../js/common/view-module.js';
import { Xss } from '../../../../js/common/platform/xss.js';
import { Browser } from '../../../../js/common/browser/browser.js';
import { Attachment } from '../../../../js/common/core/attachment.js';
+import { MsgBlock } from '../../../../js/common/core/msg-block';
export class InboxActiveThreadModule extends ViewModule {
private threadId: string | undefined;
@@ -113,8 +114,17 @@ export class InboxActiveThreadModule extends ViewModule {
// todo: review the meaning of threadHasPgpBlock
this.threadHasPgpBlock ||= blocks.some(block => ['encryptedMsg', 'publicKey', 'privateKey', 'signedMsg'].includes(block.block.type));
// todo: take `from` from the processedMessage?
- const { renderedXssSafe, attachmentBlocks, isOutgoing } = MessageRenderer.renderMsg(
- { from, blocks },
+ const messageBlocks: MsgBlock[] = [];
+ const attachmentBlocks: AttachmentBlock[] = [];
+ for (const block of blocks) {
+ if (block.file && ['encryptedAttachment', 'plainAttachment'].includes(block.block.type)) {
+ attachmentBlocks.push({ block: block.block, file: block.file });
+ } else {
+ messageBlocks.push(block.block);
+ }
+ }
+ const { renderedXssSafe, isOutgoing } = MessageRenderer.renderMsg(
+ { from, blocks: messageBlocks },
this.view.factory,
this.view.showOriginal,
message.id,
diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts
index cacb24a0d67..88b126860b8 100644
--- a/extension/js/common/browser/browser-msg.ts
+++ b/extension/js/common/browser/browser-msg.ts
@@ -7,7 +7,7 @@ import { AjaxErr } from '../api/shared/api-error.js';
import { Buf } from '../core/buf.js';
import { Dict, Str, UrlParams } from '../core/common.js';
import { ArmoredKeyIdentityWithEmails, KeyUtil } from '../core/crypto/key.js';
-import { DecryptResult, DiagnoseMsgPubkeysResult, MsgUtil, PgpMsgMethod, PgpMsgTypeResult, VerifyRes } from '../core/crypto/pgp/msg-util.js';
+import { DecryptResult, DiagnoseMsgPubkeysResult, MsgUtil, PgpMsgMethod, VerifyRes } from '../core/crypto/pgp/msg-util.js';
import { NotificationGroupType } from '../notifications.js';
import { Catch } from '../platform/catch.js';
import { AccountIndex, AcctStoreDict } from '../platform/store/acct-store.js';
@@ -76,7 +76,6 @@ export namespace Bm {
export type PgpMsgDecrypt = PgpMsgMethod.Arg.Decrypt;
export type PgpMsgDiagnoseMsgPubkeys = PgpMsgMethod.Arg.DiagnosePubkeys;
export type PgpMsgVerifyDetached = PgpMsgMethod.Arg.VerifyDetached;
- export type PgpMsgType = PgpMsgMethod.Arg.Type;
export type PgpKeyBinaryToArmored = { binaryKeysData: Uint8Array };
export type Ajax = { req: JQueryAjaxSettings; stack: string };
export type AjaxGmailAttachmentGetChunk = { acctEmail: string; msgId: string; attachmentId: string };
@@ -100,7 +99,6 @@ export namespace Bm {
export type PgpMsgDecrypt = DecryptResult;
export type PgpMsgDiagnoseMsgPubkeys = DiagnoseMsgPubkeysResult;
export type PgpMsgVerify = VerifyRes;
- export type PgpMsgType = PgpMsgTypeResult;
export type PgpKeyBinaryToArmored = { keys: ArmoredKeyIdentityWithEmails[] };
export type AjaxGmailAttachmentGetChunk = { chunk: Buf };
export type _tab_ = { tabId: string | null | undefined }; // eslint-disable-line @typescript-eslint/naming-convention
@@ -117,7 +115,6 @@ export namespace Bm {
| PgpMsgDecrypt
| PgpMsgDiagnoseMsgPubkeys
| PgpMsgVerify
- | PgpMsgType
| InMemoryStoreGet
| InMemoryStoreSet
| StoreAcctGet
@@ -161,7 +158,6 @@ export namespace Bm {
| PgpMsgDecrypt
| PgpMsgDiagnoseMsgPubkeys
| PgpMsgVerifyDetached
- | PgpMsgType
| Ajax
| ShowAttachmentPreview
| ReRenderRecipient
@@ -227,7 +223,6 @@ export class BrowserMsg {
pgpMsgDecrypt: (bm: Bm.PgpMsgDecrypt) => BrowserMsg.sendAwait(undefined, 'pgpMsgDecrypt', bm, true) as Promise,
pgpMsgVerifyDetached: (bm: Bm.PgpMsgVerifyDetached) =>
BrowserMsg.sendAwait(undefined, 'pgpMsgVerifyDetached', bm, true) as Promise,
- pgpMsgType: (bm: Bm.PgpMsgType) => BrowserMsg.sendAwait(undefined, 'pgpMsgType', bm, true) as Promise,
pgpKeyBinaryToArmored: (bm: Bm.PgpKeyBinaryToArmored) =>
BrowserMsg.sendAwait(undefined, 'pgpKeyBinaryToArmored', bm, true) as Promise,
saveFetchedPubkeys: (bm: Bm.SaveFetchedPubkeys) =>
@@ -330,7 +325,6 @@ export class BrowserMsg {
BrowserMsg.bgAddListener('pgpMsgDiagnosePubkeys', MsgUtil.diagnosePubkeys);
BrowserMsg.bgAddListener('pgpMsgDecrypt', MsgUtil.decryptMessage);
BrowserMsg.bgAddListener('pgpMsgVerifyDetached', MsgUtil.verifyDetached);
- BrowserMsg.bgAddListener('pgpMsgType', async (r: Bm.PgpMsgType) => MsgUtil.type(r));
BrowserMsg.bgAddListener('saveFetchedPubkeys', saveFetchedPubkeysIfNewerThanInStorage);
BrowserMsg.bgAddListener('pgpKeyBinaryToArmored', async (r: Bm.PgpKeyBinaryToArmored) => ({
keys: await KeyUtil.parseAndArmorKeys(r.binaryKeysData),
diff --git a/extension/js/common/core/attachment.ts b/extension/js/common/core/attachment.ts
index 19bf62f1764..dfd0f871791 100644
--- a/extension/js/common/core/attachment.ts
+++ b/extension/js/common/core/attachment.ts
@@ -5,7 +5,17 @@
import { Buf } from './buf.js';
import { Str } from './common.js';
-type Attachment$treatAs = 'publicKey' | 'privateKey' | 'encryptedMsg' | 'hidden' | 'signature' | 'encryptedFile' | 'plainFile' | 'inlineImage';
+export type Attachment$treatAs =
+ | 'publicKey'
+ | 'privateKey'
+ | 'encryptedMsg'
+ | 'hidden'
+ | 'signature'
+ | 'encryptedFile'
+ | 'plainFile'
+ | 'inlineImage'
+ | 'needChunk'
+ | 'maybePgp';
type ContentTransferEncoding = '7bit' | 'quoted-printable' | 'base64';
export type AttachmentMeta = {
data?: Uint8Array;
@@ -101,6 +111,9 @@ export class Attachment {
return `f_${Str.sloppyRandom(30)}@flowcrypt`;
};
+ /** @deprecated attachment and pgp_block frames won't be performing this analysis
+ *
+ */
public isPublicKey = (): boolean => {
if (this.treatAsValue) {
return this.treatAsValue === 'publicKey';
@@ -173,9 +186,14 @@ export class Attachment {
return 'publicKey';
} else if (this.name.match(/(cryptup|flowcrypt)-backup-[a-z0-9]+\.(key|asc)$/g)) {
return 'privateKey';
- } else if (this.name.match(/\.asc$/) && this.length < 100000 && !this.inline) {
- return 'encryptedMsg';
} else {
+ // && !Attachment.encryptedMsgNames.includes(this.name) -- already checked above
+ const isAmbiguousAscFile = /\.asc$/.test(this.name); // ambiguous .asc name
+ const isAmbiguousNonameFile = !this.name || this.name === 'noname'; // may not even be OpenPGP related
+ // todo: do we know length before fetching?
+ if (!this.inline && this.length < 100000 && (isAmbiguousAscFile || isAmbiguousNonameFile)) {
+ return this.hasData() ? 'maybePgp' : 'needChunk';
+ }
return 'plainFile';
}
};
diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts
index 64bd17385fe..38c8d2466b8 100644
--- a/extension/js/common/core/crypto/pgp/msg-util.ts
+++ b/extension/js/common/core/crypto/pgp/msg-util.ts
@@ -34,7 +34,7 @@ export namespace PgpMsgMethod {
armor: boolean;
date?: Date;
};
- export type Type = { data: Uint8Array | string };
+ export type Type = { data: Uint8Array };
export type Decrypt = {
kisWithPp: KeyInfoWithIdentityAndOptionalPp[];
encryptedData: Uint8Array | string;
@@ -122,12 +122,6 @@ export class MsgUtil {
if (!data || !data.length) {
return undefined;
}
- if (typeof data === 'string') {
- // Uint8Array sent over BrowserMsg gets converted to blobs on the sending side, and read on the receiving side
- // Firefox blocks such blobs from content scripts to background, see: https://github.com/FlowCrypt/flowcrypt-browser/issues/2587
- // that's why we add an option to send data as a base64 formatted string
- data = Buf.fromBase64Str(data);
- }
const firstByte = data[0];
// attempt to understand this as a binary PGP packet: https://tools.ietf.org/html/rfc4880#section-4.2
if ((firstByte & 0b10000000) === 0b10000000) {
diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts
index f1d85420125..2a9970bf531 100644
--- a/extension/js/common/core/mime.ts
+++ b/extension/js/common/core/mime.ts
@@ -22,12 +22,16 @@ const Iso88592 = requireIso88592();
type AddressHeader = { address: string; name: string };
type MimeContentHeader = string | AddressHeader[];
-export type MimeContent = {
+
+export type MimeContentBody = {
+ html?: string;
+ text?: string;
+};
+
+export type MimeContent = MimeContentBody & {
attachments: Attachment[];
rawSignedContent?: string;
subject?: string;
- html?: string;
- text?: string;
};
export type MimeContentWithHeaders = MimeContent & {
@@ -60,51 +64,58 @@ export type MimeProccesedFromRawMsg = MimeProccesedMsg & {
type SendingType = 'to' | 'cc' | 'bcc';
export class Mime {
- public static processDecoded = (decoded: MimeContent): MimeProccesedMsg => {
- const blocksFromBody: MsgBlock[] = [];
+ public static processBody = (decoded: MimeContentBody): MsgBlock[] => {
+ const blocks: MsgBlock[] = [];
if (decoded.text) {
const blocksFromTextPart = MsgBlockParser.detectBlocks(Str.normalize(decoded.text)).blocks;
// if there are some encryption-related blocks found in the text section, which we can use, and not look at the html section
if (blocksFromTextPart.find(b => ['pkcs7', 'encryptedMsg', 'signedMsg', 'publicKey', 'privateKey'].includes(b.type))) {
- blocksFromBody.push(...blocksFromTextPart); // because the html most likely containt the same thing, just harder to parse pgp sections cause it's html
+ blocks.push(...blocksFromTextPart); // because the html most likely containt the same thing, just harder to parse pgp sections cause it's html
} else if (decoded.html) {
// if no pgp blocks found in text part and there is html part, prefer html
- blocksFromBody.push(MsgBlock.fromContent('plainHtml', decoded.html));
+ blocks.push(MsgBlock.fromContent('plainHtml', decoded.html));
} else {
// else if no html and just a plain text message, use that
- blocksFromBody.push(...blocksFromTextPart);
+ blocks.push(...blocksFromTextPart);
}
} else if (decoded.html) {
- blocksFromBody.push(MsgBlock.fromContent('plainHtml', decoded.html));
+ blocks.push(MsgBlock.fromContent('plainHtml', decoded.html));
}
- const blocks: { block: MsgBlock; file?: Attachment }[] = blocksFromBody.map(block => {
- return { block };
- });
+ return blocks;
+ };
+
+ public static processAttachments = (messageBlocks: MsgBlock[], decoded: MimeContent): MimeProccesedMsg => {
+ const attachmentBlocks: { block: MsgBlock; file: Attachment }[] = [];
const signatureAttachments: Attachment[] = [];
for (const file of decoded.attachments) {
- const isBodyEmpty = decoded.text === '' || decoded.text === '\n';
- const treatAs = file.treatAs(decoded.attachments, isBodyEmpty);
+ const isBodyEmpty = decoded.text === '' || decoded.text === '\n'; // todo:
+ let treatAs = file.treatAs(decoded.attachments, isBodyEmpty);
+ if (['needChunk', 'maybePgp'].includes(treatAs)) {
+ // don't want to reference MsgUtil and OpenPGP.js here, so
+ // todo: think about refactoring this
+ treatAs = 'encryptedMsg'; // publicKey?
+ }
if (treatAs === 'encryptedMsg') {
const armored = PgpArmor.clip(file.getData().toUtfStr());
if (armored) {
- blocks.push({ block: MsgBlock.fromContent('encryptedMsg', armored), file });
+ attachmentBlocks.push({ block: MsgBlock.fromContent('encryptedMsg', armored), file });
}
} else if (treatAs === 'signature') {
signatureAttachments.push(file);
} else if (treatAs === 'publicKey') {
- blocks.push(
+ attachmentBlocks.push(
...MsgBlockParser.detectBlocks(file.getData().toUtfStr()).blocks.map(block => {
return { block, file }; // todo: test when more than one
})
);
} else if (treatAs === 'privateKey') {
- blocks.push(
+ attachmentBlocks.push(
...MsgBlockParser.detectBlocks(file.getData().toUtfStr()).blocks.map(block => {
return { block, file }; // todo: test when more than one
})
);
} else if (treatAs === 'encryptedFile') {
- blocks.push({
+ attachmentBlocks.push({
// todo: fromAttachment return complete record?
block: MsgBlock.fromAttachment('encryptedAttachment', '', {
name: file.name,
@@ -117,7 +128,7 @@ export class Mime {
file,
});
} else if (treatAs === 'plainFile') {
- blocks.push({
+ attachmentBlocks.push({
block: MsgBlock.fromAttachment('plainAttachment', '', {
name: file.name,
type: file.type,
@@ -137,7 +148,7 @@ export class Mime {
// todo: data may not be present
if (signatureAttachment.hasData()) {
const signature = signatureAttachment.getData().toUtfStr();
- for (const block of blocks) {
+ for (const block of attachmentBlocks) {
if (block.block.type === 'plainText') {
block.block.type = 'signedText';
block.block.signature = signature;
@@ -146,9 +157,9 @@ export class Mime {
block.block.signature = signature;
}
}
- if (!blocks.find(block => ['plainText', 'plainHtml', 'signedMsg', 'signedHtml', 'signedText'].includes(block.block.type))) {
+ if (!attachmentBlocks.find(block => ['plainText', 'plainHtml', 'signedMsg', 'signedHtml', 'signedText'].includes(block.block.type))) {
// signed an empty message
- blocks.push({ block: new MsgBlock('signedMsg', '', true, signature), file: signatureAttachment });
+ attachmentBlocks.push({ block: new MsgBlock('signedMsg', '', true, signature), file: signatureAttachment });
}
} else {
// debugger;
@@ -156,11 +167,21 @@ export class Mime {
}
}
return {
- blocks,
+ blocks: [
+ ...messageBlocks.map(block => {
+ return { block };
+ }),
+ ...attachmentBlocks,
+ ],
rawSignedContent: decoded.rawSignedContent,
};
};
+ public static processDecoded = (decoded: MimeContent): MimeProccesedMsg => {
+ const bodyBlocks = Mime.processBody(decoded);
+ return Mime.processAttachments(bodyBlocks, decoded);
+ };
+
public static process = async (mimeMsg: Uint8Array): Promise => {
const decoded = await Mime.decode(mimeMsg);
return {
diff --git a/extension/js/common/message-renderer.ts b/extension/js/common/message-renderer.ts
index 867b5715bf0..03696367c13 100644
--- a/extension/js/common/message-renderer.ts
+++ b/extension/js/common/message-renderer.ts
@@ -12,9 +12,7 @@ import { SendAsAlias } from './platform/store/acct-store.js';
import { Xss } from './platform/xss.js';
import { XssSafeFactory } from './xss-safe-factory.js';
-export type ProccesedMsg = MimeProccesedMsg & {
- from?: string;
-};
+export type ProccesedMsg = MimeProccesedMsg;
export type AttachmentBlock = {
block: MsgBlock;
@@ -23,7 +21,7 @@ export type AttachmentBlock = {
export class MessageRenderer {
public static renderMsg = (
- { from, blocks }: { blocks: { block: MsgBlock; file?: Attachment }[]; from?: string },
+ { from, blocks }: { blocks: MsgBlock[]; from?: string },
factory: XssSafeFactory,
showOriginal: boolean,
msgId: string, // todo: will be removed
@@ -31,48 +29,40 @@ export class MessageRenderer {
) => {
const isOutgoing = Boolean(from && !!sendAs?.[from]);
let r = '';
- const attachmentBlocks: AttachmentBlock[] = [];
for (const block of blocks) {
if (r) {
r += '
';
}
- if (['encryptedAttachment', 'plainAttachment'].includes(block.block.type) && !block.file) {
- // debugger;
- throw new Error('Unexpected!');
- }
- if (block.file && ['encryptedAttachment', 'plainAttachment'].includes(block.block.type)) {
- attachmentBlocks.push({ block: block.block, file: block.file });
- } else if (showOriginal) {
- r += Xss.escape(Str.with(block.block.content)).replace(/\n/g, '
');
+ if (showOriginal) {
+ r += Xss.escape(Str.with(block.content)).replace(/\n/g, '
');
} else {
- r += XssSafeFactory.renderableMsgBlock(factory, block.block, msgId, from || 'unknown', isOutgoing);
+ r += XssSafeFactory.renderableMsgBlock(factory, block, msgId, from || 'unknown', isOutgoing);
}
}
- return { renderedXssSafe: r, attachmentBlocks, isOutgoing };
+ return { renderedXssSafe: r, isOutgoing };
};
+ /* todo: remove
public static process = async (gmailMsg: GmailRes.GmailMsg): Promise => {
- const processedMsg = gmailMsg.raw ? await MessageRenderer.processMessageFromRaw(gmailMsg.raw) : MessageRenderer.processMessageFromFull(gmailMsg);
- return { from: GmailParser.findHeader(gmailMsg, 'from'), ...processedMsg };
- };
+ return gmailMsg.raw ? await MessageRenderer.processMessageFromRaw(gmailMsg.raw) : MessageRenderer.processMessageFromFull(gmailMsg);
+ }; */
public static processMessageFromRaw = async (raw: string) => {
const mimeMsg = Buf.fromBase64UrlStr(raw);
return await Mime.process(mimeMsg);
};
- private static processMessageFromFull = (gmailMsg: GmailRes.GmailMsg) => {
+ public static reconstructMimeContent = (gmailMsg: GmailRes.GmailMsg): MimeContent => {
const bodies = GmailParser.findBodies(gmailMsg);
const attachments = GmailParser.findAttachments(gmailMsg);
const text = bodies['text/plain'] ? Buf.fromBase64UrlStr(bodies['text/plain']).toUtfStr() : undefined;
// todo: do we need to strip?
const html = bodies['text/html'] ? Xss.htmlSanitizeAndStripAllTags(Buf.fromBase64UrlStr(bodies['text/html']).toUtfStr(), '\n') : undefined;
// reconstructed MIME content
- const mimeContent: MimeContent = {
+ return {
text,
html,
attachments,
};
- return Mime.processDecoded(mimeContent);
};
}
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 7cd2e359306..46d96acc3c3 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -8,7 +8,7 @@ import { GmailParser, GmailRes } from '../../common/api/email-provider/gmail/gma
import { IntervalFunction, WebmailElementReplacer } from './setup-webmail-content-script.js';
import { AjaxErr } from '../../common/api/shared/api-error.js';
import { ApiErr } from '../../common/api/shared/api-error.js';
-import { Attachment } from '../../common/core/attachment.js';
+import { Attachment, Attachment$treatAs } from '../../common/core/attachment.js';
import { BrowserMsg } from '../../common/browser/browser-msg.js';
import { Catch } from '../../common/platform/catch.js';
import { GlobalStore, LocalDraft } from '../../common/platform/store/global-store.js';
@@ -25,11 +25,13 @@ import { SendAsAlias } from '../../common/platform/store/acct-store.js';
// todo: can we somehow define a purely relay class for ContactStore to clearly show that crypto-libraries are not loaded and can't be used?
import { ContactStore } from '../../common/platform/store/contact-store.js';
import { Buf } from '../../common/core/buf.js';
-import { MessageRenderer, AttachmentBlock } from '../../common/message-renderer.js';
+import { MessageRenderer } from '../../common/message-renderer.js';
+import { Mime } from '../../common/core/mime.js';
+import { MsgUtil } from '../../common/core/crypto/pgp/msg-util.js';
type JQueryEl = JQuery;
-type ProcessedMessage = { renderedXssSafe?: string; attachmentBlocks: AttachmentBlock[] };
+type ProcessedMessage = { renderedXssSafe?: string; attachments: Attachment[] };
interface MessageCacheEntry {
full: Promise;
@@ -43,6 +45,8 @@ export class GmailElementReplacer implements WebmailElementReplacer {
private recipientHasPgpCache: Dict = {};
private sendAs: Dict;
private messages: Dict = {};
+ private chunkDownloads: { attachment: Attachment; result: Promise }[];
+ // private attachmentDownloads: { attachment: Attachment; result: Promise }[];
private factory: XssSafeFactory;
private clientConfiguration: ClientConfiguration;
private pubLookup: PubLookup;
@@ -170,6 +174,25 @@ export class GmailElementReplacer implements WebmailElementReplacer {
return msgDownload;
};
+ private queueAttachmentChunkDownload = (a: Attachment) => {
+ if (a.hasData()) {
+ return { attachment: a, result: Promise.resolve(a.getData()) };
+ }
+ let download = this.chunkDownloads.find(d => d.attachment === a);
+ if (!download) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ download = { attachment: a, result: this.gmail.attachmentGetChunk(a.msgId!, a.id!) };
+ this.chunkDownloads.push(download);
+ }
+ return download;
+ };
+
+ /*
+ private queueAttachmentDownload = (a: Attachment) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.attachmentDownloads.push({ attachment: a, result: this.gmail.attachmentGet(a.msgId!, a.id!) });
+ };
+ */
private msgGetProcessed = async (msgId: string): Promise => {
// todo: retries? exceptions?
const msgDownload = this.msgGetCached(msgId);
@@ -177,13 +200,31 @@ export class GmailElementReplacer implements WebmailElementReplacer {
return msgDownload.processedFull;
}
const msg = await msgDownload.full;
- const { blocks, from } = await MessageRenderer.process(msg);
- if (blocks.length === 1 && blocks[0].block.type === 'plainText') {
+ const mimeContent = MessageRenderer.reconstructMimeContent(msg);
+ const blocks = Mime.processBody(mimeContent);
+ // todo: only start `signature` download?
+ // start download of all attachments that are not plainFile, for 'needChunk' -- chunked download
+ for (const a of mimeContent.attachments.filter(a => !a.hasData())) {
+ const treatAs = a.treatAs(mimeContent.attachments); // todo: isBodyEmpty
+ if (treatAs === 'plainFile') continue;
+ if (treatAs === 'needChunk') {
+ this.queueAttachmentChunkDownload(a);
+ } else if (treatAs === 'publicKey') {
+ // we also want a chunk before we replace the attachment in the UI
+ // todo: or simply download in full?
+ this.queueAttachmentChunkDownload(a);
+ } else {
+ // todo: this.queueAttachmentDownload(a);
+ }
+ }
+ let renderedXssSafe: string | undefined;
+ if (blocks.length === 0 || (blocks.length === 1 && ['plainText', 'plainHtml'].includes(blocks[0].type))) {
// only has single block which is plain text
- msgDownload.processedFull = { attachmentBlocks: [] };
} else {
- msgDownload.processedFull = MessageRenderer.renderMsg({ blocks, from }, this.factory, false, msgId, this.sendAs);
+ const from = GmailParser.findHeader(msg, 'from');
+ ({ renderedXssSafe } = MessageRenderer.renderMsg({ blocks, from }, this.factory, false, msgId, this.sendAs));
}
+ msgDownload.processedFull = { renderedXssSafe, attachments: mimeContent.attachments };
return msgDownload.processedFull;
};
@@ -420,11 +461,11 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (this.debug) {
console.debug('processNewPgpAttachments() -> msgGet may take some time');
}
- const msg = await this.msgGetCached(msgId).full; // todo: msgGetProcessed
+ const { attachments } = await this.msgGetProcessed(msgId);
if (this.debug) {
- console.debug('processNewPgpAttachments() -> msgGet done -> processAttachments', msg);
+ console.debug('processNewPgpAttachments() -> msgGet done -> processAttachments', attachments);
}
- await this.processAttachments(msgId, GmailParser.findAttachments(msg), attachmentsContainer, false);
+ await this.processAttachments(msgId, attachments, attachmentsContainer, false);
} catch (e) {
if (ApiErr.isAuthErr(e)) {
this.notifications.showAuthPopupNeeded(this.acctEmail);
@@ -446,101 +487,44 @@ export class GmailElementReplacer implements WebmailElementReplacer {
private processAttachments = async (
msgId: string,
- attachmentMetas: Attachment[],
+ attachmentMetas: Attachment[], // todo: these are not Metas!
attachmentsContainerInner: JQueryEl | HTMLElement,
skipGoogleDrive: boolean
) => {
if (this.debug) {
console.debug('processAttachments()', attachmentMetas);
}
- let msgEl = this.getMsgBodyEl(msgId); // not a constant because sometimes elements get replaced, then returned by the function that replaced them
- const isBodyEmpty = msgEl.text() === '' || msgEl.text() === '\n';
+ const msgEl = this.getMsgBodyEl(msgId); // not a constant because sometimes elements get replaced, then returned by the function that replaced them
+ const isBodyEmpty = msgEl.text() === '' || msgEl.text() === '\n'; // todo:
const senderEmail = this.getSenderEmail(msgEl);
const isOutgoing = !!this.sendAs[senderEmail];
attachmentsContainerInner = $(attachmentsContainerInner);
attachmentsContainerInner.parent().find(this.sel.numberOfAttachments).hide();
let nRenderedAttachments = attachmentMetas.length;
for (const a of attachmentMetas) {
- const treatAs = a.treatAs(attachmentMetas, isBodyEmpty);
- // todo - [same name + not processed].first() ... What if attachment metas are out of order compared to how gmail shows it? And have the same name?
const attachmentSel = this.filterAttachments(
attachmentsContainerInner.children().not('.attachment_processed'),
new RegExp(`^${Str.regexEscape(a.name || 'noname')}$`)
).first();
- if (this.debug) {
- console.debug('processAttachments() treatAs');
- }
- try {
- if (treatAs !== 'plainFile') {
- this.hideAttachment(attachmentSel, attachmentsContainerInner);
- nRenderedAttachments--;
- if (treatAs === 'encryptedFile') {
- // actual encrypted attachment - show it
- attachmentsContainerInner.prepend(this.factory.embeddedAttachment(a, true)); // xss-safe-factory
- nRenderedAttachments++;
- } else if (treatAs === 'encryptedMsg') {
- const isAmbiguousAscFile = a.name.substr(-4) === '.asc' && !Attachment.encryptedMsgNames.includes(a.name); // ambiguous .asc name
- const isAmbiguousNonameFile = !a.name || a.name === 'noname'; // may not even be OpenPGP related
- if (isAmbiguousAscFile || isAmbiguousNonameFile) {
- // Inspect a chunk
- if (this.debug) {
- console.debug('processAttachments() try -> awaiting chunk + awaiting type');
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const data = await this.gmail.attachmentGetChunk(msgId, a.id!); // .id is present when fetched from api
- const openpgpType = await BrowserMsg.send.bg.await.pgpMsgType({ data: data.toBase64Str() }); // base64 for FF, see #2587
- if (openpgpType && openpgpType.type === 'publicKey' && openpgpType.armored) {
- // if it looks like OpenPGP public key
- nRenderedAttachments = await this.renderPublicKeyFromFile(a, attachmentsContainerInner, msgEl, isOutgoing, attachmentSel, nRenderedAttachments);
- } else if (openpgpType && ['encryptedMsg', 'signedMsg'].includes(openpgpType.type)) {
- msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'append', this.factory.embeddedMsg(openpgpType.type, '', msgId, false, senderEmail)); // xss-safe-factory
- } else {
- attachmentSel.show().children('.attachment_loader').text('Unknown OpenPGP format');
- nRenderedAttachments++;
- }
- if (this.debug) {
- console.debug('processAttachments() try -> awaiting done and processed');
- }
- } else {
- msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', this.factory.embeddedMsg('encryptedMsg', '', msgId, false, senderEmail)); // xss-safe-factory
- }
- } else if (treatAs === 'publicKey') {
- // todo - pubkey should be fetched in pgp_pubkey.js
- nRenderedAttachments = await this.renderPublicKeyFromFile(a, attachmentsContainerInner, msgEl, isOutgoing, attachmentSel, nRenderedAttachments);
- } else if (treatAs === 'privateKey') {
- nRenderedAttachments = await this.renderBackupFromFile(a, attachmentsContainerInner, msgEl, attachmentSel, nRenderedAttachments);
- } else if (treatAs === 'signature') {
- const embeddedSignedMsgXssSafe = this.factory.embeddedMsg('signedMsg', '', msgId, false, senderEmail, true);
- msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
- }
- } else if (treatAs === 'plainFile' && a.name.substr(-4) === '.asc') {
- // todo:
- // normal looking attachment ending with .asc
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const data = await this.gmail.attachmentGetChunk(msgId, a.id!); // .id is present when fetched from api
- const openpgpType = await BrowserMsg.send.bg.await.pgpMsgType({ data: data.toBase64Str() }); // base64 for FF, see #2587
- if (openpgpType && openpgpType.type === 'publicKey' && openpgpType.armored) {
- // if it looks like OpenPGP public key
- nRenderedAttachments = await this.renderPublicKeyFromFile(a, attachmentsContainerInner, msgEl, isOutgoing, attachmentSel, nRenderedAttachments);
- this.hideAttachment(attachmentSel, attachmentsContainerInner);
- nRenderedAttachments--;
- } else {
- attachmentSel.addClass('attachment_processed').children('.attachment_loader').remove();
- }
- } else {
- // standard file
- attachmentSel.addClass('attachment_processed').children('.attachment_loader').remove();
- }
- } catch (e) {
- if (!ApiErr.isSignificant(e) || (e instanceof AjaxErr && e.status === 200)) {
- attachmentSel.show().children('.attachment_loader').text('Categorize: net err');
- nRenderedAttachments++;
- } else {
- Catch.reportErr(e);
- attachmentSel.show().children('.attachment_loader').text('Categorize: unknown err');
- nRenderedAttachments++;
- }
+ // todo: shouldn't call `treatAs` in too many places ?
+ const renderStatus = await this.processAttachment(
+ a,
+ a.treatAs(attachmentMetas, isBodyEmpty),
+ attachmentSel,
+ attachmentsContainerInner,
+ msgEl,
+ msgId,
+ senderEmail,
+ isOutgoing
+ );
+ if (renderStatus === 'hidden') {
+ nRenderedAttachments--;
}
+ // if (renderStatus === 'shown') attachmentSel.show();
+ }
+ if (nRenderedAttachments !== attachmentMetas.length) {
+ // according to #4200, no point in showing "download all" button if at least one attachment is encrypted etc.
+ $(this.sel.attachmentsButtons).hide();
}
if (nRenderedAttachments >= 2) {
// Aligned with Gmail, the label is shown only if there are 2 or more attachments
@@ -555,6 +539,82 @@ export class GmailElementReplacer implements WebmailElementReplacer {
}
};
+ private processAttachment = async (
+ a: Attachment,
+ treatAs: Attachment$treatAs,
+ attachmentSel: JQueryEl,
+ attachmentsContainerInner: JQueryEl,
+ msgEl: JQueryEl,
+ msgId: string, // deprecated
+ senderEmail: string, // deprecated?
+ isOutgoing: boolean // deprecated
+ ): Promise<'shown' | 'replaced' | 'hidden'> => {
+ // todo - [same name + not processed].first() ... What if attachment metas are out of order compared to how gmail shows it? And have the same name?
+ try {
+ if (['needChunk', 'maybePgp', 'publicKey'].includes(treatAs)) {
+ // todo: this isn't the best way to do this
+ // todo: move into a handler
+ // Inspect a chunk
+ if (this.debug) {
+ console.debug('processAttachments() try -> awaiting chunk + awaiting type');
+ }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const data = await this.queueAttachmentChunkDownload(a).result;
+ const openpgpType = MsgUtil.type({ data });
+ if (openpgpType && openpgpType.type === 'publicKey' && openpgpType.armored) {
+ // if it looks like OpenPGP public key
+ treatAs = 'publicKey';
+ } else if (openpgpType && ['encryptedMsg', 'signedMsg'].includes(openpgpType.type)) {
+ treatAs = 'encryptedMsg'; // todo: signedMsg ?
+ } else {
+ attachmentSel.show().children('.attachment_loader').text('Unknown OpenPGP format');
+ if (this.debug) {
+ console.debug("processAttachments() try -> awaiting done and processed -- doesn't look like OpenPGP");
+ }
+ return 'shown';
+ }
+ if (this.debug) {
+ console.debug('processAttachments() try -> awaiting done and processed');
+ }
+ }
+ if (treatAs !== 'plainFile') {
+ this.hideAttachment(attachmentSel, attachmentsContainerInner);
+ }
+ if (treatAs === 'encryptedFile') {
+ // actual encrypted attachment - show it
+ attachmentsContainerInner.prepend(this.factory.embeddedAttachment(a, true)); // xss-safe-factory
+ return 'replaced'; // native should be hidden, custom should appear instead
+ } else if (treatAs === 'encryptedMsg') {
+ msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', this.factory.embeddedMsg('encryptedMsg', '', msgId, false, senderEmail)); // xss-safe-factory
+ return 'hidden'; // native attachment should be hidden, the "attachment" goes to the message container
+ } else if (treatAs === 'publicKey') {
+ // todo - pubkey should be fetched in pgp_pubkey.js
+ return await this.renderPublicKeyFromFile(a, attachmentsContainerInner, msgEl, isOutgoing, attachmentSel);
+ } else if (treatAs === 'privateKey') {
+ return await this.renderBackupFromFile(a, attachmentsContainerInner, msgEl);
+ } else if (treatAs === 'signature') {
+ const embeddedSignedMsgXssSafe = this.factory.embeddedMsg('signedMsg', '', msgId, false, senderEmail, true);
+ msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
+ return 'hidden'; // native attachment should be hidden, the "attachment" goes to the message container
+ } else {
+ // standard file
+ attachmentSel.show().addClass('attachment_processed').children('.attachment_loader').remove();
+ return 'shown';
+ }
+ } catch (e) {
+ if (!ApiErr.isSignificant(e) || (e instanceof AjaxErr && e.status === 200)) {
+ // todo: show() not needed?
+ attachmentSel.show().children('.attachment_loader').text('Categorize: net err');
+ return 'shown';
+ } else {
+ Catch.reportErr(e);
+ // todo: show() not needed?
+ attachmentSel.show().children('.attachment_loader').text('Categorize: unknown err');
+ return 'shown';
+ }
+ }
+ };
+
private processGoogleDriveAttachments = async (msgId: string, msgEl: JQueryEl, attachmentsContainerInner: JQueryEl) => {
const notProcessedAttachmentsLoaders = attachmentsContainerInner.find('.attachment_loader');
if (notProcessedAttachmentsLoaders.length && msgEl.find('.gmail_drive_chip, a[href^="https://drive.google.com/file"]').length) {
@@ -573,6 +633,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
treatAs: 'encryptedFile',
})
);
+ // todo: start download
} else {
console.info('Missing Google Drive attachments download_url');
}
@@ -586,48 +647,40 @@ export class GmailElementReplacer implements WebmailElementReplacer {
attachmentsContainerInner: JQueryEl,
msgEl: JQueryEl,
isOutgoing: boolean,
- attachmentSel: JQueryEl,
- nRenderedAttachments: number
- ) => {
+ attachmentSel: JQueryEl
+ ): Promise<'hidden' | 'shown'> => {
let downloadedAttachment: GmailRes.GmailAttachment;
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
downloadedAttachment = await this.gmail.attachmentGet(attachmentMeta.msgId!, attachmentMeta.id!); // .id! is present when fetched from api
} catch (e) {
attachmentsContainerInner.show().addClass('attachment_processed').find('.attachment_loader').text('Please reload page');
- nRenderedAttachments++;
- return nRenderedAttachments;
+ // todo: attachmentSel.show()? unit-test this case?
+ return 'shown';
}
- const openpgpType = await BrowserMsg.send.bg.await.pgpMsgType({
- data: Buf.fromUint8(downloadedAttachment.data.subarray(0, 1000)).toBase64Str(),
- }); // base64 for FF, see #2587
+ const openpgpType = MsgUtil.type({ data: downloadedAttachment.data.subarray(0, 1000) });
if (openpgpType && openpgpType.type === 'publicKey') {
this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'after', this.factory.embeddedPubkey(downloadedAttachment.data.toUtfStr(), isOutgoing)); // xss-safe-factory
+ return 'hidden';
} else {
attachmentSel.show().addClass('attachment_processed').children('.attachment_loader').text('Unknown Public Key Format');
- nRenderedAttachments++;
+ return 'shown';
}
- return nRenderedAttachments;
};
- private renderBackupFromFile = async (
- attachmentMeta: Attachment,
- attachmentsContainerInner: JQueryEl,
- msgEl: JQueryEl,
- attachmentSel: JQueryEl,
- nRenderedAttachments: number
- ) => {
+ private renderBackupFromFile = async (attachmentMeta: Attachment, attachmentsContainerInner: JQueryEl, msgEl: JQueryEl) => {
let downloadedAttachment: GmailRes.GmailAttachment;
try {
+ // todo: fetch from queue
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
downloadedAttachment = await this.gmail.attachmentGet(attachmentMeta.msgId!, attachmentMeta.id!); // .id! is present when fetched from api
} catch (e) {
+ // todo: do we really need show() for attachmentsContainerInner?
attachmentsContainerInner.show().addClass('attachment_processed').find('.attachment_loader').text('Please reload page');
- nRenderedAttachments++;
- return nRenderedAttachments;
+ return 'shown';
}
this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'append', this.factory.embeddedBackup(downloadedAttachment.data.toUtfStr())); // xss-safe-factory
- return nRenderedAttachments;
+ return 'hidden';
};
private filterAttachments = (potentialMatches: JQueryEl | HTMLElement, regExp: RegExp) => {
@@ -648,8 +701,6 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (!attachmentEl.length) {
attachmentsContainerSel.children('.attachment_loader').text('Missing file info');
}
- // according to #4200, no point in showing "download all" button if at least one attachment is encrypted etc.
- $(this.sel.attachmentsButtons).hide();
};
private determineMsgId = (innerMsgEl: HTMLElement | JQueryEl) => {
From 38ee474401fefff1718c90d129aed06d1674add1 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Wed, 12 Apr 2023 13:06:41 +0100
Subject: [PATCH 005/147] lint fix
---
.../webmail/gmail-element-replacer.ts | 24 +++++++++++--------
test/source/patterns.ts | 2 +-
2 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 46d96acc3c3..5711b9fb800 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -494,9 +494,9 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (this.debug) {
console.debug('processAttachments()', attachmentMetas);
}
- const msgEl = this.getMsgBodyEl(msgId); // not a constant because sometimes elements get replaced, then returned by the function that replaced them
- const isBodyEmpty = msgEl.text() === '' || msgEl.text() === '\n'; // todo:
- const senderEmail = this.getSenderEmail(msgEl);
+ const msgElReference = { msgEl: this.getMsgBodyEl(msgId) };
+ const isBodyEmpty = msgElReference.msgEl.text() === '' || msgElReference.msgEl.text() === '\n'; // todo:
+ const senderEmail = this.getSenderEmail(msgElReference.msgEl);
const isOutgoing = !!this.sendAs[senderEmail];
attachmentsContainerInner = $(attachmentsContainerInner);
attachmentsContainerInner.parent().find(this.sel.numberOfAttachments).hide();
@@ -512,7 +512,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
a.treatAs(attachmentMetas, isBodyEmpty),
attachmentSel,
attachmentsContainerInner,
- msgEl,
+ msgElReference,
msgId,
senderEmail,
isOutgoing
@@ -535,7 +535,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
attachmentsContainerInner.parents(this.sel.attachmentsContainerOuter).first().hide();
}
if (!skipGoogleDrive) {
- await this.processGoogleDriveAttachments(msgId, msgEl, attachmentsContainerInner);
+ await this.processGoogleDriveAttachments(msgId, msgElReference.msgEl, attachmentsContainerInner);
}
};
@@ -544,7 +544,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
treatAs: Attachment$treatAs,
attachmentSel: JQueryEl,
attachmentsContainerInner: JQueryEl,
- msgEl: JQueryEl,
+ msgElReference: { msgEl: JQueryEl },
msgId: string, // deprecated
senderEmail: string, // deprecated?
isOutgoing: boolean // deprecated
@@ -585,16 +585,20 @@ export class GmailElementReplacer implements WebmailElementReplacer {
attachmentsContainerInner.prepend(this.factory.embeddedAttachment(a, true)); // xss-safe-factory
return 'replaced'; // native should be hidden, custom should appear instead
} else if (treatAs === 'encryptedMsg') {
- msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', this.factory.embeddedMsg('encryptedMsg', '', msgId, false, senderEmail)); // xss-safe-factory
+ msgElReference.msgEl = /* xss-safe-factory */ this.updateMsgBodyEl_DANGEROUSLY(
+ msgElReference.msgEl,
+ 'set',
+ this.factory.embeddedMsg('encryptedMsg', '', msgId, false, senderEmail)
+ );
return 'hidden'; // native attachment should be hidden, the "attachment" goes to the message container
} else if (treatAs === 'publicKey') {
// todo - pubkey should be fetched in pgp_pubkey.js
- return await this.renderPublicKeyFromFile(a, attachmentsContainerInner, msgEl, isOutgoing, attachmentSel);
+ return await this.renderPublicKeyFromFile(a, attachmentsContainerInner, msgElReference.msgEl, isOutgoing, attachmentSel);
} else if (treatAs === 'privateKey') {
- return await this.renderBackupFromFile(a, attachmentsContainerInner, msgEl);
+ return await this.renderBackupFromFile(a, attachmentsContainerInner, msgElReference.msgEl);
} else if (treatAs === 'signature') {
const embeddedSignedMsgXssSafe = this.factory.embeddedMsg('signedMsg', '', msgId, false, senderEmail, true);
- msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
+ msgElReference.msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgElReference.msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
return 'hidden'; // native attachment should be hidden, the "attachment" goes to the message container
} else {
// standard file
diff --git a/test/source/patterns.ts b/test/source/patterns.ts
index d4baa8f711b..f84c841ccf0 100644
--- a/test/source/patterns.ts
+++ b/test/source/patterns.ts
@@ -26,7 +26,7 @@ const getAllFilesInDir = (dir: string, filePattern: RegExp): string[] => {
};
const hasXssComment = (line: string) => {
- return /\/\/ xss-(known-source|direct|escaped|safe-factory|safe-value|sanitized|none|reinsert|dangerous-function)/.test(line);
+ return /\/[\/\*] xss-(known-source|direct|escaped|safe-factory|safe-value|sanitized|none|reinsert|dangerous-function)/.test(line);
};
const hasErrHandledComment = (line: string) => {
From fad9db3a63a3d882b814801881cdd79d1b781d65 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Thu, 13 Apr 2023 16:39:16 +0100
Subject: [PATCH 006/147] extended test for #5029 for GmailElementReplacer
---
test/source/mock/google/google-data.ts | 29 ++++++++++++++++++--------
test/source/tests/decrypt.ts | 12 +++++++++--
2 files changed, 30 insertions(+), 11 deletions(-)
diff --git a/test/source/mock/google/google-data.ts b/test/source/mock/google/google-data.ts
index 706f5045f14..bd4f6e8ca61 100644
--- a/test/source/mock/google/google-data.ts
+++ b/test/source/mock/google/google-data.ts
@@ -224,16 +224,13 @@ export class GoogleData {
const payload = (await GoogleData.withInitializedData(acct)).getMessage(msgId)!.payload!;
const fromHeader = payload.headers!.find(header => header.name === 'From')!;
const fromAddress = fromHeader.value!;
- let htmlData: string;
- const htmlPart = payload.parts!.find(part => part.mimeType === 'text/html');
- if (htmlPart) {
- htmlData = Buf.fromBase64Str(htmlPart.body!.data!).toUtfStr();
- } else {
- const textPart = payload.parts!.find(part => part.mimeType === 'text/plain')!;
- const textData = Buf.fromBase64Str(textPart.body!.data!).toUtfStr();
- htmlData = Xss.escape(textData);
+ let htmlData = GoogleData.getHtmlDataToDisplay(payload.parts!);
+ if (typeof htmlData === 'undefined') {
+ // search inside multipart/alternative
+ const alternativePart = payload.parts!.find(part => part.mimeType === 'multipart/alternative');
+ htmlData = GoogleData.getHtmlDataToDisplay(alternativePart!.parts!);
}
- const otherParts = payload.parts!.filter(part => !['text/plain', 'text/html'].includes(part.mimeType!));
+ const otherParts = payload.parts!.filter(part => part.filename);
if (otherParts.length) {
attachmentsBlock =
`${otherParts.length} Attachments
@@ -292,6 +289,20 @@ export class GoogleData {
);
};
+ private static getHtmlDataToDisplay = (parts: GmailMsg$payload$part[]): string | undefined => {
+ const htmlPart = parts.find(part => part.mimeType === 'text/html');
+ if (htmlPart) {
+ return Buf.fromBase64Str(htmlPart.body!.data!).toUtfStr();
+ } else {
+ const textPart = parts.find(part => part.mimeType === 'text/plain');
+ if (typeof textPart?.body?.data === 'undefined') {
+ return undefined;
+ }
+ const textData = Buf.fromBase64Str(textPart.body.data).toUtfStr();
+ return Xss.escape(textData);
+ }
+ };
+
public storeSentMessage = (parseResult: ParseMsgResult, id: string): string => {
let bodyContentAtt: { data: string; size: number; filename?: string; id: string } | undefined;
const parsedMail = parseResult.mimeMsg;
diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts
index 987bc264ec5..4f839fdc988 100644
--- a/test/source/tests/decrypt.ts
+++ b/test/source/tests/decrypt.ts
@@ -70,7 +70,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te
);
test(
- `decrypt - parsed signed message with signautre.asc as plain attachment`,
+ `decrypt - parsed signed message with signature.asc as plain attachment`,
testWithBrowser('compatibility', async (t, browser) => {
const threadId = '187085b874fb727c';
const acctEmail = 'flowcrypt.compatibility@gmail.com';
@@ -78,8 +78,16 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te
await inboxPage.waitForSelTestState('ready');
await inboxPage.waitAll('iframe');
const pgpBlock = await inboxPage.getFrame(['pgp_block.htm']);
- await pgpBlock.waitForContent('@pgp-block-content', 'flowcrypt-browser issue #5029 test email');
+ const expectedContent = 'flowcrypt-browser issue #5029 test email';
+ await pgpBlock.waitForContent('@pgp-block-content', expectedContent);
+ const accessToken = await BrowserRecipe.getGoogleAccessToken(inboxPage, acctEmail);
await inboxPage.close();
+ const extraAuthHeaders = { Authorization: `Bearer ${accessToken}` }; // eslint-disable-line @typescript-eslint/naming-convention
+ const gmailPage = await browser.newPage(t, `${t.urls?.mockGmailUrl()}/${threadId}`, undefined, extraAuthHeaders);
+ await gmailPage.waitAll('iframe');
+ const pgpBlock2 = await gmailPage.getFrame(['pgp_block.htm']);
+ await pgpBlock2.waitForContent('@pgp-block-content', expectedContent);
+ await gmailPage.close();
})
);
From 02d31d842e672202e4b36c366785b596305699ef Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Thu, 13 Apr 2023 16:40:30 +0100
Subject: [PATCH 007/147] fix signature attachment
---
extension/js/common/core/mime.ts | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts
index 2a9970bf531..e84ba150e44 100644
--- a/extension/js/common/core/mime.ts
+++ b/extension/js/common/core/mime.ts
@@ -84,7 +84,7 @@ export class Mime {
return blocks;
};
- public static processAttachments = (messageBlocks: MsgBlock[], decoded: MimeContent): MimeProccesedMsg => {
+ public static processAttachments = (bodyBlocks: MsgBlock[], decoded: MimeContent): MimeProccesedMsg => {
const attachmentBlocks: { block: MsgBlock; file: Attachment }[] = [];
const signatureAttachments: Attachment[] = [];
for (const file of decoded.attachments) {
@@ -148,16 +148,17 @@ export class Mime {
// todo: data may not be present
if (signatureAttachment.hasData()) {
const signature = signatureAttachment.getData().toUtfStr();
- for (const block of attachmentBlocks) {
- if (block.block.type === 'plainText') {
- block.block.type = 'signedText';
- block.block.signature = signature;
- } else if (block.block.type === 'plainHtml') {
- block.block.type = 'signedHtml';
- block.block.signature = signature;
+ const blocksToRevisit = [...bodyBlocks, ...attachmentBlocks.map(x => x.block)];
+ for (const block of blocksToRevisit) {
+ if (block.type === 'plainText') {
+ block.type = 'signedText';
+ block.signature = signature;
+ } else if (block.type === 'plainHtml') {
+ block.type = 'signedHtml';
+ block.signature = signature;
}
}
- if (!attachmentBlocks.find(block => ['plainText', 'plainHtml', 'signedMsg', 'signedHtml', 'signedText'].includes(block.block.type))) {
+ if (!blocksToRevisit.find(block => ['plainText', 'plainHtml', 'signedMsg', 'signedHtml', 'signedText'].includes(block.type))) {
// signed an empty message
attachmentBlocks.push({ block: new MsgBlock('signedMsg', '', true, signature), file: signatureAttachment });
}
@@ -168,7 +169,7 @@ export class Mime {
}
return {
blocks: [
- ...messageBlocks.map(block => {
+ ...bodyBlocks.map(block => {
return { block };
}),
...attachmentBlocks,
From 269b55fadfef2035f8663bcd608e49eb5a1a0fbf Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sun, 16 Apr 2023 08:11:00 +0100
Subject: [PATCH 008/147] Temporarily increase test overall timeout
---
package.json | 2 +-
test/source/test.ts | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index e337f0eeb16..917d8efe28b 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
"test_patterns": "node build/test/test/source/patterns.js",
"test_async_stack": "node build/test/test/source/async-stack.js",
"test_buf": "npx ava --timeout=3m --verbose --concurrency=10 build/test/test/source/buf.js",
- "test_ci_chrome_consumer_live_gmail": "npx ava --timeout=30m --verbose --tap --concurrency=1 build/test/test/source/test.js -- CONSUMER-LIVE-GMAIL STANDARD-GROUP | npx tap-xunit > report.xml",
+ "test_ci_chrome_consumer_live_gmail": "npx ava --timeout=60m --verbose --tap --concurrency=1 build/test/test/source/test.js -- CONSUMER-LIVE-GMAIL STANDARD-GROUP | npx tap-xunit > report.xml",
"test_ci_chrome_consumer": "npx ava --timeout=30m --verbose --tap --concurrency=10 build/test/test/source/test.js -- CONSUMER-MOCK STANDARD-GROUP | npx tap-xunit > report.xml",
"test_ci_chrome_enterprise": "npx ava --timeout=30m --verbose --tap --concurrency=10 build/test/test/source/test.js -- ENTERPRISE-MOCK STANDARD-GROUP | npx tap-xunit > report.xml",
"test_ci_chrome_consumer_flaky": "npx ava --timeout=30m --verbose --tap --concurrency=10 build/test/test/source/test.js -- CONSUMER-MOCK FLAKY-GROUP | npx tap-xunit > report.xml",
diff --git a/test/source/test.ts b/test/source/test.ts
index 730d48d537c..861af3ecd82 100644
--- a/test/source/test.ts
+++ b/test/source/test.ts
@@ -38,8 +38,8 @@ const consts = {
// higher concurrency can cause 429 google errs when composing
TIMEOUT_SHORT: minutes(1),
TIMEOUT_EACH_RETRY: minutes(4),
- TIMEOUT_ALL_RETRIES: minutes(25), // this has to suffer waiting for semaphore between retries, thus almost the same as below
- TIMEOUT_OVERALL: minutes(30),
+ TIMEOUT_ALL_RETRIES: minutes(55), // this has to suffer waiting for semaphore between retries, thus almost the same as below
+ TIMEOUT_OVERALL: minutes(60),
ATTEMPTS: testGroup === 'STANDARD-GROUP' ? oneIfNotPooled(3) : process.argv.includes('--retry=false') ? 1 : 3,
POOL_SIZE: oneIfNotPooled(isMock ? 20 : 3),
PROMISE_TIMEOUT_OVERALL: undefined as unknown as Promise, // will be set right below
From 2e1101bdba30df0c3c2a91d9fd00f40db67d2a3e Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sun, 16 Apr 2023 18:47:28 +0100
Subject: [PATCH 009/147] fix
---
.../js/content_scripts/webmail/gmail-element-replacer.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 5711b9fb800..13b49a24bee 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -580,7 +580,9 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (treatAs !== 'plainFile') {
this.hideAttachment(attachmentSel, attachmentsContainerInner);
}
- if (treatAs === 'encryptedFile') {
+ if (treatAs === 'hidden') {
+ return 'hidden';
+ } else if (treatAs === 'encryptedFile') {
// actual encrypted attachment - show it
attachmentsContainerInner.prepend(this.factory.embeddedAttachment(a, true)); // xss-safe-factory
return 'replaced'; // native should be hidden, custom should appear instead
From 598735d99606542de3ebd3c17ddbde24525e4cdc Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Mon, 17 Apr 2023 10:00:39 +0100
Subject: [PATCH 010/147] fix
---
.../js/content_scripts/webmail/gmail-element-replacer.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 13b49a24bee..cc9e9bdd761 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -45,8 +45,8 @@ export class GmailElementReplacer implements WebmailElementReplacer {
private recipientHasPgpCache: Dict = {};
private sendAs: Dict;
private messages: Dict = {};
- private chunkDownloads: { attachment: Attachment; result: Promise }[];
- // private attachmentDownloads: { attachment: Attachment; result: Promise }[];
+ private chunkDownloads: { attachment: Attachment; result: Promise }[] = [];
+ // private attachmentDownloads: { attachment: Attachment; result: Promise }[] = [];
private factory: XssSafeFactory;
private clientConfiguration: ClientConfiguration;
private pubLookup: PubLookup;
From 557ea8dfa1f3e708b2721e0317bf1add339dcc37 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Mon, 17 Apr 2023 15:33:11 +0100
Subject: [PATCH 011/147] inrease execution timeout
---
.semaphore/semaphore.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml
index 1081ea0cb36..22cbd1fbdee 100644
--- a/.semaphore/semaphore.yml
+++ b/.semaphore/semaphore.yml
@@ -60,7 +60,7 @@ blocks:
run:
when: branch = 'master' OR branch =~ 'live-test' OR branch =~ 'gmail-test'
execution_time_limit:
- minutes: 30
+ minutes: 45
task:
secrets:
- name: flowcrypt-browser-ci-secrets
From f3313fbe5578685d282462bced0bd1a1ea44cbf5 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Thu, 20 Apr 2023 11:33:30 +0100
Subject: [PATCH 012/147] (wip) render-only pgp block iframe
---
.../chrome/elements/pgp_base_block_view.ts | 18 ++
extension/chrome/elements/pgp_block.ts | 22 +-
.../pgp-block-decrypt-module.ts | 83 +++++-
.../pgp-block-print-module.ts | 131 +++++++++
.../pgp-block-quote-module.ts | 12 +-
.../pgp-block-render-module.ts | 264 +-----------------
.../chrome/elements/pgp_render_block.htm | 42 +++
extension/chrome/elements/pgp_render_block.ts | 66 +++++
extension/js/common/message-renderer.ts | 185 +++++++++++-
extension/js/common/render-interface.ts | 13 +
extension/js/common/render-message.ts | 13 +
extension/js/common/render-relay.ts | 40 +++
extension/js/common/xss-safe-factory.ts | 25 ++
.../webmail/gmail-element-replacer.ts | 41 ++-
extension/manifest.json | 18 +-
15 files changed, 684 insertions(+), 289 deletions(-)
create mode 100644 extension/chrome/elements/pgp_base_block_view.ts
create mode 100644 extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts
create mode 100644 extension/chrome/elements/pgp_render_block.htm
create mode 100644 extension/chrome/elements/pgp_render_block.ts
create mode 100644 extension/js/common/render-interface.ts
create mode 100644 extension/js/common/render-message.ts
create mode 100644 extension/js/common/render-relay.ts
diff --git a/extension/chrome/elements/pgp_base_block_view.ts b/extension/chrome/elements/pgp_base_block_view.ts
new file mode 100644
index 00000000000..b2ddbf1d80d
--- /dev/null
+++ b/extension/chrome/elements/pgp_base_block_view.ts
@@ -0,0 +1,18 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+import { View } from '../../js/common/view.js';
+import { PgpBlockViewQuoteModule } from './pgp_block_modules/pgp-block-quote-module.js';
+import { PgpBlockViewRenderModule } from './pgp_block_modules/pgp-block-render-module.js';
+
+export abstract class PgpBaseBlockView extends View {
+ public readonly quoteModule: PgpBlockViewQuoteModule;
+ public readonly renderModule: PgpBlockViewRenderModule;
+
+ public constructor(public readonly parentTabId: string, public readonly frameId: string) {
+ super();
+ this.quoteModule = new PgpBlockViewQuoteModule(this);
+ this.renderModule = new PgpBlockViewRenderModule(this);
+ }
+}
diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts
index 38174e27a23..1f6d311d18c 100644
--- a/extension/chrome/elements/pgp_block.ts
+++ b/extension/chrome/elements/pgp_block.ts
@@ -10,8 +10,6 @@ import { Lang } from '../../js/common/lang.js';
import { PgpBlockViewAttachmentsModule } from './pgp_block_modules/pgp-block-attachmens-module.js';
import { PgpBlockViewDecryptModule } from './pgp_block_modules/pgp-block-decrypt-module.js';
import { PgpBlockViewErrorModule } from './pgp_block_modules/pgp-block-error-module.js';
-import { PgpBlockViewQuoteModule } from './pgp_block_modules/pgp-block-quote-module.js';
-import { PgpBlockViewRenderModule } from './pgp_block_modules/pgp-block-render-module.js';
import { PgpBlockViewSignatureModule } from './pgp_block_modules/pgp-block-signature-module.js';
import { Ui } from '../../js/common/browser/ui.js';
import { View } from '../../js/common/view.js';
@@ -20,11 +18,11 @@ import { ClientConfiguration } from '../../js/common/client-configuration.js';
import { AcctStore } from '../../js/common/platform/store/acct-store.js';
import { ContactStore } from '../../js/common/platform/store/contact-store.js';
import { KeyUtil } from '../../js/common/core/crypto/key.js';
+import { PgpBaseBlockView } from './pgp_base_block_view.js';
+import { PgpBlockViewPrintModule } from './pgp_block_modules/pgp-block-print-module.js';
-export class PgpBlockView extends View {
+export class PgpBlockView extends PgpBaseBlockView {
public readonly acctEmail: string;
- public readonly parentTabId: string;
- public readonly frameId: string;
public readonly isOutgoing: boolean;
public readonly senderEmail: string;
public readonly msgId: string | undefined;
@@ -41,20 +39,17 @@ export class PgpBlockView extends View {
public readonly debug: boolean;
public readonly attachmentsModule: PgpBlockViewAttachmentsModule;
public readonly signatureModule: PgpBlockViewSignatureModule;
- public readonly quoteModule: PgpBlockViewQuoteModule;
public readonly errorModule: PgpBlockViewErrorModule;
- public readonly renderModule: PgpBlockViewRenderModule;
+ public readonly printModule: PgpBlockViewPrintModule;
public readonly decryptModule: PgpBlockViewDecryptModule;
public fesUrl?: string;
public constructor() {
- super();
Ui.event.protect();
const uncheckedUrlParams = Url.parse(['acctEmail', 'frameId', 'message', 'parentTabId', 'msgId', 'isOutgoing', 'senderEmail', 'signature', 'debug']);
+ super(Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'), Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId'));
this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail');
- this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId');
- this.frameId = Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId');
this.isOutgoing = uncheckedUrlParams.isOutgoing === true;
this.debug = uncheckedUrlParams.debug === true;
const senderEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'senderEmail');
@@ -73,9 +68,8 @@ export class PgpBlockView extends View {
// modules
this.attachmentsModule = new PgpBlockViewAttachmentsModule(this);
this.signatureModule = new PgpBlockViewSignatureModule(this);
- this.quoteModule = new PgpBlockViewQuoteModule(this);
this.errorModule = new PgpBlockViewErrorModule(this);
- this.renderModule = new PgpBlockViewRenderModule(this);
+ this.printModule = new PgpBlockViewPrintModule(this);
this.decryptModule = new PgpBlockViewDecryptModule(this);
}
@@ -93,7 +87,7 @@ export class PgpBlockView extends View {
this.fesUrl = storage.fesUrl;
this.clientConfiguration = await ClientConfiguration.newInstance(this.acctEmail);
this.pubLookup = new PubLookup(this.clientConfiguration);
- await this.renderModule.initPrintView();
+ await this.printModule.initPrintView();
if (storage.setup_done) {
const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.getExpectedSignerEmail()))?.sortedPubkeys ?? [];
// todo: we don't actually need parsed pubs here because we're going to pass them to the backgorund page
@@ -108,7 +102,7 @@ export class PgpBlockView extends View {
public setHandlers = () => {
$('.pgp_print_button').on(
'click',
- this.setHandler(() => this.renderModule.printPGPBlock())
+ this.setHandler(() => this.printModule.printPGPBlock())
);
};
}
diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts
index f7de897d0b4..db658e2dc35 100644
--- a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts
+++ b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts
@@ -4,7 +4,7 @@
import { BrowserMsg } from '../../../js/common/browser/browser-msg.js';
import { Buf } from '../../../js/common/core/buf.js';
-import { DecryptErrTypes } from '../../../js/common/core/crypto/pgp/msg-util.js';
+import { DecryptErrTypes, VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js';
import { GmailResponseFormat } from '../../../js/common/api/email-provider/gmail/gmail.js';
import { Lang } from '../../../js/common/lang.js';
import { Mime } from '../../../js/common/core/mime.js';
@@ -15,6 +15,9 @@ import { KeyStore } from '../../../js/common/platform/store/key-store.js';
import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js';
import { MsgBlockParser } from '../../../js/common/core/msg-block-parser.js';
import { Str } from '../../../js/common/core/common.js';
+import { Attachment } from '../../../js/common/core/attachment.js';
+import { MsgBlock } from '../../../js/common/core/msg-block.js';
+import { MessageRenderer } from '../../../js/common/message-renderer.js';
export class PgpBlockViewDecryptModule {
private msgFetchedFromApi: false | GmailResponseFormat = false;
@@ -118,7 +121,7 @@ export class PgpBlockViewDecryptModule {
return await this.decryptAndRender(fetchedContent, verificationPubs);
}
}
- await this.view.renderModule.decideDecryptedContentFormattingAndRender(
+ await this.decideDecryptedContentFormattingAndRender(
result.content,
result.isEncrypted,
result.signature,
@@ -191,7 +194,81 @@ export class PgpBlockViewDecryptModule {
const verify = async (verificationPubs: string[]) =>
await BrowserMsg.send.bg.await.pgpMsgVerifyDetached({ plaintext: encryptedData, sigText, verificationPubs });
const signatureResult = await verify(verificationPubs);
- await this.view.renderModule.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult, verificationPubs, verify);
+ await this.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult, verificationPubs, verify);
+ }
+ };
+
+ private decideDecryptedContentFormattingAndRender = async (
+ decryptedBytes: Uint8Array | string,
+ isEncrypted: boolean,
+ sigResult: VerifyRes | undefined,
+ verificationPubs: string[],
+ retryVerification: (verificationPubs: string[]) => Promise,
+ plainSubject?: string
+ ) => {
+ if (isEncrypted) {
+ this.view.renderModule.renderEncryptionStatus('encrypted');
+ this.view.renderModule.setFrameColor('green');
+ } else {
+ this.view.renderModule.renderEncryptionStatus('not encrypted');
+ this.view.renderModule.setFrameColor('gray');
+ }
+ const publicKeys: string[] = [];
+ let renderableAttachments: Attachment[] = [];
+ let decryptedContent: string | undefined;
+ let isHtml = false;
+ // todo - replace with MsgBlockParser.fmtDecryptedAsSanitizedHtmlBlocks, then the extract/strip methods could be private?
+ if (!Mime.resemblesMsg(decryptedBytes)) {
+ const fcAttachmentBlocks: MsgBlock[] = [];
+ decryptedContent = Str.with(decryptedBytes);
+ decryptedContent = MsgBlockParser.extractFcAttachments(decryptedContent, fcAttachmentBlocks);
+ decryptedContent = MsgBlockParser.stripFcTeplyToken(decryptedContent);
+ decryptedContent = MsgBlockParser.stripPublicKeys(decryptedContent, publicKeys);
+ if (fcAttachmentBlocks.length) {
+ renderableAttachments = fcAttachmentBlocks.map(
+ attachmentBlock => new Attachment(attachmentBlock.attachmentMeta!) // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ );
+ }
+ } else {
+ this.view.renderModule.renderText('Formatting...');
+ const decoded = await Mime.decode(decryptedBytes);
+ let inlineCIDAttachments: Attachment[] = [];
+ if (typeof decoded.html !== 'undefined') {
+ ({ sanitizedHtml: decryptedContent, inlineCIDAttachments } = MessageRenderer.replaceInlineImageCIDs(decoded.html, decoded.attachments));
+ isHtml = true;
+ } else if (typeof decoded.text !== 'undefined') {
+ decryptedContent = decoded.text;
+ } else {
+ decryptedContent = '';
+ }
+ if (
+ decoded.subject &&
+ isEncrypted &&
+ (!plainSubject || !Mime.subjectWithoutPrefixes(plainSubject).includes(Mime.subjectWithoutPrefixes(decoded.subject)))
+ ) {
+ // there is an encrypted subject + (either there is no plain subject or the plain subject does not contain what's in the encrypted subject)
+ decryptedContent = MessageRenderer.getEncryptedSubjectText(decoded.subject, isHtml) + decryptedContent; // render encrypted subject in message
+ }
+ for (const attachment of decoded.attachments) {
+ if (attachment.isPublicKey()) {
+ publicKeys.push(attachment.getData().toUtfStr());
+ } else if (!inlineCIDAttachments.some(inlineAttachment => inlineAttachment.cid === attachment.cid)) {
+ renderableAttachments.push(attachment);
+ }
+ }
+ }
+ this.view.quoteModule.separateQuotedContentAndRenderText(decryptedContent, isHtml);
+ await this.view.signatureModule.renderPgpSignatureCheckResult(sigResult, verificationPubs, retryVerification);
+ if (isEncrypted && publicKeys.length) {
+ BrowserMsg.send.renderPublicKeys(this.view.parentTabId, { afterFrameId: this.view.frameId, publicKeys });
+ }
+ if (renderableAttachments.length) {
+ this.view.attachmentsModule.renderInnerAttachments(renderableAttachments, isEncrypted);
+ }
+ this.view.renderModule.resizePgpBlockFrame();
+ if (!this.view.renderModule.doNotSetStateAsReadyYet) {
+ // in case async tasks are still being worked at
+ Ui.setTestState('ready');
}
};
}
diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts
new file mode 100644
index 00000000000..351946ad7d7
--- /dev/null
+++ b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts
@@ -0,0 +1,131 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+import { PgpBlockView } from '../pgp_block.js';
+import { Time } from '../../../js/common/browser/time.js';
+import { Xss } from '../../../js/common/platform/xss.js';
+import { AcctStore } from '../../../js/common/platform/store/acct-store.js';
+import { GmailParser } from '../../../js/common/api/email-provider/gmail/gmail-parser.js';
+import { Str } from '../../../js/common/core/common.js';
+
+export class PgpBlockViewPrintModule {
+ private printMailInfoHtml!: string;
+
+ public constructor(private view: PgpBlockView) {}
+
+ public initPrintView = async () => {
+ const fullName = await AcctStore.get(this.view.acctEmail, ['full_name']);
+ Xss.sanitizeRender('.print_user_email', `${fullName.full_name} <${this.view.acctEmail}>`);
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const gmailMsg = await this.view.gmail.msgGet(this.view.msgId!, 'metadata', undefined);
+ const sentDate = new Date(GmailParser.findHeader(gmailMsg, 'date') ?? '');
+ const sentDateStr = Str.fromDate(sentDate).replace(' ', ' at ');
+ const from = Str.parseEmail(GmailParser.findHeader(gmailMsg, 'from') ?? '');
+ const fromHtml = from.name ? `${Xss.htmlSanitize(from.name)} <${from.email}>` : from.email;
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
+ const ccString = GmailParser.findHeader(gmailMsg, 'cc')
+ ? `Cc: ${Xss.escape(GmailParser.findHeader(gmailMsg, 'cc')!)}
`
+ : '';
+ const bccString = GmailParser.findHeader(gmailMsg, 'bcc') ? `Bcc: ${Xss.escape(GmailParser.findHeader(gmailMsg, 'bcc')!)}
` : '';
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
+ this.printMailInfoHtml = `
+
+ ${Xss.htmlSanitize(GmailParser.findHeader(gmailMsg, 'subject') ?? '')}
+
+
+
+
+ From: ${fromHtml}
+
+
+ ${sentDateStr}
+
+
+ To: ${Xss.escape(GmailParser.findHeader(gmailMsg, 'to') ?? '')}
+ ${ccString}
+ ${bccString}
+
+ `;
+ } catch (e) {
+ this.view.errorModule.debug(`Error while getting gmail message for ${this.view.msgId} message. ${e}`);
+ }
+ };
+
+ public printPGPBlock = async () => {
+ const w = window.open();
+ const html = `
+
+
+
+
+
+
+ ${$('#print-header').html()}
+
+ ${Xss.htmlSanitize(this.printMailInfoHtml)}
+
+
+ ${Xss.htmlSanitize($('#pgp_block').html())}
+
+
+
+ `;
+ w?.document.write(html);
+ // Give some time for above dom to load in print dialog
+ // https://stackoverflow.com/questions/31725373/google-chrome-not-showing-image-in-print-preview
+ await Time.sleep(250);
+ w?.window.print();
+ w?.document.close();
+ };
+}
diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-quote-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-quote-module.ts
index 03298dc4049..dc51b979c79 100644
--- a/extension/chrome/elements/pgp_block_modules/pgp-block-quote-module.ts
+++ b/extension/chrome/elements/pgp_block_modules/pgp-block-quote-module.ts
@@ -2,14 +2,14 @@
'use strict';
-import { PgpBlockView } from '../pgp_block.js';
import { Str } from '../../../js/common/core/common.js';
import { Xss } from '../../../js/common/platform/xss.js';
+import { PgpBaseBlockView } from '../pgp_base_block_view.js';
export class PgpBlockViewQuoteModule {
- public constructor(private view: PgpBlockView) {}
+ public constructor(private view: PgpBaseBlockView) {}
- public separateQuotedContentAndRenderText = async (decryptedContent: string, isHtml: boolean) => {
+ public separateQuotedContentAndRenderText = (decryptedContent: string, isHtml: boolean) => {
if (isHtml) {
const message = $('').html(Xss.htmlSanitizeKeepBasicTags(decryptedContent)); // xss-sanitized
let htmlBlockQuoteExists = false;
@@ -32,10 +32,10 @@ export class PgpBlockViewQuoteModule {
message[0].removeChild(shouldBeQuoted[i]);
quotedHtml += shouldBeQuoted[i].outerHTML;
}
- await this.view.renderModule.renderContent(message.html(), false);
+ this.view.renderModule.renderContent(message.html(), false);
this.appendCollapsedQuotedContentButton(quotedHtml, true);
} else {
- await this.view.renderModule.renderContent(decryptedContent, false);
+ this.view.renderModule.renderContent(decryptedContent, false);
}
} else {
const lines = decryptedContent.split(/\r?\n/);
@@ -62,7 +62,7 @@ export class PgpBlockViewQuoteModule {
// only got quoted part, no real text -> show everything as real text, without quoting
lines.push(...linesQuotedPart.splice(0, linesQuotedPart.length));
}
- await this.view.renderModule.renderContent(Str.escapeTextAsRenderableHtml(lines.join('\n')), false);
+ this.view.renderModule.renderContent(Str.escapeTextAsRenderableHtml(lines.join('\n')), false);
if (linesQuotedPart.join('').trim()) {
this.appendCollapsedQuotedContentButton(linesQuotedPart.join('\n'));
}
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 b806aaf2faf..a59020c3184 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
@@ -2,144 +2,17 @@
'use strict';
-import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js';
-import { Attachment } from '../../../js/common/core/attachment.js';
import { BrowserMsg } from '../../../js/common/browser/browser-msg.js';
import { Catch } from '../../../js/common/platform/catch.js';
-import { Mime } from '../../../js/common/core/mime.js';
-import { MsgBlock } from '../../../js/common/core/msg-block.js';
-import { PgpBlockView } from '../pgp_block.js';
-import { Ui } from '../../../js/common/browser/ui.js';
import { Xss } from '../../../js/common/platform/xss.js';
-import { MsgBlockParser } from '../../../js/common/core/msg-block-parser.js';
-import { AcctStore } from '../../../js/common/platform/store/acct-store.js';
-import { GmailParser } from '../../../js/common/api/email-provider/gmail/gmail-parser.js';
-import { CID_PATTERN, Str } from '../../../js/common/core/common.js';
-import DOMPurify from 'dompurify';
-import { Time } from '../../../js/common/browser/time.js';
+import { PgpBaseBlockView } from '../pgp_base_block_view';
export class PgpBlockViewRenderModule {
public doNotSetStateAsReadyYet = false;
private heightHist: number[] = [];
- private printMailInfoHtml!: string;
- public constructor(private view: PgpBlockView) {}
-
- public initPrintView = async () => {
- const fullName = await AcctStore.get(this.view.acctEmail, ['full_name']);
- Xss.sanitizeRender('.print_user_email', `
${fullName.full_name} <${this.view.acctEmail}>`);
- try {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const gmailMsg = await this.view.gmail.msgGet(this.view.msgId!, 'metadata', undefined);
- const sentDate = new Date(GmailParser.findHeader(gmailMsg, 'date') ?? '');
- const sentDateStr = Str.fromDate(sentDate).replace(' ', ' at ');
- const from = Str.parseEmail(GmailParser.findHeader(gmailMsg, 'from') ?? '');
- const fromHtml = from.name ? `
${Xss.htmlSanitize(from.name)} <${from.email}>` : from.email;
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- const ccString = GmailParser.findHeader(gmailMsg, 'cc')
- ? `Cc:
${Xss.escape(GmailParser.findHeader(gmailMsg, 'cc')!)}`
- : '';
- const bccString = GmailParser.findHeader(gmailMsg, 'bcc') ? `Bcc:
${Xss.escape(GmailParser.findHeader(gmailMsg, 'bcc')!)}` : '';
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
- this.printMailInfoHtml = `
-
-
${Xss.htmlSanitize(GmailParser.findHeader(gmailMsg, 'subject') ?? '')}
-
-
-
-
- From: ${fromHtml}
-
-
- ${sentDateStr}
-
-
-
To: ${Xss.escape(GmailParser.findHeader(gmailMsg, 'to') ?? '')}
- ${ccString}
- ${bccString}
-
- `;
- } catch (e) {
- this.view.errorModule.debug(`Error while getting gmail message for ${this.view.msgId} message. ${e}`);
- }
- };
-
- public printPGPBlock = async () => {
- const w = window.open();
- const html = `
-
-
-
-
-
-
- ${$('#print-header').html()}
-
- ${Xss.htmlSanitize(this.printMailInfoHtml)}
-
-
- ${Xss.htmlSanitize($('#pgp_block').html())}
-
-
-
- `;
- w?.document.write(html);
- // Give some time for above dom to load in print dialog
- // https://stackoverflow.com/questions/31725373/google-chrome-not-showing-image-in-print-preview
- await Time.sleep(250);
- w?.window.print();
- w?.document.close();
- };
+ public constructor(private view: PgpBaseBlockView) {}
public renderText = (text: string) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -172,12 +45,15 @@ export class PgpBlockViewRenderModule {
});
};
- public renderContent = async (htmlContent: string, isErr: boolean) => {
+ public renderContent = (htmlContent: string, isErr: boolean) => {
+ /*
+ todo:
if (!isErr && !this.view.isOutgoing) {
// successfully opened incoming message
// eslint-disable-next-line @typescript-eslint/naming-convention
await AcctStore.set(this.view.acctEmail, { successfully_received_at_leat_one_message: true });
}
+ */
if (!isErr) {
// rendering message content
$('.pgp_print_button').show();
@@ -243,132 +119,4 @@ export class PgpBlockViewRenderModule {
.addClass(status === 'signed' ? 'green_label' : 'red_label')
.text(status);
};
-
- public decideDecryptedContentFormattingAndRender = async (
- decryptedBytes: Uint8Array | string,
- isEncrypted: boolean,
- sigResult: VerifyRes | undefined,
- verificationPubs: string[],
- retryVerification: (verificationPubs: string[]) => Promise
,
- plainSubject?: string
- ) => {
- if (isEncrypted) {
- this.renderEncryptionStatus('encrypted');
- this.setFrameColor('green');
- } else {
- this.renderEncryptionStatus('not encrypted');
- this.setFrameColor('gray');
- }
- const publicKeys: string[] = [];
- let renderableAttachments: Attachment[] = [];
- let decryptedContent: string | undefined;
- let isHtml = false;
- // todo - replace with MsgBlockParser.fmtDecryptedAsSanitizedHtmlBlocks, then the extract/strip methods could be private?
- if (!Mime.resemblesMsg(decryptedBytes)) {
- const fcAttachmentBlocks: MsgBlock[] = [];
- decryptedContent = Str.with(decryptedBytes);
- decryptedContent = MsgBlockParser.extractFcAttachments(decryptedContent, fcAttachmentBlocks);
- decryptedContent = MsgBlockParser.stripFcTeplyToken(decryptedContent);
- decryptedContent = MsgBlockParser.stripPublicKeys(decryptedContent, publicKeys);
- if (fcAttachmentBlocks.length) {
- renderableAttachments = fcAttachmentBlocks.map(
- attachmentBlock => new Attachment(attachmentBlock.attachmentMeta!) // eslint-disable-line @typescript-eslint/no-non-null-assertion
- );
- }
- } else {
- this.renderText('Formatting...');
- const decoded = await Mime.decode(decryptedBytes);
- let inlineCIDAttachments: Attachment[] = [];
- if (typeof decoded.html !== 'undefined') {
- ({ sanitizedHtml: decryptedContent, inlineCIDAttachments } = this.replaceInlineImageCIDs(decoded.html, decoded.attachments));
- isHtml = true;
- } else if (typeof decoded.text !== 'undefined') {
- decryptedContent = decoded.text;
- } else {
- decryptedContent = '';
- }
- if (
- decoded.subject &&
- isEncrypted &&
- (!plainSubject || !Mime.subjectWithoutPrefixes(plainSubject).includes(Mime.subjectWithoutPrefixes(decoded.subject)))
- ) {
- // there is an encrypted subject + (either there is no plain subject or the plain subject does not contain what's in the encrypted subject)
- decryptedContent = this.getEncryptedSubjectText(decoded.subject, isHtml) + decryptedContent; // render encrypted subject in message
- }
- for (const attachment of decoded.attachments) {
- if (attachment.isPublicKey()) {
- publicKeys.push(attachment.getData().toUtfStr());
- } else if (!inlineCIDAttachments.some(inlineAttachment => inlineAttachment.cid === attachment.cid)) {
- renderableAttachments.push(attachment);
- }
- }
- }
- await this.view.quoteModule.separateQuotedContentAndRenderText(decryptedContent, isHtml);
- await this.view.signatureModule.renderPgpSignatureCheckResult(sigResult, verificationPubs, retryVerification);
- if (isEncrypted && publicKeys.length) {
- BrowserMsg.send.renderPublicKeys(this.view.parentTabId, { afterFrameId: this.view.frameId, publicKeys });
- }
- if (renderableAttachments.length) {
- this.view.attachmentsModule.renderInnerAttachments(renderableAttachments, isEncrypted);
- }
- this.resizePgpBlockFrame();
- if (!this.doNotSetStateAsReadyYet) {
- // in case async tasks are still being worked at
- Ui.setTestState('ready');
- }
- };
-
- /**
- * Replaces inline image CID references with base64 encoded data in sanitized HTML
- * and returns the sanitized HTML along with the inline CID attachments.
- *
- * @param html - The original HTML content.
- * @param attachments - An array of email attachments.
- * @returns An object containing sanitized HTML and an array of inline CID attachments.
- */
- private replaceInlineImageCIDs = (html: string, attachments: Attachment[]): { sanitizedHtml: string; inlineCIDAttachments: Attachment[] } => {
- // Array to store inline CID attachments
- const inlineCIDAttachments: Attachment[] = [];
-
- // Define the hook function for DOMPurify to process image elements after sanitizing attributes
- const processImageElements = (node: Element | null) => {
- // Ensure the node exists and has a 'src' attribute
- if (!node || !('src' in node)) return;
- const imageSrc = node.getAttribute('src') as string;
- if (!imageSrc) return;
- const matches = imageSrc.match(CID_PATTERN);
-
- // Check if the src attribute contains a CID
- if (matches && matches[1]) {
- const contentId = matches[1];
- const contentIdAttachment = attachments.find(attachment => attachment.cid === `<${contentId}>`);
-
- // Replace the src attribute with a base64 encoded string
- if (contentIdAttachment) {
- inlineCIDAttachments.push(contentIdAttachment);
- node.setAttribute('src', `data:${contentIdAttachment.type};base64,${contentIdAttachment.getData().toBase64Str()}`);
- }
- }
- };
-
- // Add the DOMPurify hook
- DOMPurify.addHook('afterSanitizeAttributes', processImageElements);
-
- // Sanitize the HTML and remove the DOMPurify hooks
- const sanitizedHtml = DOMPurify.sanitize(html);
- DOMPurify.removeAllHooks();
-
- return { sanitizedHtml, inlineCIDAttachments };
- };
-
- private getEncryptedSubjectText = (subject: string, isHtml: boolean) => {
- if (isHtml) {
- return ` Encrypted Subject:
- ${Xss.escape(subject)}
-
-
`;
- } else {
- return `Encrypted Subject: ${subject}\n----------------------------------------------------------------------------------------------------\n`;
- }
- };
}
diff --git a/extension/chrome/elements/pgp_render_block.htm b/extension/chrome/elements/pgp_render_block.htm
new file mode 100644
index 00000000000..b4a4defe2d5
--- /dev/null
+++ b/extension/chrome/elements/pgp_render_block.htm
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ FlowCrypt
+
+
+
+
+
+
+
+
+
+
+
Print
+
+
+
+
Loading...
+
+
+
+
+
+
+
+
diff --git a/extension/chrome/elements/pgp_render_block.ts b/extension/chrome/elements/pgp_render_block.ts
new file mode 100644
index 00000000000..06adecb9ef0
--- /dev/null
+++ b/extension/chrome/elements/pgp_render_block.ts
@@ -0,0 +1,66 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+import { Url } from '../../js/common/core/common.js';
+import { Assert } from '../../js/common/assert.js';
+import { Ui } from '../../js/common/browser/ui.js';
+import { View } from '../../js/common/view.js';
+import { PgpBaseBlockView } from './pgp_base_block_view.js';
+import { RenderMessage } from '../../js/common/render-message.js';
+
+export class PgpRenderBlockView extends PgpBaseBlockView {
+ public readonly debug: boolean;
+
+ public constructor() {
+ Ui.event.protect();
+ const uncheckedUrlParams = Url.parse(['frameId', 'parentTabId', 'debug']);
+ super(Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'), Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId'));
+ this.debug = uncheckedUrlParams.debug === true;
+ window.addEventListener('message', this.handleMessage, true);
+ }
+
+ public render = async () => {
+ // await this.renderModule.initPrintView();
+ };
+
+ public setHandlers = () => {
+ /*
+ $('.pgp_print_button').on(
+ 'click',
+ this.setHandler(() => this.renderModule.printPGPBlock())
+ );
+ */
+ };
+
+ private handleMessage = (event: MessageEvent) => {
+ const data = event.data as RenderMessage;
+ // todo: order better
+ if (data?.renderEncryptionStatus) {
+ this.renderModule.renderEncryptionStatus(data.renderEncryptionStatus);
+ }
+ if (data?.renderSignatureStatus) {
+ this.renderModule.renderSignatureStatus(data.renderSignatureStatus); // todo: "offline"->click->reload?
+ }
+ if (data?.renderText) {
+ this.renderModule.renderText(data.renderText);
+ }
+ if (data?.resizePgpBlockFrame) {
+ this.renderModule.resizePgpBlockFrame();
+ }
+ if (data?.separateQuotedContentAndRenderText) {
+ this.quoteModule.separateQuotedContentAndRenderText(
+ data.separateQuotedContentAndRenderText.decryptedContent,
+ data.separateQuotedContentAndRenderText.isHtml
+ );
+ }
+ if (data?.setFrameColor) {
+ this.renderModule.setFrameColor(data.setFrameColor);
+ }
+ if (data?.setTestState) {
+ Ui.setTestState(data.setTestState);
+ }
+ };
+}
+
+View.run(PgpRenderBlockView);
diff --git a/extension/js/common/message-renderer.ts b/extension/js/common/message-renderer.ts
index 03696367c13..776491d72e7 100644
--- a/extension/js/common/message-renderer.ts
+++ b/extension/js/common/message-renderer.ts
@@ -5,12 +5,18 @@
import { GmailParser, GmailRes } from './api/email-provider/gmail/gmail-parser.js';
import { Attachment } from './core/attachment.js';
import { Buf } from './core/buf.js';
-import { Dict, Str } from './core/common.js';
+import { CID_PATTERN, Dict, Str } from './core/common.js';
+import { KeyUtil } from './core/crypto/key.js';
+import { MsgUtil, VerifyRes } from './core/crypto/pgp/msg-util.js';
import { Mime, MimeContent, MimeProccesedMsg } from './core/mime.js';
+import { MsgBlockParser } from './core/msg-block-parser.js';
import { MsgBlock } from './core/msg-block.js';
import { SendAsAlias } from './platform/store/acct-store.js';
+import { ContactStore } from './platform/store/contact-store.js';
import { Xss } from './platform/xss.js';
+import { RenderInterface } from './render-interface.js';
import { XssSafeFactory } from './xss-safe-factory.js';
+import * as DOMPurify from 'dompurify';
export type ProccesedMsg = MimeProccesedMsg;
@@ -20,6 +26,38 @@ export type AttachmentBlock = {
};
export class MessageRenderer {
+ public static renderSignedMessage = async (raw: string, renderModule: RenderInterface, signerEmail: string) => {
+ // ... from PgpBlockViewDecryptModule.initialize
+ const mimeMsg = Buf.fromBase64UrlStr(raw);
+ const parsed = await Mime.decode(mimeMsg);
+ if (parsed && typeof parsed.rawSignedContent === 'string') {
+ const signatureAttachment = parsed.attachments.find(a => a.treatAs(parsed.attachments) === 'signature'); // todo: more than one signature candidate?
+ if (signatureAttachment) {
+ const parsedSignature = signatureAttachment.getData().toUtfStr();
+ // ... from PgpBlockViewDecryptModule.decryptAndRender
+ const sigText = parsedSignature.replace('\n=3D', '\n=');
+ const encryptedData = parsed.rawSignedContent;
+ try {
+ const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, signerEmail))?.sortedPubkeys ?? [];
+ // todo: we don't actually need parsed pubs here because we're going to pass them to the backgorund page
+ // maybe we can have a method in ContactStore to extract armored keys
+ const verificationPubs = parsedPubs.map(key => KeyUtil.armor(key.pubkey));
+ const verify = async (verificationPubs: string[]) => await MsgUtil.verifyDetached({ plaintext: encryptedData, sigText, verificationPubs });
+ const signatureResult = await verify(verificationPubs);
+ // todo: retry verification with looked up keys?
+ await MessageRenderer.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult, renderModule);
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ }
+ /* todo: await this.view.errorModule.renderErr(
+ 'Error: could not properly parse signed message',
+ parsed.rawSignedContent || parsed.text || parsed.html || mimeMsg.toUtfStr(),
+ 'parse error'
+ ); */
+ };
+
public static renderMsg = (
{ from, blocks }: { blocks: MsgBlock[]; from?: string },
factory: XssSafeFactory,
@@ -65,4 +103,149 @@ export class MessageRenderer {
attachments,
};
};
+
+ /**
+ * Replaces inline image CID references with base64 encoded data in sanitized HTML
+ * and returns the sanitized HTML along with the inline CID attachments.
+ *
+ * @param html - The original HTML content.
+ * @param attachments - An array of email attachments.
+ * @returns An object containing sanitized HTML and an array of inline CID attachments.
+ */
+ public static replaceInlineImageCIDs = (html: string, attachments: Attachment[]): { sanitizedHtml: string; inlineCIDAttachments: Attachment[] } => {
+ // Array to store inline CID attachments
+ const inlineCIDAttachments: Attachment[] = [];
+
+ // Define the hook function for DOMPurify to process image elements after sanitizing attributes
+ const processImageElements = (node: Element | null) => {
+ // Ensure the node exists and has a 'src' attribute
+ if (!node || !('src' in node)) return;
+ const imageSrc = node.getAttribute('src') as string;
+ if (!imageSrc) return;
+ const matches = imageSrc.match(CID_PATTERN);
+
+ // Check if the src attribute contains a CID
+ if (matches && matches[1]) {
+ const contentId = matches[1];
+ const contentIdAttachment = attachments.find(attachment => attachment.cid === `<${contentId}>`);
+
+ // Replace the src attribute with a base64 encoded string
+ if (contentIdAttachment) {
+ inlineCIDAttachments.push(contentIdAttachment);
+ node.setAttribute('src', `data:${contentIdAttachment.type};base64,${contentIdAttachment.getData().toBase64Str()}`);
+ }
+ }
+ };
+
+ // Add the DOMPurify hook
+ DOMPurify.addHook('afterSanitizeAttributes', processImageElements);
+
+ // Sanitize the HTML and remove the DOMPurify hooks
+ const sanitizedHtml = DOMPurify.sanitize(html);
+ DOMPurify.removeAllHooks();
+
+ return { sanitizedHtml, inlineCIDAttachments };
+ };
+
+ public static getEncryptedSubjectText = (subject: string, isHtml: boolean) => {
+ if (isHtml) {
+ return ` Encrypted Subject:
+ ${Xss.escape(subject)}
+
+
`;
+ } else {
+ return `Encrypted Subject: ${subject}\n----------------------------------------------------------------------------------------------------\n`;
+ }
+ };
+
+ public static decideDecryptedContentFormattingAndRender = async (
+ decryptedBytes: Uint8Array | string,
+ isEncrypted: boolean,
+ sigResult: VerifyRes | undefined,
+ renderModule: RenderInterface,
+ plainSubject?: string
+ ) => {
+ if (isEncrypted) {
+ renderModule.renderEncryptionStatus('encrypted');
+ renderModule.setFrameColor('green');
+ } else {
+ renderModule.renderEncryptionStatus('not encrypted');
+ renderModule.setFrameColor('gray');
+ }
+ const publicKeys: string[] = [];
+ let renderableAttachments: Attachment[] = [];
+ let decryptedContent: string | undefined;
+ let isHtml = false;
+ // todo - replace with MsgBlockParser.fmtDecryptedAsSanitizedHtmlBlocks, then the extract/strip methods could be private?
+ if (!Mime.resemblesMsg(decryptedBytes)) {
+ const fcAttachmentBlocks: MsgBlock[] = [];
+ decryptedContent = Str.with(decryptedBytes);
+ decryptedContent = MsgBlockParser.extractFcAttachments(decryptedContent, fcAttachmentBlocks);
+ decryptedContent = MsgBlockParser.stripFcTeplyToken(decryptedContent);
+ decryptedContent = MsgBlockParser.stripPublicKeys(decryptedContent, publicKeys);
+ if (fcAttachmentBlocks.length) {
+ renderableAttachments = fcAttachmentBlocks.map(
+ attachmentBlock => new Attachment(attachmentBlock.attachmentMeta!) // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ );
+ }
+ } else {
+ renderModule.renderText('Formatting...');
+ const decoded = await Mime.decode(decryptedBytes);
+ let inlineCIDAttachments: Attachment[] = [];
+ if (typeof decoded.html !== 'undefined') {
+ ({ sanitizedHtml: decryptedContent, inlineCIDAttachments } = MessageRenderer.replaceInlineImageCIDs(decoded.html, decoded.attachments));
+ isHtml = true;
+ } else if (typeof decoded.text !== 'undefined') {
+ decryptedContent = decoded.text;
+ } else {
+ decryptedContent = '';
+ }
+ if (
+ decoded.subject &&
+ isEncrypted &&
+ (!plainSubject || !Mime.subjectWithoutPrefixes(plainSubject).includes(Mime.subjectWithoutPrefixes(decoded.subject)))
+ ) {
+ // there is an encrypted subject + (either there is no plain subject or the plain subject does not contain what's in the encrypted subject)
+ decryptedContent = MessageRenderer.getEncryptedSubjectText(decoded.subject, isHtml) + decryptedContent; // render encrypted subject in message
+ }
+ for (const attachment of decoded.attachments) {
+ if (attachment.isPublicKey()) {
+ publicKeys.push(attachment.getData().toUtfStr());
+ } else if (!inlineCIDAttachments.some(inlineAttachment => inlineAttachment.cid === attachment.cid)) {
+ renderableAttachments.push(attachment);
+ }
+ }
+ }
+ renderModule.separateQuotedContentAndRenderText(decryptedContent, isHtml); // todo: quoteModule ?
+ MessageRenderer.renderPgpSignatureCheckResult(renderModule, sigResult);
+ if (isEncrypted && publicKeys.length) {
+ // todo: BrowserMsg.send.renderPublicKeys(this.view.parentTabId, { afterFrameId: this.view.frameId, publicKeys });
+ }
+ if (renderableAttachments.length) {
+ // todo: this.view.attachmentsModule.renderInnerAttachments(renderableAttachments, isEncrypted);
+ }
+ renderModule.resizePgpBlockFrame();
+ renderModule.setTestState('ready');
+ };
+
+ public static renderPgpSignatureCheckResult = (renderModule: RenderInterface, verifyRes: VerifyRes | undefined) => {
+ if (verifyRes?.error) {
+ /* todo: if (not raw) {
+ // Sometimes the signed content is slightly modified when parsed from DOM,
+ // so the message should be re-fetched straight from API to make sure we get the original signed data and verify again
+ this.view.signature.parsedSignature = undefined; // force to re-parse
+ await this.view.decryptModule.initialize(verificationPubs, true);
+ return;
+ } */
+ renderModule.renderSignatureStatus(`error verifying signature: ${verifyRes.error}`);
+ renderModule.setFrameColor('red');
+ } else if (!verifyRes || !verifyRes.signerLongids.length) {
+ renderModule.renderSignatureStatus('not signed');
+ } else if (verifyRes.match) {
+ renderModule.renderSignatureStatus('signed');
+ } else {
+ // todo: renderModule.renderMissingPubkeyOrBadSignature(verifyRes);
+ }
+ renderModule.setTestState('ready');
+ };
}
diff --git a/extension/js/common/render-interface.ts b/extension/js/common/render-interface.ts
new file mode 100644
index 00000000000..e98d53f130e
--- /dev/null
+++ b/extension/js/common/render-interface.ts
@@ -0,0 +1,13 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+export interface RenderInterface {
+ setTestState(state: 'ready' | 'working' | 'waiting'): void;
+ resizePgpBlockFrame(): void;
+ separateQuotedContentAndRenderText(decryptedContent: string, isHtml: boolean): void;
+ renderText(text: string): void;
+ setFrameColor(color: 'red' | 'green' | 'gray'): void;
+ renderEncryptionStatus(status: string): void;
+ renderSignatureStatus(status: string): void; // todo: need to implement "offline error"->"click"->retry scenario
+}
diff --git a/extension/js/common/render-message.ts b/extension/js/common/render-message.ts
new file mode 100644
index 00000000000..ecbdc90efdc
--- /dev/null
+++ b/extension/js/common/render-message.ts
@@ -0,0 +1,13 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+export type RenderMessage = {
+ setTestState?: 'ready' | 'working' | 'waiting';
+ resizePgpBlockFrame?: boolean;
+ separateQuotedContentAndRenderText?: { decryptedContent: string; isHtml: boolean };
+ renderText?: string;
+ setFrameColor?: 'green' | 'gray' | 'red';
+ renderEncryptionStatus?: string;
+ renderSignatureStatus?: string;
+};
diff --git a/extension/js/common/render-relay.ts b/extension/js/common/render-relay.ts
new file mode 100644
index 00000000000..ad7885b4614
--- /dev/null
+++ b/extension/js/common/render-relay.ts
@@ -0,0 +1,40 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+import { RenderInterface } from './render-interface.js';
+import { RenderMessage } from './render-message.js';
+
+export class RenderRelay implements RenderInterface {
+ public constructor(private frameWindow: Window) {}
+ public setTestState = (state: 'ready' | 'working' | 'waiting') => {
+ this.relay({ setTestState: state });
+ };
+ public resizePgpBlockFrame = () => {
+ this.relay({ resizePgpBlockFrame: true });
+ };
+
+ public separateQuotedContentAndRenderText = (decryptedContent: string, isHtml: boolean) => {
+ this.relay({ separateQuotedContentAndRenderText: { decryptedContent, isHtml } });
+ };
+
+ public renderText = (text: string) => {
+ this.relay({ renderText: text });
+ };
+
+ public setFrameColor = (color: 'green' | 'gray' | 'red') => {
+ this.relay({ setFrameColor: color });
+ };
+
+ public renderEncryptionStatus = (status: string) => {
+ this.relay({ renderEncryptionStatus: status });
+ };
+
+ public renderSignatureStatus = (status: string) => {
+ this.relay({ renderSignatureStatus: status });
+ };
+
+ private relay = (message: RenderMessage) => {
+ this.frameWindow.postMessage(message, '*'); // todo: targetOrigin
+ };
+}
diff --git a/extension/js/common/xss-safe-factory.ts b/extension/js/common/xss-safe-factory.ts
index d36fcad36c2..79411ade2d2 100644
--- a/extension/js/common/xss-safe-factory.ts
+++ b/extension/js/common/xss-safe-factory.ts
@@ -105,6 +105,16 @@ export class XssSafeFactory {
return blocks.map(block => XssSafeFactory.renderableMsgBlock(factory, block, msgId, senderEmail, isOutgoing)).join('\n\n');
};
+ public static getWindowOfEmbeddedMsg = (frameId: string): Window | undefined => {
+ // const iframe = document.getElementById(frameId) as HTMLIFrameElement;
+ const iframe = $(`iframe#${frameId}`).get(0) as HTMLIFrameElement;
+ if (iframe?.contentWindow) {
+ return iframe?.contentWindow;
+ }
+ Catch.report('Unable to find iframe by frameId=' + frameId);
+ return undefined;
+ };
+
public srcImg = (relPath: string) => {
return this.extUrl(`img/${relPath}`);
};
@@ -162,6 +172,16 @@ export class XssSafeFactory {
});
};
+ public srcPgpRenderBlockIframe = () => {
+ const frameId = this.newId();
+ return {
+ frameId,
+ frameSrc: this.frameSrc(this.extUrl('chrome/elements/pgp_render_block.htm'), {
+ frameId,
+ }),
+ };
+ };
+
public srcPgpPubkeyIframe = (armoredPubkey: string, isOutgoing?: boolean) => {
return this.frameSrc(this.extUrl('chrome/elements/pgp_pubkey.htm'), {
frameId: this.newId(),
@@ -228,6 +248,11 @@ export class XssSafeFactory {
return this.iframe(this.srcPgpBlockIframe(armored, msgId, isOutgoing, sender, signature), ['pgp_block', type]) + this.hideGmailNewMsgInThreadNotification;
};
+ public embeddedRenderMsg = () => {
+ const { frameId, frameSrc } = this.srcPgpRenderBlockIframe();
+ return { frameId, frameHtml: this.iframe(frameSrc, ['pgp_block']) + this.hideGmailNewMsgInThreadNotification };
+ };
+
public embeddedPubkey = (armoredPubkey: string, isOutgoing?: boolean) => {
return this.iframe(this.srcPgpPubkeyIframe(armoredPubkey, isOutgoing), ['pgp_block', 'publicKey']);
};
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index cc9e9bdd761..7660438688c 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -26,15 +26,17 @@ import { SendAsAlias } from '../../common/platform/store/acct-store.js';
import { ContactStore } from '../../common/platform/store/contact-store.js';
import { Buf } from '../../common/core/buf.js';
import { MessageRenderer } from '../../common/message-renderer.js';
+import { RenderRelay } from '../../common/render-relay.js';
import { Mime } from '../../common/core/mime.js';
import { MsgUtil } from '../../common/core/crypto/pgp/msg-util.js';
+import { RenderInterface } from '../../common/render-interface.js';
type JQueryEl = JQuery;
type ProcessedMessage = { renderedXssSafe?: string; attachments: Attachment[] };
interface MessageCacheEntry {
- full: Promise;
+ download: { full: Promise; raw?: Promise };
processedFull?: ProcessedMessage;
}
@@ -168,12 +170,20 @@ export class GmailElementReplacer implements WebmailElementReplacer {
// todo: retries? exceptions?
let msgDownload = this.messages[msgId];
if (!msgDownload) {
- this.messages[msgId] = { full: this.gmail.msgGet(msgId, 'full') };
+ this.messages[msgId] = { download: { full: this.gmail.msgGet(msgId, 'full') } };
msgDownload = this.messages[msgId];
}
return msgDownload;
};
+ private msgGetRaw = async (msgId: string): Promise => {
+ const msgDownload = this.msgGetCached(msgId).download;
+ if (!msgDownload.raw) {
+ msgDownload.raw = this.gmail.msgGet(msgId, 'raw');
+ }
+ return (await msgDownload.raw).raw || '';
+ };
+
private queueAttachmentChunkDownload = (a: Attachment) => {
if (a.hasData()) {
return { attachment: a, result: Promise.resolve(a.getData()) };
@@ -199,8 +209,9 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (msgDownload.processedFull) {
return msgDownload.processedFull;
}
- const msg = await msgDownload.full;
- const mimeContent = MessageRenderer.reconstructMimeContent(msg);
+ const fullOrRawMsg = await Promise.race(Object.values(msgDownload.download).filter(Boolean));
+ // todo: what should we do if we received 'raw'?
+ const mimeContent = MessageRenderer.reconstructMimeContent(fullOrRawMsg);
const blocks = Mime.processBody(mimeContent);
// todo: only start `signature` download?
// start download of all attachments that are not plainFile, for 'needChunk' -- chunked download
@@ -221,7 +232,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
if (blocks.length === 0 || (blocks.length === 1 && ['plainText', 'plainHtml'].includes(blocks[0].type))) {
// only has single block which is plain text
} else {
- const from = GmailParser.findHeader(msg, 'from');
+ const from = GmailParser.findHeader(fullOrRawMsg, 'from');
({ renderedXssSafe } = MessageRenderer.renderMsg({ blocks, from }, this.factory, false, msgId, this.sendAs));
}
msgDownload.processedFull = { renderedXssSafe, attachments: mimeContent.attachments };
@@ -599,8 +610,16 @@ export class GmailElementReplacer implements WebmailElementReplacer {
} else if (treatAs === 'privateKey') {
return await this.renderBackupFromFile(a, attachmentsContainerInner, msgElReference.msgEl);
} else if (treatAs === 'signature') {
- const embeddedSignedMsgXssSafe = this.factory.embeddedMsg('signedMsg', '', msgId, false, senderEmail, true);
+ // todo: generate frameId here, prevent frameId duplicates?
+ const { frameId, frameHtml: embeddedSignedMsgXssSafe } = this.factory.embeddedRenderMsg();
msgElReference.msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgElReference.msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
+ const frameWindow = XssSafeFactory.getWindowOfEmbeddedMsg(frameId);
+ if (frameWindow) {
+ const renderModule = new RenderRelay(frameWindow);
+ this.processSignedMessage(msgId, renderModule, senderEmail).catch(Catch.reportErr); // todo: clear cached items
+ } else {
+ Catch.report('Unexpected: unable to reference a newly created message frame'); // todo:
+ }
return 'hidden'; // native attachment should be hidden, the "attachment" goes to the message container
} else {
// standard file
@@ -621,6 +640,16 @@ export class GmailElementReplacer implements WebmailElementReplacer {
}
};
+ private processSignedMessage = async (msgId: string, renderModule: RenderInterface, senderEmail: string) => {
+ try {
+ renderModule.renderText('Loading signed message...');
+ const raw = await this.msgGetRaw(msgId);
+ await MessageRenderer.renderSignedMessage(raw, renderModule, senderEmail);
+ } catch {
+ // todo: render error via renderModule
+ }
+ };
+
private processGoogleDriveAttachments = async (msgId: string, msgEl: JQueryEl, attachmentsContainerInner: JQueryEl) => {
const notProcessedAttachmentsLoaders = attachmentsContainerInner.find('.attachment_loader');
if (notProcessedAttachmentsLoaders.length && msgEl.find('.gmail_drive_chip, a[href^="https://drive.google.com/file"]').length) {
diff --git a/extension/manifest.json b/extension/manifest.json
index 9a1161f5ad1..7d1b7fc67b0 100644
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -42,7 +42,22 @@
{
"matches": ["https://mail.google.com/*"],
"css": ["/css/webmail.css", "/css/sweetalert2.css"],
- "js": ["/lib/purify.js", "/lib/jquery.min.js", "/lib/openpgp.js", "/lib/sweetalert2.js", "/lib/streams_web.js", "/js/content_scripts/webmail_bundle.js"]
+ "js": [
+ "/lib/purify.js",
+ "/lib/jquery.min.js",
+ "/lib/openpgp.js",
+ "/lib/sweetalert2.js",
+ "/lib/streams_web.js",
+ "/lib/emailjs/punycode.js",
+ "/lib/iso-8859-2.js",
+ "/lib/emailjs/emailjs-stringencoding.js",
+ "/lib/emailjs/emailjs-mime-codec.js",
+ "/lib/emailjs/emailjs-mime-types.js",
+ "/lib/emailjs/emailjs-addressparser.js",
+ "/lib/emailjs/emailjs-mime-builder.js",
+ "/lib/emailjs/emailjs-mime-parser.js",
+ "/js/content_scripts/webmail_bundle.js"
+ ]
},
{
"matches": ["https://www.google.com/robots.txt*"],
@@ -66,6 +81,7 @@
"/img/logo/flowcrypt-logo-19-19.png",
"/chrome/elements/compose.htm",
"/chrome/elements/pgp_block.htm",
+ "/chrome/elements/pgp_render_block.htm",
"/chrome/elements/setup_dialog.htm",
"/chrome/elements/attachment.htm",
"/chrome/elements/attachment_preview.htm",
From c6ffa6dbdb38d0ecb46e391649d6471afaa48904 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sat, 22 Apr 2023 11:39:25 +0100
Subject: [PATCH 013/147] test fix
---
test/source/tests/decrypt.ts | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts
index cc93b2caf98..d11aaa569dc 100644
--- a/test/source/tests/decrypt.ts
+++ b/test/source/tests/decrypt.ts
@@ -90,26 +90,25 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te
);
test(
- `decrypt - parsed signed message with signautre.asc as plain attachment`,
+ `decrypt - parsed signed message with signature.asc as plain attachment`,
testWithBrowser(async (t, browser) => {
const threadId = '187085b874fb727c';
const acctEmail = 'flowcrypt.compatibility@gmail.com';
t.mockApi!.configProvider = new ConfigurationProvider({
attester: singlePubKeyAttesterConfig(acctEmail, somePubkey),
});
- await BrowserRecipe.setUpCommonAcct(t, browser, 'compatibility');
+ const { accessToken } = await BrowserRecipe.setUpCommonAcct(t, browser, 'compatibility');
+ const expectedContent = 'flowcrypt-browser issue #5029 test email';
const inboxPage = await browser.newExtensionPage(t, `chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`);
await inboxPage.waitForSelTestState('ready');
await inboxPage.waitAll('iframe');
const pgpBlock = await inboxPage.getFrame(['pgp_block.htm']);
- const expectedContent = 'flowcrypt-browser issue #5029 test email';
await pgpBlock.waitForContent('@pgp-block-content', expectedContent);
- const accessToken = await BrowserRecipe.getGoogleAccessToken(inboxPage, acctEmail);
await inboxPage.close();
const extraAuthHeaders = { Authorization: `Bearer ${accessToken}` }; // eslint-disable-line @typescript-eslint/naming-convention
const gmailPage = await browser.newPage(t, `${t.urls?.mockGmailUrl()}/${threadId}`, undefined, extraAuthHeaders);
await gmailPage.waitAll('iframe');
- const pgpBlock2 = await gmailPage.getFrame(['pgp_block.htm']);
+ const pgpBlock2 = await gmailPage.getFrame(['pgp_render_block.htm']);
await pgpBlock2.waitForContent('@pgp-block-content', expectedContent);
await gmailPage.close();
})
From 43219bf63e95ad0f774e2d9153f8b13f0eac1bc1 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sat, 22 Apr 2023 14:27:59 +0100
Subject: [PATCH 014/147] fixed relay renderer to await for the destination
frame readyToReceive message
---
extension/chrome/elements/pgp_render_block.ts | 3 +-
.../js/common/relay-manager-interface.ts | 9 ++++
extension/js/common/relay-manager.ts | 42 +++++++++++++++++++
extension/js/common/render-relay.ts | 5 ++-
.../webmail/gmail-element-replacer.ts | 29 ++++---------
.../webmail/setup-webmail-content-script.ts | 13 +++++-
.../js/content_scripts/webmail/webmail.ts | 8 ++--
7 files changed, 80 insertions(+), 29 deletions(-)
create mode 100644 extension/js/common/relay-manager-interface.ts
create mode 100644 extension/js/common/relay-manager.ts
diff --git a/extension/chrome/elements/pgp_render_block.ts b/extension/chrome/elements/pgp_render_block.ts
index 06adecb9ef0..9e99bff856f 100644
--- a/extension/chrome/elements/pgp_render_block.ts
+++ b/extension/chrome/elements/pgp_render_block.ts
@@ -17,7 +17,8 @@ export class PgpRenderBlockView extends PgpBaseBlockView {
const uncheckedUrlParams = Url.parse(['frameId', 'parentTabId', 'debug']);
super(Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'), Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId'));
this.debug = uncheckedUrlParams.debug === true;
- window.addEventListener('message', this.handleMessage, true);
+ window.addEventListener('message', this.handleMessage, true); // todo: capture?
+ window.addEventListener('load', () => window.parent.postMessage({ readyToReceive: this.frameId }, '*'));
}
public render = async () => {
diff --git a/extension/js/common/relay-manager-interface.ts b/extension/js/common/relay-manager-interface.ts
new file mode 100644
index 00000000000..df44c5ebfd6
--- /dev/null
+++ b/extension/js/common/relay-manager-interface.ts
@@ -0,0 +1,9 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+import { RenderMessage } from './render-message.js';
+
+export interface RelayManagerInterface {
+ relay(frameId: string, message: RenderMessage): void;
+}
diff --git a/extension/js/common/relay-manager.ts b/extension/js/common/relay-manager.ts
new file mode 100644
index 00000000000..0284617d66b
--- /dev/null
+++ b/extension/js/common/relay-manager.ts
@@ -0,0 +1,42 @@
+/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
+
+'use strict';
+
+import { Dict } from './core/common.js';
+import { RelayManagerInterface } from './relay-manager-interface.js';
+import { RenderInterface } from './render-interface.js';
+import { RenderMessage } from './render-message.js';
+import { RenderRelay } from './render-relay.js';
+
+export class RelayManager implements RelayManagerInterface {
+ private frames: Dict<{ frameWindow: Window; readyToReceive?: true; queue: RenderMessage[] }> = {};
+
+ public relay = (frameId: string, message: RenderMessage) => {
+ const frameData = this.frames[frameId];
+ frameData.queue.push(message);
+ if (frameData.readyToReceive) {
+ this.flush(frameData);
+ }
+ };
+
+ public createRelay = (frameId: string, frameWindow: Window): RenderInterface => {
+ this.frames[frameId] = { frameWindow, queue: [] }; // can readyToReceive message come earlier? Probably not.
+ return new RenderRelay(this, frameId);
+ };
+
+ public readyToReceive = (frameId: string) => {
+ const frameData = this.frames[frameId];
+ frameData.readyToReceive = true;
+ this.flush(frameData);
+ };
+
+ private flush = ({ frameWindow, queue }: { frameWindow: Window; queue: RenderMessage[] }) => {
+ while (true) {
+ const message = queue.shift();
+ if (message) {
+ frameWindow.postMessage(message, '*'); // todo: targetOrigin
+ // todo: if ready status, release resources -- callback function?
+ } else break;
+ }
+ };
+}
diff --git a/extension/js/common/render-relay.ts b/extension/js/common/render-relay.ts
index ad7885b4614..fc294e809e8 100644
--- a/extension/js/common/render-relay.ts
+++ b/extension/js/common/render-relay.ts
@@ -2,11 +2,12 @@
'use strict';
+import { RelayManagerInterface } from './relay-manager-interface.js';
import { RenderInterface } from './render-interface.js';
import { RenderMessage } from './render-message.js';
export class RenderRelay implements RenderInterface {
- public constructor(private frameWindow: Window) {}
+ public constructor(private relayManager: RelayManagerInterface, private frameId: string) {}
public setTestState = (state: 'ready' | 'working' | 'waiting') => {
this.relay({ setTestState: state });
};
@@ -35,6 +36,6 @@ export class RenderRelay implements RenderInterface {
};
private relay = (message: RenderMessage) => {
- this.frameWindow.postMessage(message, '*'); // todo: targetOrigin
+ this.relayManager.relay(this.frameId, message);
};
}
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 7660438688c..648a0870212 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -26,7 +26,7 @@ import { SendAsAlias } from '../../common/platform/store/acct-store.js';
import { ContactStore } from '../../common/platform/store/contact-store.js';
import { Buf } from '../../common/core/buf.js';
import { MessageRenderer } from '../../common/message-renderer.js';
-import { RenderRelay } from '../../common/render-relay.js';
+import { RelayManager } from '../../common/relay-manager.js';
import { Mime } from '../../common/core/mime.js';
import { MsgUtil } from '../../common/core/crypto/pgp/msg-util.js';
import { RenderInterface } from '../../common/render-interface.js';
@@ -45,16 +45,10 @@ export class GmailElementReplacer implements WebmailElementReplacer {
private gmail: Gmail;
private recipientHasPgpCache: Dict = {};
- private sendAs: Dict;
private messages: Dict = {};
private chunkDownloads: { attachment: Attachment; result: Promise }[] = [];
// private attachmentDownloads: { attachment: Attachment; result: Promise }[] = [];
- private factory: XssSafeFactory;
- private clientConfiguration: ClientConfiguration;
private pubLookup: PubLookup;
- private acctEmail: string;
- private injector: Injector;
- private notifications: Notifications;
private webmailCommon: WebmailCommon;
private currentlyEvaluatingStandardComposeBoxRecipients = false;
private currentlyReplacingAttachments = false;
@@ -85,21 +79,16 @@ export class GmailElementReplacer implements WebmailElementReplacer {
};
public constructor(
- factory: XssSafeFactory,
- clientConfiguration: ClientConfiguration,
- acctEmail: string,
- sendAs: Dict,
- injector: Injector,
- notifications: Notifications
+ private factory: XssSafeFactory,
+ private clientConfiguration: ClientConfiguration,
+ private acctEmail: string,
+ private sendAs: Dict,
+ private injector: Injector,
+ private notifications: Notifications,
+ private relayManager: RelayManager
) {
- this.factory = factory;
- this.acctEmail = acctEmail;
- this.sendAs = sendAs;
- this.injector = injector;
- this.notifications = notifications;
this.webmailCommon = new WebmailCommon(acctEmail, injector);
this.gmail = new Gmail(acctEmail);
- this.clientConfiguration = clientConfiguration;
this.pubLookup = new PubLookup(this.clientConfiguration);
}
@@ -615,7 +604,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
msgElReference.msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgElReference.msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
const frameWindow = XssSafeFactory.getWindowOfEmbeddedMsg(frameId);
if (frameWindow) {
- const renderModule = new RenderRelay(frameWindow);
+ const renderModule = this.relayManager.createRelay(frameId, frameWindow);
this.processSignedMessage(msgId, renderModule, senderEmail).catch(Catch.reportErr); // todo: clear cached items
} else {
Catch.report('Unexpected: unable to reference a newly created message frame'); // todo:
diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts
index 96c573a7ce4..f159abc49ca 100644
--- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts
+++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts
@@ -24,6 +24,7 @@ import { AcctStore } from '../../common/platform/store/acct-store.js';
import { GlobalStore } from '../../common/platform/store/global-store.js';
import { InMemoryStore } from '../../common/platform/store/in-memory-store.js';
import { WebmailVariantString, XssSafeFactory } from '../../common/xss-safe-factory.js';
+import { RelayManager } from '../../common/relay-manager.js';
export type WebmailVariantObject = {
newDataLayer: undefined | boolean;
@@ -44,7 +45,8 @@ type WebmailSpecificInfo = {
inject: Injector,
notifications: Notifications,
factory: XssSafeFactory,
- notifyMurdered: () => void
+ notifyMurdered: () => void,
+ relayManager: RelayManager
) => Promise;
};
export interface WebmailElementReplacer {
@@ -433,7 +435,14 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi
ppEvent,
Catch.try(() => notifyExpiringKeys(acctEmail, clientConfiguration, notifications))
);
- await webmailSpecific.start(acctEmail, clientConfiguration, inject, notifications, factory, notifyMurdered);
+ const relayManager = new RelayManager();
+ window.addEventListener('message', e => {
+ const regex = new RegExp(`^(chrome|moz)-extension://${chrome.runtime.id}$`);
+ if (regex.test(e.origin) && typeof e.data?.readyToReceive === 'string') {
+ relayManager.readyToReceive(e.data.readyToReceive);
+ }
+ });
+ await webmailSpecific.start(acctEmail, clientConfiguration, inject, notifications, factory, notifyMurdered, relayManager);
} catch (e) {
if (e instanceof TabIdRequiredError) {
console.error(`FlowCrypt cannot start: ${String(e)}`);
diff --git a/extension/js/content_scripts/webmail/webmail.ts b/extension/js/content_scripts/webmail/webmail.ts
index 7b321afef07..33ed4114d7f 100644
--- a/extension/js/content_scripts/webmail/webmail.ts
+++ b/extension/js/content_scripts/webmail/webmail.ts
@@ -4,8 +4,6 @@
// todo - a few things are duplicated here, refactor
-///
-
import { WebmailVariantObject, contentScriptSetupIfVacant } from './setup-webmail-content-script.js';
import { Catch } from '../../common/platform/catch.js';
import { ContentScriptWindow } from '../../common/browser/browser-window.js';
@@ -17,6 +15,7 @@ import { Str } from '../../common/core/common.js';
import { XssSafeFactory } from '../../common/xss-safe-factory.js';
import { ClientConfiguration } from '../../common/client-configuration.js';
import { AcctStore } from '../../common/platform/store/acct-store.js';
+import { RelayManager } from '../../common/relay-manager.js';
Catch.try(async () => {
const gmailWebmailStartup = async () => {
@@ -98,7 +97,8 @@ Catch.try(async () => {
injector: Injector,
notifications: Notifications,
factory: XssSafeFactory,
- notifyMurdered: () => void
+ notifyMurdered: () => void,
+ relayManager: RelayManager
) => {
hijackGmailHotkeys();
const storage = await AcctStore.get(acctEmail, ['sendAs', 'full_name']);
@@ -107,7 +107,7 @@ Catch.try(async () => {
storage.sendAs[acctEmail] = { name: storage.full_name, isPrimary: true };
}
injector.btns();
- replacer = new GmailElementReplacer(factory, clientConfiguration, acctEmail, storage.sendAs, injector, notifications);
+ replacer = new GmailElementReplacer(factory, clientConfiguration, acctEmail, storage.sendAs, injector, notifications, relayManager);
await notifications.showInitial(acctEmail);
const intervaliFunctions = replacer.getIntervalFunctions();
for (const intervalFunction of intervaliFunctions) {
From 21d8d1878db91fe52adb16a92d22ef8ff0c2a400 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sat, 22 Apr 2023 14:52:21 +0100
Subject: [PATCH 015/147] update test and recipe to check pgp block for
correctness
---
test/source/tests/decrypt.ts | 12 ++++++++----
test/source/tests/tooling/browser-recipe.ts | 11 +++++++++--
2 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts
index d11aaa569dc..a9afb7eda02 100644
--- a/test/source/tests/decrypt.ts
+++ b/test/source/tests/decrypt.ts
@@ -102,14 +102,18 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te
const inboxPage = await browser.newExtensionPage(t, `chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`);
await inboxPage.waitForSelTestState('ready');
await inboxPage.waitAll('iframe');
- const pgpBlock = await inboxPage.getFrame(['pgp_block.htm']);
- await pgpBlock.waitForContent('@pgp-block-content', expectedContent);
+ await BrowserRecipe.pgpBlockCheck(t, await inboxPage.getFrame(['pgp_block.htm']), {
+ encryption: 'not encrypted',
+ content: [expectedContent],
+ });
await inboxPage.close();
const extraAuthHeaders = { Authorization: `Bearer ${accessToken}` }; // eslint-disable-line @typescript-eslint/naming-convention
const gmailPage = await browser.newPage(t, `${t.urls?.mockGmailUrl()}/${threadId}`, undefined, extraAuthHeaders);
await gmailPage.waitAll('iframe');
- const pgpBlock2 = await gmailPage.getFrame(['pgp_render_block.htm']);
- await pgpBlock2.waitForContent('@pgp-block-content', expectedContent);
+ await BrowserRecipe.pgpBlockCheck(t, await gmailPage.getFrame(['pgp_render_block.htm']), {
+ encryption: 'not encrypted',
+ content: [expectedContent],
+ });
await gmailPage.close();
})
);
diff --git a/test/source/tests/tooling/browser-recipe.ts b/test/source/tests/tooling/browser-recipe.ts
index e6f24d75d1c..bc24a68dd56 100644
--- a/test/source/tests/tooling/browser-recipe.ts
+++ b/test/source/tests/tooling/browser-recipe.ts
@@ -13,6 +13,7 @@ import { testConstants } from './consts';
import { PageRecipe } from '../page-recipe/abstract-page-recipe';
import { InMemoryStoreKeys } from '../../core/const';
import { GmailPageRecipe } from '../page-recipe/gmail-page-recipe';
+import { expect } from 'chai';
export class BrowserRecipe {
public static oldAndNewComposeButtonSelectors = ['div.z0[class*="_destroyable"]', 'div.pb-25px[class*="_destroyable"]', '.new_secure_compose_window_button'];
@@ -208,19 +209,25 @@ export class BrowserRecipe {
}
}
}
+ const sigBadgeContent = await pgpBlockPage.read('@pgp-signature');
if (m.signature) {
- const sigBadgeContent = await pgpBlockPage.read('@pgp-signature');
if (sigBadgeContent !== m.signature) {
t.log(`found sig content:${sigBadgeContent}`);
throw new Error(`pgp_block_verify_decrypted_content:missing expected signature content:${m.signature}\nactual sig content:${sigBadgeContent}`);
}
+ } else {
+ // some badge still has to be present
+ expect(sigBadgeContent).to.be.ok;
}
+ const encBadgeContent = await pgpBlockPage.read('@pgp-encryption');
if (m.encryption) {
- const encBadgeContent = await pgpBlockPage.read('@pgp-encryption');
if (encBadgeContent !== m.encryption) {
t.log(`found enc content:${encBadgeContent}`);
throw new Error(`pgp_block_verify_decrypted_content:missing expected encryption content:${m.encryption}`);
}
+ } else {
+ // some badge still has to be present
+ expect(encBadgeContent).to.be.ok;
}
if (m.error) {
await pgpBlockPage.notPresent('@action-print');
From 4f881d9b41fe27e1a6d3abd7516a23c3150202fd Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sat, 22 Apr 2023 18:48:31 +0100
Subject: [PATCH 016/147] show signature badge
---
.../pgp-block-signature-module.ts | 26 +++----------------
extension/js/common/message-renderer.ts | 24 ++++++++++++++---
extension/js/common/render-interface.ts | 9 ++++---
3 files changed, 31 insertions(+), 28 deletions(-)
diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts
index 65b10bc4965..9d365b43756 100644
--- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts
+++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts
@@ -4,11 +4,11 @@
import { ApiErr } from '../../../js/common/api/shared/api-error.js';
import { Catch } from '../../../js/common/platform/catch.js';
-import { PgpBlockView } from '../pgp_block';
+import { PgpBlockView } from '../pgp_block.js';
import { Ui } from '../../../js/common/browser/ui.js';
import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js';
-import { Value } from '../../../js/common/core/common.js';
import { BrowserMsg } from '../../../js/common/browser/browser-msg.js';
+import { MessageRenderer } from '../../../js/common/message-renderer.js';
export class PgpBlockViewSignatureModule {
public constructor(private view: PgpBlockView) {}
@@ -48,7 +48,7 @@ export class PgpBlockViewSignatureModule {
await this.renderPgpSignatureCheckResult(await retryVerification(pubkeys), pubkeys, undefined);
return;
}
- this.renderMissingPubkeyOrBadSignature(verifyRes);
+ MessageRenderer.renderMissingPubkeyOrBadSignature(this.view.renderModule, verifyRes);
} catch (e) {
if (ApiErr.isSignificant(e)) {
Catch.reportErr(e);
@@ -63,28 +63,10 @@ export class PgpBlockViewSignatureModule {
}
} else {
// !retryVerification
- this.renderMissingPubkeyOrBadSignature(verifyRes);
+ MessageRenderer.renderMissingPubkeyOrBadSignature(this.view.renderModule, verifyRes);
}
}
this.view.renderModule.doNotSetStateAsReadyYet = false;
Ui.setTestState('ready');
};
-
- private renderMissingPubkey = (signerLongid: string) => {
- this.view.renderModule.renderSignatureStatus(`could not verify signature: missing pubkey ${signerLongid}`);
- };
-
- private renderBadSignature = () => {
- this.view.renderModule.renderSignatureStatus('bad signature');
- this.view.renderModule.setFrameColor('red'); // todo: in what other cases should we set the frame red?
- };
-
- private renderMissingPubkeyOrBadSignature = (verifyRes: VerifyRes): void => {
- // eslint-disable-next-line no-null/no-null
- if (verifyRes.match === null || !Value.arr.hasIntersection(verifyRes.signerLongids, verifyRes.suppliedLongids)) {
- this.renderMissingPubkey(verifyRes.signerLongids[0]);
- } else {
- this.renderBadSignature();
- }
- };
}
diff --git a/extension/js/common/message-renderer.ts b/extension/js/common/message-renderer.ts
index 776491d72e7..a77c44fa352 100644
--- a/extension/js/common/message-renderer.ts
+++ b/extension/js/common/message-renderer.ts
@@ -5,7 +5,7 @@
import { GmailParser, GmailRes } from './api/email-provider/gmail/gmail-parser.js';
import { Attachment } from './core/attachment.js';
import { Buf } from './core/buf.js';
-import { CID_PATTERN, Dict, Str } from './core/common.js';
+import { CID_PATTERN, Dict, Str, Value } from './core/common.js';
import { KeyUtil } from './core/crypto/key.js';
import { MsgUtil, VerifyRes } from './core/crypto/pgp/msg-util.js';
import { Mime, MimeContent, MimeProccesedMsg } from './core/mime.js';
@@ -14,7 +14,7 @@ import { MsgBlock } from './core/msg-block.js';
import { SendAsAlias } from './platform/store/acct-store.js';
import { ContactStore } from './platform/store/contact-store.js';
import { Xss } from './platform/xss.js';
-import { RenderInterface } from './render-interface.js';
+import { RenderInterface, RenderInterfaceBase } from './render-interface.js';
import { XssSafeFactory } from './xss-safe-factory.js';
import * as DOMPurify from 'dompurify';
@@ -244,8 +244,26 @@ export class MessageRenderer {
} else if (verifyRes.match) {
renderModule.renderSignatureStatus('signed');
} else {
- // todo: renderModule.renderMissingPubkeyOrBadSignature(verifyRes);
+ MessageRenderer.renderMissingPubkeyOrBadSignature(renderModule, verifyRes);
}
renderModule.setTestState('ready');
};
+
+ public static renderMissingPubkeyOrBadSignature = (renderModule: RenderInterfaceBase, verifyRes: VerifyRes): void => {
+ // eslint-disable-next-line no-null/no-null
+ if (verifyRes.match === null || !Value.arr.hasIntersection(verifyRes.signerLongids, verifyRes.suppliedLongids)) {
+ MessageRenderer.renderMissingPubkey(renderModule, verifyRes.signerLongids[0]);
+ } else {
+ MessageRenderer.renderBadSignature(renderModule);
+ }
+ };
+
+ public static renderMissingPubkey = (renderModule: RenderInterfaceBase, signerLongid: string) => {
+ renderModule.renderSignatureStatus(`could not verify signature: missing pubkey ${signerLongid}`);
+ };
+
+ public static renderBadSignature = (renderModule: RenderInterfaceBase) => {
+ renderModule.renderSignatureStatus('bad signature');
+ renderModule.setFrameColor('red'); // todo: in what other cases should we set the frame red?
+ };
}
diff --git a/extension/js/common/render-interface.ts b/extension/js/common/render-interface.ts
index e98d53f130e..9e76a469621 100644
--- a/extension/js/common/render-interface.ts
+++ b/extension/js/common/render-interface.ts
@@ -2,12 +2,15 @@
'use strict';
-export interface RenderInterface {
- setTestState(state: 'ready' | 'working' | 'waiting'): void;
+export interface RenderInterfaceBase {
resizePgpBlockFrame(): void;
- separateQuotedContentAndRenderText(decryptedContent: string, isHtml: boolean): void;
renderText(text: string): void;
setFrameColor(color: 'red' | 'green' | 'gray'): void;
renderEncryptionStatus(status: string): void;
renderSignatureStatus(status: string): void; // todo: need to implement "offline error"->"click"->retry scenario
}
+
+export interface RenderInterface extends RenderInterfaceBase {
+ setTestState(state: 'ready' | 'working' | 'waiting'): void;
+ separateQuotedContentAndRenderText(decryptedContent: string, isHtml: boolean): void;
+}
From ab682c9157e57a40f93be2ce9eef54c1bcca7abf Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sat, 22 Apr 2023 19:02:58 +0100
Subject: [PATCH 017/147] fix test recipe
---
test/source/tests/tooling/browser-recipe.ts | 41 +++++++++++----------
1 file changed, 22 insertions(+), 19 deletions(-)
diff --git a/test/source/tests/tooling/browser-recipe.ts b/test/source/tests/tooling/browser-recipe.ts
index bc24a68dd56..30e6eb1bea0 100644
--- a/test/source/tests/tooling/browser-recipe.ts
+++ b/test/source/tests/tooling/browser-recipe.ts
@@ -185,7 +185,7 @@ export class BrowserRecipe {
if (m.expectPercentageProgress) {
await pgpBlockPage.waitForContent('@pgp-block-content', /Retrieving message... \d+%/, 20, 10);
}
- await pgpBlockPage.waitForSelTestState('ready', 100);
+ await pgpBlockPage.waitForSelTestState('ready', 10000);
await Util.sleep(1);
if (m.quoted) {
await pgpBlockPage.waitAndClick('@action-show-quoted-content');
@@ -210,26 +210,10 @@ export class BrowserRecipe {
}
}
const sigBadgeContent = await pgpBlockPage.read('@pgp-signature');
- if (m.signature) {
- if (sigBadgeContent !== m.signature) {
- t.log(`found sig content:${sigBadgeContent}`);
- throw new Error(`pgp_block_verify_decrypted_content:missing expected signature content:${m.signature}\nactual sig content:${sigBadgeContent}`);
- }
- } else {
- // some badge still has to be present
- expect(sigBadgeContent).to.be.ok;
- }
const encBadgeContent = await pgpBlockPage.read('@pgp-encryption');
- if (m.encryption) {
- if (encBadgeContent !== m.encryption) {
- t.log(`found enc content:${encBadgeContent}`);
- throw new Error(`pgp_block_verify_decrypted_content:missing expected encryption content:${m.encryption}`);
- }
- } else {
- // some badge still has to be present
- expect(encBadgeContent).to.be.ok;
- }
if (m.error) {
+ expect(sigBadgeContent).to.be.empty;
+ expect(encBadgeContent).to.be.empty;
await pgpBlockPage.notPresent('@action-print');
const errBadgeContent = await pgpBlockPage.read('@pgp-error');
if (errBadgeContent !== m.error) {
@@ -240,6 +224,25 @@ export class BrowserRecipe {
if (!(await pgpBlockPage.isElementVisible('@action-print'))) {
throw new Error(`Print button is invisible`);
}
+ } else {
+ if (m.signature) {
+ if (sigBadgeContent !== m.signature) {
+ t.log(`found sig content:${sigBadgeContent}`);
+ throw new Error(`pgp_block_verify_decrypted_content:missing expected signature content:${m.signature}\nactual sig content:${sigBadgeContent}`);
+ }
+ } else {
+ // some badge still has to be present
+ expect(sigBadgeContent).to.be.ok;
+ }
+ if (m.encryption) {
+ if (encBadgeContent !== m.encryption) {
+ t.log(`found enc content:${encBadgeContent}`);
+ throw new Error(`pgp_block_verify_decrypted_content:missing expected encryption content:${m.encryption}`);
+ }
+ } else {
+ // some badge still has to be present
+ expect(encBadgeContent).to.be.ok;
+ }
}
};
}
From e202c16df14d88e53aab3cd5479b7186513dadbf Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sun, 23 Apr 2023 17:13:17 +0100
Subject: [PATCH 018/147] simplify tests
---
test/source/tests/decrypt.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts
index a9afb7eda02..a43c4fe9525 100644
--- a/test/source/tests/decrypt.ts
+++ b/test/source/tests/decrypt.ts
@@ -1603,8 +1603,6 @@ XZ8r4OC6sguP/yozWlkG+7dDxsgKQVBENeG6Lw==
await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, {
params,
content: [expectedContent],
- encryption: '',
- signature: '',
error: 'decrypt error',
});
})
@@ -1698,8 +1696,6 @@ XZ8r4OC6sguP/yozWlkG+7dDxsgKQVBENeG6Lw==
await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, {
params,
content: [],
- encryption: '',
- signature: '',
error: 'parse error',
});
})
From 7d6aa45a52b6fb27a77a957ac0b14f5fb0a0ce06 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Sun, 23 Apr 2023 17:14:30 +0100
Subject: [PATCH 019/147] fix test
---
test/source/tests/gmail.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts
index 799b5738e84..2d962db3435 100644
--- a/test/source/tests/gmail.ts
+++ b/test/source/tests/gmail.ts
@@ -236,7 +236,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
expect(pgpBlockUrls.length).to.equal(1);
await testMinimumElementHeight(gmailPage, '.pgp_block.signedMsg', 80);
await testMinimumElementHeight(gmailPage, '.pgp_block.publicKey', 120);
- const url = pgpBlockUrls[0].split('/chrome/elements/pgp_block.htm')[1];
+ const url = pgpBlockUrls[0].split('/chrome/elements/pgp_render_block.htm')[1];
await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, {
params: url,
content: ['1234'],
From e0197ab88ec456de2753d58aa6dc62fc19e626f8 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Wed, 26 Apr 2023 18:01:19 +0100
Subject: [PATCH 020/147] test fixes
---
extension/js/common/xss-safe-factory.ts | 6 ++++--
.../webmail/gmail-element-replacer.ts | 2 +-
test/source/tests/gmail.ts | 14 ++++++--------
3 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/extension/js/common/xss-safe-factory.ts b/extension/js/common/xss-safe-factory.ts
index 79411ade2d2..4d6b5285a18 100644
--- a/extension/js/common/xss-safe-factory.ts
+++ b/extension/js/common/xss-safe-factory.ts
@@ -248,9 +248,11 @@ export class XssSafeFactory {
return this.iframe(this.srcPgpBlockIframe(armored, msgId, isOutgoing, sender, signature), ['pgp_block', type]) + this.hideGmailNewMsgInThreadNotification;
};
- public embeddedRenderMsg = () => {
+ public embeddedRenderMsg = (
+ type: MsgBlockType // for diagnostic purposes
+ ) => {
const { frameId, frameSrc } = this.srcPgpRenderBlockIframe();
- return { frameId, frameHtml: this.iframe(frameSrc, ['pgp_block']) + this.hideGmailNewMsgInThreadNotification };
+ return { frameId, frameHtml: this.iframe(frameSrc, ['pgp_block', type]) + this.hideGmailNewMsgInThreadNotification };
};
public embeddedPubkey = (armoredPubkey: string, isOutgoing?: boolean) => {
diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
index 648a0870212..2d203a3ee81 100644
--- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts
+++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts
@@ -600,7 +600,7 @@ export class GmailElementReplacer implements WebmailElementReplacer {
return await this.renderBackupFromFile(a, attachmentsContainerInner, msgElReference.msgEl);
} else if (treatAs === 'signature') {
// todo: generate frameId here, prevent frameId duplicates?
- const { frameId, frameHtml: embeddedSignedMsgXssSafe } = this.factory.embeddedRenderMsg();
+ const { frameId, frameHtml: embeddedSignedMsgXssSafe } = this.factory.embeddedRenderMsg('signedMsg');
msgElReference.msgEl = this.updateMsgBodyEl_DANGEROUSLY(msgElReference.msgEl, 'set', embeddedSignedMsgXssSafe); // xss-safe-factory
const frameWindow = XssSafeFactory.getWindowOfEmbeddedMsg(frameId);
if (frameWindow) {
diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts
index 2d962db3435..a863b839dff 100644
--- a/test/source/tests/gmail.ts
+++ b/test/source/tests/gmail.ts
@@ -202,14 +202,13 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
const gmailPage = await openGmailPage(t, browser);
await gotoGmailPage(gmailPage, '/FMfcgzGkbDZKPJrNLplXZhKfWwtgjrXt');
// validate pgp_block.htm is rendered
- const pgpBlockUrls = await gmailPage.getFramesUrls(['/chrome/elements/pgp_block.htm'], {
+ const pgpBlockUrls = await gmailPage.getFramesUrls(['/chrome/elements/pgp_render_block.htm'], {
sleep: 10,
appearIn: 25,
});
expect(pgpBlockUrls.length).to.equal(1);
- const url = pgpBlockUrls[0].split('/chrome/elements/pgp_block.htm')[1];
- await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, {
- params: url,
+ const pgpBlockFrame = await gmailPage.getFrame([pgpBlockUrls[0]]);
+ await BrowserRecipe.pgpBlockCheck(t, pgpBlockFrame, {
content: ['1234'],
encryption: 'not encrypted',
signature: 'signed',
@@ -229,16 +228,15 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
const gmailPage = await openGmailPage(t, browser);
await gotoGmailPage(gmailPage, '/FMfcgzGkbDZKPKzSnGtGKZrPZSbTBNnB');
// validate pgp_block.htm is rendered
- const pgpBlockUrls = await gmailPage.getFramesUrls(['/chrome/elements/pgp_block.htm'], {
+ const pgpBlockUrls = await gmailPage.getFramesUrls(['/chrome/elements/pgp_render_block.htm'], {
sleep: 10,
appearIn: 25,
});
expect(pgpBlockUrls.length).to.equal(1);
await testMinimumElementHeight(gmailPage, '.pgp_block.signedMsg', 80);
await testMinimumElementHeight(gmailPage, '.pgp_block.publicKey', 120);
- const url = pgpBlockUrls[0].split('/chrome/elements/pgp_render_block.htm')[1];
- await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, {
- params: url,
+ const pgpBlockFrame = await gmailPage.getFrame([pgpBlockUrls[0]]);
+ await BrowserRecipe.pgpBlockCheck(t, pgpBlockFrame, {
content: ['1234'],
encryption: 'not encrypted',
signature: 'signed',
From 4f44474ad0406ca213308bc9daf857d2c28c3740 Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Fri, 28 Apr 2023 19:41:11 +0100
Subject: [PATCH 021/147] encrypted message relay rendering, inner attachments,
pubkeys inside encrypted attachments
---
extension/chrome/dev/export.ts | 2 +-
extension/chrome/elements/attachment.ts | 26 +++----
.../chrome/elements/attachment_preview.ts | 4 +-
.../compose-modules/compose-quote-module.ts | 46 ++++++------
.../chrome/elements/pgp_base_block_view.ts | 9 ++-
extension/chrome/elements/pgp_block.ts | 11 ++-
.../pgp-block-attachmens-module.ts | 4 +-
.../chrome/elements/pgp_render_block.htm | 1 +
extension/chrome/elements/pgp_render_block.ts | 13 +++-
.../inbox-active-thread-module.ts | 2 +-
.../api/email-provider/gmail/gmail-parser.ts | 6 +-
.../common/api/email-provider/gmail/gmail.ts | 4 +-
extension/js/common/core/attachment.ts | 72 +++++++++++--------
extension/js/common/core/mime.ts | 9 ++-
extension/js/common/message-renderer.ts | 64 ++++++++++++-----
extension/js/common/render-interface.ts | 3 +
extension/js/common/render-message.ts | 6 ++
extension/js/common/render-relay.ts | 6 ++
.../webmail/gmail-element-replacer.ts | 71 +++++++++++++-----
.../webmail/setup-webmail-content-script.ts | 10 ---
test/source/tests/gmail.ts | 11 +--
21 files changed, 243 insertions(+), 137 deletions(-)
diff --git a/extension/chrome/dev/export.ts b/extension/chrome/dev/export.ts
index 5f25164f562..a232bd3b1b3 100644
--- a/extension/chrome/dev/export.ts
+++ b/extension/chrome/dev/export.ts
@@ -87,7 +87,7 @@ Catch.try(async () => {
const fetchableAttachments: Attachment[] = [];
const skippedAttachments: Attachment[] = [];
for (const msg of messages) {
- for (const attachment of GmailParser.findAttachments(msg)) {
+ for (const attachment of GmailParser.findAttachments(msg, msg.id)) {
if (attachment.length > 1024 * 1024 * 7) {
// over 7 mb - attachment too big
skippedAttachments.push(
diff --git a/extension/chrome/elements/attachment.ts b/extension/chrome/elements/attachment.ts
index e2233530445..6ca35b0f500 100644
--- a/extension/chrome/elements/attachment.ts
+++ b/extension/chrome/elements/attachment.ts
@@ -8,7 +8,7 @@ import { PromiseCancellation, Str, Url } from '../../js/common/core/common.js';
import { Api } from '../../js/common/api/shared/api.js';
import { ApiErr } from '../../js/common/api/shared/api-error.js';
import { Assert } from '../../js/common/assert.js';
-import { Attachment } from '../../js/common/core/attachment.js';
+import { Attachment, AttachmentId } from '../../js/common/core/attachment.js';
import { Browser } from '../../js/common/browser/browser.js';
import { Catch } from '../../js/common/platform/catch.js';
import { Gmail } from '../../js/common/api/email-provider/gmail/gmail.js';
@@ -32,10 +32,8 @@ export class AttachmentDownloadView extends View {
protected readonly isEncrypted: boolean;
protected readonly errorDetailsOpened: boolean;
protected readonly type: string | undefined;
- protected readonly msgId: string | undefined;
- protected readonly id: string | undefined;
protected readonly name: string | undefined;
- protected readonly url: string | undefined;
+ protected readonly attachmentId: AttachmentId;
protected readonly gmail: Gmail;
protected attachment!: Attachment;
protected ppChangedPromiseCancellation: PromiseCancellation = { cancel: false };
@@ -73,11 +71,17 @@ export class AttachmentDownloadView extends View {
this.errorDetailsOpened = uncheckedUrlParams.errorDetailsOpened === true;
this.size = uncheckedUrlParams.size ? parseInt(String(uncheckedUrlParams.size)) : undefined;
this.type = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'type');
- this.msgId = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'msgId');
- this.id = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'attachmentId');
this.name = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'name');
// url contains either actual url of remote content or objectUrl for direct content, either way needs to be downloaded
- this.url = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'url');
+ const url = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'url');
+ if (url) {
+ this.attachmentId = { url };
+ } else {
+ this.attachmentId = {
+ msgId: Assert.urlParamRequire.string(uncheckedUrlParams, 'msgId'),
+ id: Assert.urlParamRequire.string(uncheckedUrlParams, 'attachmentId'),
+ };
+ }
this.gmail = new Gmail(this.acctEmail);
}
@@ -91,11 +95,9 @@ export class AttachmentDownloadView extends View {
this.fesUrl = storage.fesUrl;
try {
this.attachment = new Attachment({
+ ...this.attachmentId,
name: this.origNameBasedOnFilename,
type: this.type,
- msgId: this.msgId,
- id: this.id,
- url: this.url,
});
} catch (e) {
Catch.reportErr(e);
@@ -107,9 +109,9 @@ export class AttachmentDownloadView extends View {
this.renderHeader();
$('#name').attr('title', this.name || '');
$('img#file-format').attr('src', this.getFileIconSrc());
- if (!this.size && this.url) {
+ if (!this.size && 'url' in this.attachmentId) {
// download url of a file that has an unknown size
- this.getUrlFileSize(this.url)
+ this.getUrlFileSize(this.attachmentId.url)
.then(fileSize => {
if (typeof fileSize !== 'undefined') {
this.size = fileSize;
diff --git a/extension/chrome/elements/attachment_preview.ts b/extension/chrome/elements/attachment_preview.ts
index af687abbfff..446e61d7713 100644
--- a/extension/chrome/elements/attachment_preview.ts
+++ b/extension/chrome/elements/attachment_preview.ts
@@ -36,11 +36,9 @@ View.run(
try {
Xss.sanitizeRender(this.attachmentPreviewContainer, `${Ui.spinner('green', 'large_spinner')}`);
this.attachment = new Attachment({
+ ...this.attachmentId,
name: this.origNameBasedOnFilename,
type: this.type,
- msgId: this.msgId,
- id: this.id,
- url: this.url,
});
await this.downloadDataIfNeeded();
const result = this.isEncrypted ? await this.decrypt() : this.attachment.getData();
diff --git a/extension/chrome/elements/compose-modules/compose-quote-module.ts b/extension/chrome/elements/compose-modules/compose-quote-module.ts
index 442ca3284ca..823860ca79a 100644
--- a/extension/chrome/elements/compose-modules/compose-quote-module.ts
+++ b/extension/chrome/elements/compose-modules/compose-quote-module.ts
@@ -123,29 +123,31 @@ export class ComposeQuoteModule extends ViewModule {
decryptedAndFormatedContent.push(Xss.htmlUnescape(htmlParsed));
} else if (block.type === 'plainHtml') {
decryptedAndFormatedContent.push(Xss.htmlUnescape(Xss.htmlSanitizeAndStripAllTags(stringContent, '\n', false)));
- } else if (['encryptedAttachment', 'decryptedAttachment', 'plainAttachment'].includes(block.type)) {
- if (block.attachmentMeta?.data) {
- let attachmentMeta: { content: Buf; filename?: string } | undefined;
- if (block.type === 'encryptedAttachment') {
- this.setQuoteLoaderProgress('decrypting...');
- const result = await MsgUtil.decryptMessage({
- kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail),
- encryptedData: block.attachmentMeta.data,
- verificationPubs: [], // todo: #4158 signature verification of attachments
- });
- if (result.success) {
- attachmentMeta = { content: result.content, filename: result.filename };
- }
- } else {
- attachmentMeta = {
- content: Buf.fromUint8(block.attachmentMeta.data),
- filename: block.attachmentMeta.name,
- };
- }
- if (attachmentMeta) {
- const file = new File([attachmentMeta.content], attachmentMeta.filename || '');
- decryptedFiles.push(file);
+ } else if (
+ block.attachmentMeta &&
+ 'data' in block.attachmentMeta &&
+ ['encryptedAttachment', 'decryptedAttachment', 'plainAttachment'].includes(block.type)
+ ) {
+ let attachmentMeta: { content: Buf; filename?: string } | undefined;
+ if (block.type === 'encryptedAttachment') {
+ this.setQuoteLoaderProgress('decrypting...');
+ const result = await MsgUtil.decryptMessage({
+ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail),
+ encryptedData: block.attachmentMeta.data,
+ verificationPubs: [], // todo: #4158 signature verification of attachments
+ });
+ if (result.success) {
+ attachmentMeta = { content: result.content, filename: result.filename };
}
+ } else {
+ attachmentMeta = {
+ content: Buf.fromUint8(block.attachmentMeta.data),
+ filename: block.attachmentMeta.name,
+ };
+ }
+ if (attachmentMeta) {
+ const file = new File([attachmentMeta.content], attachmentMeta.filename || '');
+ decryptedFiles.push(file);
}
} else {
decryptedAndFormatedContent.push(stringContent);
diff --git a/extension/chrome/elements/pgp_base_block_view.ts b/extension/chrome/elements/pgp_base_block_view.ts
index b2ddbf1d80d..375d54c2820 100644
--- a/extension/chrome/elements/pgp_base_block_view.ts
+++ b/extension/chrome/elements/pgp_base_block_view.ts
@@ -3,16 +3,23 @@
'use strict';
import { View } from '../../js/common/view.js';
+import { PgpBlockViewAttachmentsModule } from './pgp_block_modules/pgp-block-attachmens-module.js';
import { PgpBlockViewQuoteModule } from './pgp_block_modules/pgp-block-quote-module.js';
import { PgpBlockViewRenderModule } from './pgp_block_modules/pgp-block-render-module.js';
export abstract class PgpBaseBlockView extends View {
public readonly quoteModule: PgpBlockViewQuoteModule;
public readonly renderModule: PgpBlockViewRenderModule;
+ public readonly attachmentsModule: PgpBlockViewAttachmentsModule;
- public constructor(public readonly parentTabId: string, public readonly frameId: string) {
+ public constructor(
+ public readonly parentTabId: string,
+ public readonly frameId: string,
+ public readonly acctEmail: string // needed for attachment decryption, probably should be refactored out
+ ) {
super();
this.quoteModule = new PgpBlockViewQuoteModule(this);
this.renderModule = new PgpBlockViewRenderModule(this);
+ this.attachmentsModule = new PgpBlockViewAttachmentsModule(this);
}
}
diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts
index 1f6d311d18c..36dfbc401b7 100644
--- a/extension/chrome/elements/pgp_block.ts
+++ b/extension/chrome/elements/pgp_block.ts
@@ -7,7 +7,6 @@ import { Assert } from '../../js/common/assert.js';
import { Buf } from '../../js/common/core/buf.js';
import { Gmail } from '../../js/common/api/email-provider/gmail/gmail.js';
import { Lang } from '../../js/common/lang.js';
-import { PgpBlockViewAttachmentsModule } from './pgp_block_modules/pgp-block-attachmens-module.js';
import { PgpBlockViewDecryptModule } from './pgp_block_modules/pgp-block-decrypt-module.js';
import { PgpBlockViewErrorModule } from './pgp_block_modules/pgp-block-error-module.js';
import { PgpBlockViewSignatureModule } from './pgp_block_modules/pgp-block-signature-module.js';
@@ -22,7 +21,6 @@ import { PgpBaseBlockView } from './pgp_base_block_view.js';
import { PgpBlockViewPrintModule } from './pgp_block_modules/pgp-block-print-module.js';
export class PgpBlockView extends PgpBaseBlockView {
- public readonly acctEmail: string;
public readonly isOutgoing: boolean;
public readonly senderEmail: string;
public readonly msgId: string | undefined;
@@ -37,7 +35,6 @@ export class PgpBlockView extends PgpBaseBlockView {
public pubLookup!: PubLookup;
public readonly debug: boolean;
- public readonly attachmentsModule: PgpBlockViewAttachmentsModule;
public readonly signatureModule: PgpBlockViewSignatureModule;
public readonly errorModule: PgpBlockViewErrorModule;
public readonly printModule: PgpBlockViewPrintModule;
@@ -48,8 +45,11 @@ export class PgpBlockView extends PgpBaseBlockView {
public constructor() {
Ui.event.protect();
const uncheckedUrlParams = Url.parse(['acctEmail', 'frameId', 'message', 'parentTabId', 'msgId', 'isOutgoing', 'senderEmail', 'signature', 'debug']);
- super(Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'), Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId'));
- this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail');
+ super(
+ Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'),
+ Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId'),
+ Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail')
+ );
this.isOutgoing = uncheckedUrlParams.isOutgoing === true;
this.debug = uncheckedUrlParams.debug === true;
const senderEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'senderEmail');
@@ -66,7 +66,6 @@ export class PgpBlockView extends PgpBaseBlockView {
}
this.gmail = new Gmail(this.acctEmail);
// modules
- this.attachmentsModule = new PgpBlockViewAttachmentsModule(this);
this.signatureModule = new PgpBlockViewSignatureModule(this);
this.errorModule = new PgpBlockViewErrorModule(this);
this.printModule = new PgpBlockViewPrintModule(this);
diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts
index bc71a93fc78..800283bf425 100644
--- a/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts
+++ b/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts
@@ -6,7 +6,7 @@ import { Api } from '../../../js/common/api/shared/api.js';
import { Attachment } from '../../../js/common/core/attachment.js';
import { Browser } from '../../../js/common/browser/browser.js';
import { BrowserMsg } from '../../../js/common/browser/browser-msg.js';
-import { PgpBlockView } from '../pgp_block';
+import { PgpBaseBlockView } from '../pgp_base_block_view.js';
import { CommonHandlers, Ui } from '../../../js/common/browser/ui.js';
import { Xss } from '../../../js/common/platform/xss.js';
import { KeyStore } from '../../../js/common/platform/store/key-store.js';
@@ -19,7 +19,7 @@ declare const filesize: { filesize: Function }; // eslint-disable-line @typescri
export class PgpBlockViewAttachmentsModule {
public includedAttachments: Attachment[] = [];
- public constructor(private view: PgpBlockView) {}
+ public constructor(private view: PgpBaseBlockView) {}
public getParentTabId = () => {
return this.view.parentTabId;
diff --git a/extension/chrome/elements/pgp_render_block.htm b/extension/chrome/elements/pgp_render_block.htm
index b4a4defe2d5..c6659042175 100644
--- a/extension/chrome/elements/pgp_render_block.htm
+++ b/extension/chrome/elements/pgp_render_block.htm
@@ -37,6 +37,7 @@
+