Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#5858 add FlowCrypt secure messaging options in Gmail's actions menu #5867

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions extension/css/webmail.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
26 changes: 26 additions & 0 deletions extension/img/svgs/forward-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions extension/js/common/xss-safe-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<img title="Secure Reply" src="${this.srcImg('svgs/reply-icon.svg')}" />
</div>`;
};

public actionsMenuBtn = (action: 'reply' | 'forward') => {
return `<div class="action_${action}_message_button action_menu_message_button" data-test="action-${action}-message-button" role="button" tabindex="0" aria-label="Secure Reply">
<img src="${this.srcImg(`svgs/${action}-icon.svg`)}" /><span>secure ${action}</span>
</div>`;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact [email protected] */

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';

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

public abstract getIntervalFunctions: () => IntervalFunction[];
public abstract setReplyBoxEditable: () => Promise<void>;
public abstract setReplyBoxEditable: (replyOption?: ReplyOption) => Promise<void>;
public abstract reinsertReplyBox: (replyMsgId: string) => void;
public abstract scrollToReplyBox: (replyMsgId: string) => void;
public abstract scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -276,6 +281,22 @@ export class GmailElementReplacer extends WebmailElementReplacer {
return !!$('iframe.pgp_block').filter(':visible').length;
};

private replaceActionsMenu = () => {
const gmailActionsMenuContainer = $(this.sel.msgActionsMenu);
if ($('.action_menu_message_button').length <= 0) {
const menuSecureReplyBtn = $(this.factory.actionsMenuBtn('reply')).insertAfter($(gmailActionsMenuContainer).find('#r')); // xss-safe-factory
const menuSecureForwardBtn = $(this.factory.actionsMenuBtn('forward')).insertAfter($(gmailActionsMenuContainer).find('#r3')); // xss-safe-factory
menuSecureReplyBtn.on(
'click',
Ui.event.handle((el, ev: JQuery.Event) => this.actionActivateSecureReplyHandler(el, ev))
);
menuSecureForwardBtn.on(
'click',
Ui.event.handle((el, ev: JQuery.Event) => this.actionActivateSecureReplyHandler(el, ev))
);
}
};

private replaceConvoBtns = (force = false) => {
const convoUpperIconsContainer = $('div.hj:visible');
const convoUpperIcons = $('span.pYTkkf-JX-ank-Rtc0Jf');
Expand Down Expand Up @@ -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();
}
};

Expand Down Expand Up @@ -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<Element>) => {
private insertEncryptedReplyBox = (messageContainer: JQuery<Element>, 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 */ `<div class="remove_borders reply_message_iframe_container inserted">${this.factory.embeddedReply(
replyParams,
true,
Expand Down
1 change: 1 addition & 0 deletions extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions test/source/tests/gmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,30 @@ 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);
expect(await gmailPage.isElementPresent('@action-reply-message-button'));
await gmailPage.waitAndClick('@action-reply-message-button');
await Util.sleep(3);
const replyBox = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 });
await Util.sleep(3);
await replyBox.waitForContent('@input-body', '');
await gmailPage.waitAndClick(actionsMenuSelector);
expect(await gmailPage.isElementPresent('@action-forward-message-button'));
await Util.sleep(3);
await gmailPage.waitAndClick('@action-forward-message-button');
await Util.sleep(3);
await replyBox.waitForContent('@input-body', '---------- Forwarded message ---------');
})
);

// convo-sensitive, draft-sensitive test
test.serial(
'mail.google.com - plain reply draft',
Expand Down
Loading