Skip to content

Commit b88601d

Browse files
authored
#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
1 parent aa27485 commit b88601d

File tree

7 files changed

+115
-10
lines changed

7 files changed

+115
-10
lines changed

extension/css/webmail.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,25 @@ body.cryptup_gmail .inserted div.reply_message_button {
202202
}
203203
}
204204

205+
body.cryptup_gmail div.action_menu_message_button {
206+
display: flex;
207+
align-items: center;
208+
text-transform: capitalize;
209+
cursor: pointer;
210+
padding: 4px;
211+
}
212+
213+
body.cryptup_gmail div.action_menu_message_button:hover {
214+
background-color: #eee; /* mimic Gmail hover. uses exact color pallete from Gmail */
215+
}
216+
217+
body.cryptup_gmail div.action_menu_message_button > img {
218+
height: 20px;
219+
width: 20px;
220+
padding: 0 12px;
221+
object-fit: contain;
222+
}
223+
205224
body.cryptup_gmail.firefox .inserted div.reply_message_button {
206225
padding-top: 16px;
207226
}
Lines changed: 26 additions & 0 deletions
Loading

extension/js/common/xss-safe-factory.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ export class XssSafeFactory {
266266
this.destroyableCls
267267
} reply_message_button" data-test="secure-reply-button" role="button" tabindex="0" data-tooltip="Secure Reply" aria-label="Secure Reply">
268268
<img title="Secure Reply" src="${this.srcImg('svgs/reply-icon.svg')}" />
269+
</div>`;
270+
};
271+
272+
public actionsMenuBtn = (action: 'reply' | 'forward') => {
273+
return `<div class="action_${action}_message_button action_menu_message_button" data-test="action-${action}-message-button">
274+
<img src="${this.srcImg(`svgs/${action}-icon.svg`)}" /><span>secure ${action}</span>
269275
</div>`;
270276
};
271277

extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact [email protected] */
22

3+
import { ReplyOption } from '../../../../chrome/elements/compose-modules/compose-reply-btn-popover-module';
34
import { ContentScriptWindow } from '../../../common/browser/browser-window';
45
import { notifyMurdered } from './setup-webmail-content-script';
56

@@ -9,7 +10,7 @@ export abstract class WebmailElementReplacer {
910
private replacePgpElsInterval: number;
1011

1112
public abstract getIntervalFunctions: () => IntervalFunction[];
12-
public abstract setReplyBoxEditable: () => Promise<void>;
13+
public abstract setReplyBoxEditable: (replyOption?: ReplyOption) => Promise<void>;
1314
public abstract reinsertReplyBox: (replyMsgId: string) => void;
1415
public abstract scrollToReplyBox: (replyMsgId: string) => void;
1516
public abstract scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void;

extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export class GmailElementReplacer extends WebmailElementReplacer {
5151
msgInner: 'div.a3s:visible:not(.undefined), .message_inner_body:visible',
5252
msgInnerText: 'table.cf.An',
5353
msgInnerContainingPgp: "div.a3s:not(.undefined):contains('" + PgpArmor.headers('null').begin + "')",
54+
msgActionsBtn: '.J-J5-Ji.aap',
55+
msgActionsMenu: '.b7.J-M',
5456
attachmentsContainerOuter: 'div.hq.gt',
5557
attachmentsContainerInner: 'div.aQH',
5658
translatePrompt: '.adI, .wl4W9b',
@@ -85,10 +87,12 @@ export class GmailElementReplacer extends WebmailElementReplacer {
8587
];
8688
};
8789

88-
public setReplyBoxEditable = async () => {
90+
public setReplyBoxEditable = async (replyOption?: ReplyOption) => {
8991
const replyContainerIframe = $('.reply_message_iframe_container > iframe').last();
9092
if (replyContainerIframe.length) {
91-
$(replyContainerIframe).replaceWith(this.factory.embeddedReply(this.getLastMsgReplyParams(this.getConvoRootEl(replyContainerIframe[0])), true)); // xss-safe-value
93+
$(replyContainerIframe).replaceWith(
94+
this.factory.embeddedReply(this.getLastMsgReplyParams(this.getConvoRootEl(replyContainerIframe[0]), replyOption), true)
95+
); // xss-safe-value
9296
} else {
9397
await this.replaceStandardReplyBox(undefined, true);
9498
}
@@ -147,6 +151,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
147151
this.replaceArmoredBlocks().catch(Catch.reportErr);
148152
this.replaceAttachments().catch(Catch.reportErr);
149153
this.replaceComposeDraftLinks();
154+
this.replaceActionsMenu();
150155
this.replaceConvoBtns();
151156
this.replaceStandardReplyBox().catch(Catch.reportErr);
152157
this.evaluateStandardComposeRecipients().catch(Catch.reportErr);
@@ -276,6 +281,22 @@ export class GmailElementReplacer extends WebmailElementReplacer {
276281
return !!$('iframe.pgp_block').filter(':visible').length;
277282
};
278283

284+
private addMenuButton = (action: 'reply' | 'forward', selector: string) => {
285+
const gmailActionsMenuContainer = $(this.sel.msgActionsMenu).find(selector);
286+
const button = $(this.factory.actionsMenuBtn(action)).insertAfter(gmailActionsMenuContainer); // xss-safe-factory
287+
button.on(
288+
'click',
289+
Ui.event.handle((el, ev: JQuery.Event) => this.actionActivateSecureReplyHandler(el, ev))
290+
);
291+
};
292+
293+
private replaceActionsMenu = () => {
294+
if ($('.action_menu_message_button').length <= 0) {
295+
this.addMenuButton('reply', '#r');
296+
this.addMenuButton('forward', '#r3');
297+
}
298+
};
299+
279300
private replaceConvoBtns = (force = false) => {
280301
const convoUpperIconsContainer = $('div.hj:visible');
281302
const convoUpperIcons = $('span.pYTkkf-JX-ank-Rtc0Jf');
@@ -346,20 +367,26 @@ export class GmailElementReplacer extends WebmailElementReplacer {
346367

347368
private actionActivateSecureReplyHandler = async (btn: HTMLElement, event: JQuery.Event) => {
348369
event.stopImmediatePropagation();
370+
const secureReplyInvokedFromMenu = btn.className.includes('action_menu_message_button');
371+
const replyOption: ReplyOption = btn.className.includes('reply') ? 'a_reply' : 'a_forward';
349372
if ($('#switch_to_encrypted_reply').length) {
350373
$('#switch_to_encrypted_reply').trigger('click');
351374
return;
352375
}
353376
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
354-
const messageContainer = $(btn.closest('.h7')!);
377+
const messageContainer = secureReplyInvokedFromMenu ? $('.T-I-JO.T-I-Kq').closest('.h7') : $(btn.closest('.h7')!);
355378
if (messageContainer.is(':last-child')) {
356379
if (this.isEncrypted()) {
357-
await this.setReplyBoxEditable();
380+
await this.setReplyBoxEditable(replyOption);
358381
} else {
359382
await this.replaceStandardReplyBox(undefined, true);
360383
}
361384
} else {
362-
this.insertEncryptedReplyBox(messageContainer);
385+
this.insertEncryptedReplyBox(messageContainer, replyOption);
386+
}
387+
if (secureReplyInvokedFromMenu) {
388+
$(this.sel.msgActionsBtn).removeClass('T-I-JO T-I-Kq');
389+
$(this.sel.msgActionsMenu).hide();
363390
}
364391
};
365392

@@ -589,18 +616,18 @@ export class GmailElementReplacer extends WebmailElementReplacer {
589616
return from ? Str.parseEmail(from) : undefined;
590617
};
591618

592-
private getLastMsgReplyParams = (convoRootEl: JQuery): FactoryReplyParams => {
593-
return { replyMsgId: this.determineMsgId($(convoRootEl).find(this.sel.msgInner).last()) };
619+
private getLastMsgReplyParams = (convoRootEl: JQuery, replyOption?: ReplyOption): FactoryReplyParams => {
620+
return { replyMsgId: this.determineMsgId($(convoRootEl).find(this.sel.msgInner).last()), replyOption };
594621
};
595622

596623
private getConvoRootEl = (anyInnerElement: HTMLElement) => {
597624
return $(anyInnerElement).closest('div.if, div.aHU, td.Bu').first();
598625
};
599626

600-
private insertEncryptedReplyBox = (messageContainer: JQuery<Element>) => {
627+
private insertEncryptedReplyBox = (messageContainer: JQuery<Element>, replyOption: ReplyOption) => {
601628
const msgIdElement = messageContainer.find('[data-legacy-message-id], [data-message-id]');
602629
const msgId = msgIdElement.attr('data-legacy-message-id') || msgIdElement.attr('data-message-id');
603-
const replyParams: FactoryReplyParams = { replyMsgId: msgId, removeAfterClose: true };
630+
const replyParams: FactoryReplyParams = { replyMsgId: msgId, removeAfterClose: true, replyOption };
604631
const secureReplyBoxXssSafe = /* xss-safe-factory */ `<div class="remove_borders reply_message_iframe_container inserted">${this.factory.embeddedReply(
605632
replyParams,
606633
true,

extension/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"resources": [
7979
"/css/webmail.css",
8080
"/img/svgs/reply-icon.svg",
81+
"/img/svgs/forward-icon.svg",
8182
"/img/svgs/spinner-white-small.svg",
8283
"/img/svgs/spinner-green-small.svg",
8384
"/img/svgs/unlock.svg",

test/source/tests/gmail.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,31 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
457457
})
458458
);
459459

