From b88601d9cfc64f399317937f1e3cf27d39290d02 Mon Sep 17 00:00:00 2001 From: Mart <46025304+martgil@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:43:05 +0800 Subject: [PATCH] #5858 add FlowCrypt secure messaging options in Gmail's actions menu (#5870) * add "secure reply" option in Gmail actions menu * add base ui for secure forward * fix forward icon * add secure forward functionality * add test * revert unneeded changes * update test * update test * remove unneeded attributes * pr reviews: make code concise --- extension/css/webmail.css | 19 ++++++++ extension/img/svgs/forward-icon.svg | 26 +++++++++++ extension/js/common/xss-safe-factory.ts | 6 +++ .../generic/webmail-element-replacer.ts | 3 +- .../webmail/gmail/gmail-element-replacer.ts | 45 +++++++++++++++---- extension/manifest.json | 1 + test/source/tests/gmail.ts | 25 +++++++++++ 7 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 extension/img/svgs/forward-icon.svg diff --git a/extension/css/webmail.css b/extension/css/webmail.css index 09a2869c620..cc45985f285 100644 --- a/extension/css/webmail.css +++ b/extension/css/webmail.css @@ -202,6 +202,25 @@ body.cryptup_gmail .inserted div.reply_message_button { } } +body.cryptup_gmail div.action_menu_message_button { + display: flex; + align-items: center; + text-transform: capitalize; + cursor: pointer; + padding: 4px; +} + +body.cryptup_gmail div.action_menu_message_button:hover { + background-color: #eee; /* mimic Gmail hover. uses exact color pallete from Gmail */ +} + +body.cryptup_gmail div.action_menu_message_button > img { + height: 20px; + width: 20px; + padding: 0 12px; + object-fit: contain; +} + body.cryptup_gmail.firefox .inserted div.reply_message_button { padding-top: 16px; } diff --git a/extension/img/svgs/forward-icon.svg b/extension/img/svgs/forward-icon.svg new file mode 100644 index 00000000000..e0702a6462f --- /dev/null +++ b/extension/img/svgs/forward-icon.svg @@ -0,0 +1,26 @@ + + reply-icon-svg + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extension/js/common/xss-safe-factory.ts b/extension/js/common/xss-safe-factory.ts index 676158c75e0..686435c749e 100644 --- a/extension/js/common/xss-safe-factory.ts +++ b/extension/js/common/xss-safe-factory.ts @@ -266,6 +266,12 @@ export class XssSafeFactory { this.destroyableCls } reply_message_button" data-test="secure-reply-button" role="button" tabindex="0" data-tooltip="Secure Reply" aria-label="Secure Reply"> + `; + }; + + public actionsMenuBtn = (action: 'reply' | 'forward') => { + return `
+ secure ${action}
`; }; diff --git a/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts b/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts index ed5fbf06606..db51b273ac9 100644 --- a/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts +++ b/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts @@ -1,5 +1,6 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ +import { ReplyOption } from '../../../../chrome/elements/compose-modules/compose-reply-btn-popover-module'; import { ContentScriptWindow } from '../../../common/browser/browser-window'; import { notifyMurdered } from './setup-webmail-content-script'; @@ -9,7 +10,7 @@ export abstract class WebmailElementReplacer { private replacePgpElsInterval: number; public abstract getIntervalFunctions: () => IntervalFunction[]; - public abstract setReplyBoxEditable: () => Promise; + public abstract setReplyBoxEditable: (replyOption?: ReplyOption) => Promise; public abstract reinsertReplyBox: (replyMsgId: string) => void; public abstract scrollToReplyBox: (replyMsgId: string) => void; public abstract scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void; diff --git a/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts index a691c7be657..f2775edc7f4 100644 --- a/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts +++ b/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts @@ -51,6 +51,8 @@ export class GmailElementReplacer extends WebmailElementReplacer { msgInner: 'div.a3s:visible:not(.undefined), .message_inner_body:visible', msgInnerText: 'table.cf.An', msgInnerContainingPgp: "div.a3s:not(.undefined):contains('" + PgpArmor.headers('null').begin + "')", + msgActionsBtn: '.J-J5-Ji.aap', + msgActionsMenu: '.b7.J-M', attachmentsContainerOuter: 'div.hq.gt', attachmentsContainerInner: 'div.aQH', translatePrompt: '.adI, .wl4W9b', @@ -85,10 +87,12 @@ export class GmailElementReplacer extends WebmailElementReplacer { ]; }; - public setReplyBoxEditable = async () => { + public setReplyBoxEditable = async (replyOption?: ReplyOption) => { const replyContainerIframe = $('.reply_message_iframe_container > iframe').last(); if (replyContainerIframe.length) { - $(replyContainerIframe).replaceWith(this.factory.embeddedReply(this.getLastMsgReplyParams(this.getConvoRootEl(replyContainerIframe[0])), true)); // xss-safe-value + $(replyContainerIframe).replaceWith( + this.factory.embeddedReply(this.getLastMsgReplyParams(this.getConvoRootEl(replyContainerIframe[0]), replyOption), true) + ); // xss-safe-value } else { await this.replaceStandardReplyBox(undefined, true); } @@ -147,6 +151,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { this.replaceArmoredBlocks().catch(Catch.reportErr); this.replaceAttachments().catch(Catch.reportErr); this.replaceComposeDraftLinks(); + this.replaceActionsMenu(); this.replaceConvoBtns(); this.replaceStandardReplyBox().catch(Catch.reportErr); this.evaluateStandardComposeRecipients().catch(Catch.reportErr); @@ -276,6 +281,22 @@ export class GmailElementReplacer extends WebmailElementReplacer { return !!$('iframe.pgp_block').filter(':visible').length; }; + private addMenuButton = (action: 'reply' | 'forward', selector: string) => { + const gmailActionsMenuContainer = $(this.sel.msgActionsMenu).find(selector); + const button = $(this.factory.actionsMenuBtn(action)).insertAfter(gmailActionsMenuContainer); // xss-safe-factory + button.on( + 'click', + Ui.event.handle((el, ev: JQuery.Event) => this.actionActivateSecureReplyHandler(el, ev)) + ); + }; + + private replaceActionsMenu = () => { + if ($('.action_menu_message_button').length <= 0) { + this.addMenuButton('reply', '#r'); + this.addMenuButton('forward', '#r3'); + } + }; + private replaceConvoBtns = (force = false) => { const convoUpperIconsContainer = $('div.hj:visible'); const convoUpperIcons = $('span.pYTkkf-JX-ank-Rtc0Jf'); @@ -346,20 +367,26 @@ export class GmailElementReplacer extends WebmailElementReplacer { private actionActivateSecureReplyHandler = async (btn: HTMLElement, event: JQuery.Event) => { event.stopImmediatePropagation(); + const secureReplyInvokedFromMenu = btn.className.includes('action_menu_message_button'); + const replyOption: ReplyOption = btn.className.includes('reply') ? 'a_reply' : 'a_forward'; if ($('#switch_to_encrypted_reply').length) { $('#switch_to_encrypted_reply').trigger('click'); return; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const messageContainer = $(btn.closest('.h7')!); + const messageContainer = secureReplyInvokedFromMenu ? $('.T-I-JO.T-I-Kq').closest('.h7') : $(btn.closest('.h7')!); if (messageContainer.is(':last-child')) { if (this.isEncrypted()) { - await this.setReplyBoxEditable(); + await this.setReplyBoxEditable(replyOption); } else { await this.replaceStandardReplyBox(undefined, true); } } else { - this.insertEncryptedReplyBox(messageContainer); + this.insertEncryptedReplyBox(messageContainer, replyOption); + } + if (secureReplyInvokedFromMenu) { + $(this.sel.msgActionsBtn).removeClass('T-I-JO T-I-Kq'); + $(this.sel.msgActionsMenu).hide(); } }; @@ -589,18 +616,18 @@ export class GmailElementReplacer extends WebmailElementReplacer { return from ? Str.parseEmail(from) : undefined; }; - private getLastMsgReplyParams = (convoRootEl: JQuery): FactoryReplyParams => { - return { replyMsgId: this.determineMsgId($(convoRootEl).find(this.sel.msgInner).last()) }; + private getLastMsgReplyParams = (convoRootEl: JQuery, replyOption?: ReplyOption): FactoryReplyParams => { + return { replyMsgId: this.determineMsgId($(convoRootEl).find(this.sel.msgInner).last()), replyOption }; }; private getConvoRootEl = (anyInnerElement: HTMLElement) => { return $(anyInnerElement).closest('div.if, div.aHU, td.Bu').first(); }; - private insertEncryptedReplyBox = (messageContainer: JQuery) => { + private insertEncryptedReplyBox = (messageContainer: JQuery, replyOption: ReplyOption) => { const msgIdElement = messageContainer.find('[data-legacy-message-id], [data-message-id]'); const msgId = msgIdElement.attr('data-legacy-message-id') || msgIdElement.attr('data-message-id'); - const replyParams: FactoryReplyParams = { replyMsgId: msgId, removeAfterClose: true }; + const replyParams: FactoryReplyParams = { replyMsgId: msgId, removeAfterClose: true, replyOption }; const secureReplyBoxXssSafe = /* xss-safe-factory */ `
${this.factory.embeddedReply( replyParams, true, diff --git a/extension/manifest.json b/extension/manifest.json index 34c0e03f66c..8c57be8e8c8 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -78,6 +78,7 @@ "resources": [ "/css/webmail.css", "/img/svgs/reply-icon.svg", + "/img/svgs/forward-icon.svg", "/img/svgs/spinner-white-small.svg", "/img/svgs/spinner-green-small.svg", "/img/svgs/unlock.svg", diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts index fa6e659ceee..b360f2d0df8 100644 --- a/test/source/tests/gmail.ts +++ b/test/source/tests/gmail.ts @@ -457,6 +457,31 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test }) ); + test( + 'mail.google.com - secure reply and forward in dot menu', + testWithBrowser(async (t, browser) => { + await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail'); + const gmailPage = await openGmailPage(t, browser); + await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo + await Util.sleep(5); + const actionsMenuSelector = '.J-J5-Ji.aap'; + await gmailPage.waitAndClick(actionsMenuSelector); + await Util.sleep(3); + expect(await gmailPage.isElementPresent('@action-reply-message-button')); + await gmailPage.waitAndClick('@action-reply-message-button'); + const replyBox = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 }); + await Util.sleep(3); + await replyBox.waitForContent('@input-body', ''); + await gmailPage.waitAndClick(actionsMenuSelector); + await Util.sleep(3); + expect(await gmailPage.isElementPresent('@action-forward-message-button')); + await gmailPage.waitAndClick('@action-forward-message-button'); + const replyBox2 = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 }); + await Util.sleep(3); + await replyBox2.waitForContent('@input-body', '---------- Forwarded message ---------'); + }) + ); + // convo-sensitive, draft-sensitive test test.serial( 'mail.google.com - plain reply draft',