460+
test(
461+
'mail.google.com - secure reply and forward in dot menu',
462+
testWithBrowser(async (t, browser) => {
463+
await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail');
464+
const gmailPage = await openGmailPage(t, browser);
465+
await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo
466+
await Util.sleep(5);
467+
const actionsMenuSelector = '.J-J5-Ji.aap';
468+
await gmailPage.waitAndClick(actionsMenuSelector);
469+
await Util.sleep(3);
470+
expect(await gmailPage.isElementPresent('@action-reply-message-button'));
471+
await gmailPage.waitAndClick('@action-reply-message-button');
472+
const replyBox = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 });
473+
await Util.sleep(3);
474+
await replyBox.waitForContent('@input-body', '');
475+
await gmailPage.waitAndClick(actionsMenuSelector);
476+
await Util.sleep(3);
477+
expect(await gmailPage.isElementPresent('@action-forward-message-button'));
478+
await gmailPage.waitAndClick('@action-forward-message-button');
479+
const replyBox2 = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 });
480+
await Util.sleep(3);
481+
await replyBox2.waitForContent('@input-body', '---------- Forwarded message ---------');
482+
})
483+
);
484+
460485
// convo-sensitive, draft-sensitive test
461486
test.serial(
462487
'mail.google.com - plain reply draft',

0 commit comments

Comments
 (0)