Skip to content

Commit fae2bb7

Browse files
rrrooommmaaaRoman Shevchenkoflowcrypt-machine-user
authored
issue #4269 Individual message per recipient (#4284)
* wip * wip * eslint fix * test rendering of recipients after successful sending * lint fix * testing PWD-encrypted message in reply thread * lint fix * lint fix * wip * fixes * removed extra test code * use Reply-To to reply to PWD recipients from pubkey-encrypted message * lint fix * support for pwd-encrypted messages with no pubkey recipients * added tests for content of uploaded pwd-protected message * returned legacy mode * lint fix * error handling when sending multiple messages * test fix / lint fix * lint fix * lint fix * display a supplementary operation error as a toast * lint fix * lint fix * don't fail the test on reported 'Test error' * simplify * Refactored attempSendMsg and bindMessage into separate functions * rename * moved getEmailWithOptionalName to storage module * using a type guard for RecipientType * added comments * optimizing refactoring * tslint fix * refactored msg formatter * fix S/MIME drafts * updated remarks * refactored pwd-related code to a separate method formatttSendablePwdMsgs * separate recipients and attachments to render into a separate renderSentMessage object * renamed createPgpMimeAttachments to formatEncryptedMimeDataAsPgpMimeMetaAttachments * Use plain sendableNonPwdMsg for pubkey recipients * lint fix * corrected legacy pwd message * fixed test setup * Added a test for one legacy pwd recipient and one pubkey recipient of a message * fix Co-authored-by: Roman Shevchenko <roman@flowcrypt.com> Co-authored-by: Tom <tom@flowcrypt.com>
1 parent 1d648c9 commit fae2bb7

27 files changed

+1059
-237
lines changed

extension/chrome/elements/attachment.htm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<div id="header">
2121
<span></span>
2222
</div>
23-
<div id="name"></div>
23+
<div id="name" data-test="attachment-name"></div>
2424

2525
<script src="/lib/purify.js"></script>
2626
<script src="/lib/jquery.min.js"></script>

extension/chrome/elements/compose-modules/compose-draft-module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ export class ComposeDraftModule extends ViewModule<ComposeView> {
119119
if (this.hasBodyChanged(this.view.inputModule.squire.getHTML()) || this.hasSubjectChanged(String(this.view.S.cached('input_subject').val())) || forceSave) {
120120
this.currentlySavingDraft = true;
121121
try {
122-
const msgData = this.view.inputModule.extractAll();
123-
const { pubkeys } = await this.view.storageModule.collectSingleFamilyKeys([], msgData.from, true);
122+
const msgData = await this.view.inputModule.extractAll();
123+
const { pubkeys } = await this.view.storageModule.collectSingleFamilyKeys([], msgData.from.email, true);
124124
// collectSingleFamilyKeys filters out bad keys, but only if there are any good keys available
125125
// if no good keys available, it leaves bad keys so we can explain the issue here
126126
if (pubkeys.some(pub => pub.pubkey.expiration && pub.pubkey.expiration < Date.now())) {
@@ -133,7 +133,7 @@ export class ComposeDraftModule extends ViewModule<ComposeView> {
133133
throw new UnreportableError('Your account keys are not usable for encryption');
134134
}
135135
msgData.pwd = undefined; // not needed for drafts
136-
const sendable = await new EncryptedMsgMailFormatter(this.view, true).sendableMsg(msgData, pubkeys);
136+
const sendable = await new EncryptedMsgMailFormatter(this.view, true).sendableNonPwdMsg(msgData, pubkeys);
137137
if (this.view.replyParams?.inReplyTo) {
138138
sendable.headers.References = this.view.replyParams.inReplyTo;
139139
sendable.headers['In-Reply-To'] = this.view.replyParams.inReplyTo;

extension/chrome/elements/compose-modules/compose-err-module.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { Browser } from '../../../js/common/browser/browser.js';
66
import { BrowserEventErrHandler, Ui } from '../../../js/common/browser/ui.js';
77
import { Catch } from '../../../js/common/platform/catch.js';
8-
import { NewMsgData, SendBtnTexts } from './compose-types.js';
8+
import { NewMsgData, SendBtnTexts, SendMsgsResult } from './compose-types.js';
99
import { ApiErr } from '../../../js/common/api/shared/api-error.js';
1010
import { BrowserExtension } from '../../../js/common/browser/browser-extension.js';
1111
import { BrowserMsg } from '../../../js/common/browser/browser-msg.js';
@@ -27,6 +27,11 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
2727

2828
private debugId = Str.sloppyRandom();
2929

30+
private static getErrSayingSomeMessagesHaveBeenSent = (sendMsgsResult: SendMsgsResult) => {
31+
return 'Messages to some recipients were sent successfully, while messages to ' +
32+
Str.formatEmailList(sendMsgsResult.failures.map(el => el.recipient)) + ' encountered ';
33+
};
34+
3035
public handle = (couldNotDoWhat: string): BrowserEventErrHandler => {
3136
return {
3237
network: async () => await Ui.modal.info(`Could not ${couldNotDoWhat} (network error). Please try again.`),
@@ -61,22 +66,34 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
6166
}
6267
};
6368

64-
public handleSendErr = async (e: any) => {
69+
public handleSendErr = async (e: any, sendMsgsResult?: SendMsgsResult) => {
6570
this.view.errModule.debug(`handleSendErr: ${String(e)}`);
6671
if (ApiErr.isNetErr(e)) {
67-
let netErrMsg = 'Could not send message due to network error. Please check your internet connection and try again.\n';
68-
netErrMsg += '(This may also be caused by <a href="https://flowcrypt.com/docs/help/network-error.html" target="_blank">missing extension permissions</a>).';
72+
let netErrMsg: string | undefined;
73+
if (sendMsgsResult?.success.length) {
74+
// there were some successful sends
75+
netErrMsg = ComposeErrModule.getErrSayingSomeMessagesHaveBeenSent(sendMsgsResult) +
76+
'network errors. Please check your internet connection and try again.';
77+
} else {
78+
netErrMsg = 'Could not send message due to network error. Please check your internet connection and try again.\n' +
79+
'(This may also be caused by <a href="https://flowcrypt.com/docs/help/network-error.html" target="_blank">missing extension permissions</a>).';
80+
}
6981
await Ui.modal.error(netErrMsg, true);
7082
} else if (ApiErr.isAuthErr(e)) {
7183
BrowserMsg.send.notificationShowAuthPopupNeeded(this.view.parentTabId, { acctEmail: this.view.acctEmail });
7284
Settings.offerToLoginWithPopupShowModalOnErr(this.view.acctEmail);
7385
} else if (ApiErr.isReqTooLarge(e)) {
7486
await Ui.modal.error(`Could not send: message or attachments too large.`);
7587
} else if (ApiErr.isBadReq(e)) {
88+
let gmailErrMsg: string | undefined;
89+
if (sendMsgsResult?.success.length) {
90+
gmailErrMsg = ComposeErrModule.getErrSayingSomeMessagesHaveBeenSent(sendMsgsResult) + 'error(s) from Gmail';
91+
}
7692
if (e.resMsg === AjaxErrMsgs.GOOGLE_INVALID_TO_HEADER || e.resMsg === AjaxErrMsgs.GOOGLE_RECIPIENT_ADDRESS_REQUIRED) {
77-
await Ui.modal.error('Error from google: Invalid recipients\n\nPlease remove recipients, add them back and re-send the message.');
93+
await Ui.modal.error((gmailErrMsg || 'Error from google') + ': Invalid recipients\n\nPlease remove recipients, add them back and re-send the message.');
7894
} else {
79-
if (await Ui.modal.confirm(`Google returned an error when sending message. Please help us improve FlowCrypt by reporting the error to us.`)) {
95+
if (await Ui.modal.confirm((gmailErrMsg || 'Google returned an error when sending message') +
96+
`. Please help us improve FlowCrypt by reporting the error to us.`)) {
8097
const page = '/chrome/settings/modules/help.htm';
8198
const pageUrlParams = { bugReport: BrowserExtension.prepareBugReport(`composer: send: bad request (errMsg: ${e.resMsg})`, {}, e) };
8299
await Browser.openSettingsPage('index.htm', this.view.acctEmail, page, pageUrlParams);
@@ -125,7 +142,7 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
125142
if (!subject && ! await Ui.modal.confirm('Send without a subject?')) {
126143
throw new ComposerResetBtnTrigger();
127144
}
128-
let footer = await this.view.footerModule.getFooterFromStorage(from);
145+
let footer = await this.view.footerModule.getFooterFromStorage(from.email);
129146
if (footer) { // format footer the way it would be in outgoing plaintext
130147
footer = Xss.htmlUnescape(Xss.htmlSanitizeAndStripAllTags(this.view.footerModule.createFooterHtml(footer), '\n')).trim();
131148
}

extension/chrome/elements/compose-modules/compose-input-module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ export class ComposeInputModule extends ViewModule<ComposeView> {
6060
return this.view.S.cached('fineuploader').find('.qq-upload-file').toArray().map((el) => $(el).text().trim());
6161
};
6262

63-
public extractAll = (): NewMsgData => {
63+
public extractAll = async (): Promise<NewMsgData> => {
6464
const recipients = this.mapRecipients(this.view.recipientsModule.getValidRecipients());
6565
const subject = this.view.isReplyBox && this.view.replyParams ? this.view.replyParams.subject : String($('#input_subject').val() || '');
6666
const plaintext = this.view.inputModule.extract('text', 'input_text');
6767
const plainhtml = this.view.inputModule.extract('html', 'input_text');
6868
const password = this.view.S.cached('input_password').val();
6969
const pwd = typeof password === 'string' && password ? password : undefined;
70-
const from = this.view.senderModule.getSender();
70+
const from = await this.view.storageModule.getEmailWithOptionalName(this.view.senderModule.getSender());
7171
return { recipients, subject, plaintext, plainhtml, pwd, from };
7272
};
7373

extension/chrome/elements/compose-modules/compose-recipients-module.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
'use strict';
44

5-
import { ChunkedCb, EmailProviderContact, RecipientType } from '../../../js/common/api/shared/api.js';
5+
import { Api, ChunkedCb, EmailProviderContact, RecipientType } from '../../../js/common/api/shared/api.js';
66
import { ContactInfoWithSortedPubkeys, KeyUtil, PubkeyInfo } from '../../../js/common/core/crypto/key.js';
77
import { PUBKEY_LOOKUP_RESULT_FAIL } from './compose-err-module.js';
88
import { ProviderContactsQuery, Recipients } from '../../../js/common/api/email-provider/email-provider-api.js';
@@ -160,9 +160,8 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
160160

161161
public addRecipients = async (recipients: Recipients, triggerCallback: boolean = true) => {
162162
const newRecipients: ValidRecipientElement[] = [];
163-
for (const [key, value] of Object.entries(recipients)) {
164-
if (['to', 'cc', 'bcc'].includes(key)) {
165-
const sendingType = key as RecipientType;
163+
for (const [sendingType, value] of Object.entries(recipients)) {
164+
if (Api.isRecipientHeaderNameType(sendingType)) {
166165
if (value?.length) {
167166
const recipientsContainer = this.view.S.cached('input_addresses_container_outer').find(`#input-container-${sendingType}`);
168167
for (const email of value) {

extension/chrome/elements/compose-modules/compose-render-module.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { BrowserMsg } from '../../../js/common/browser/browser-msg.js';
88
import { Catch } from '../../../js/common/platform/catch.js';
99
import { KeyImportUi } from '../../../js/common/ui/key-import-ui.js';
1010
import { Lang } from '../../../js/common/lang.js';
11-
import { Recipients } from '../../../js/common/api/email-provider/email-provider-api.js';
12-
import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js';
11+
import { ParsedRecipients, Recipients } from '../../../js/common/api/email-provider/email-provider-api.js';
1312
import { Str } from '../../../js/common/core/common.js';
1413
import { Ui } from '../../../js/common/browser/ui.js';
1514
import { Xss } from '../../../js/common/platform/xss.js';
@@ -114,7 +113,7 @@ export class ComposeRenderModule extends ViewModule<ComposeView> {
114113
}
115114
};
116115

117-
public renderReplySuccess = (msg: SendableMsg, msgId: string) => {
116+
public renderReplySuccess = (attachments: Attachment[], recipients: ParsedRecipients, msgId: string) => {
118117
this.view.renderModule.renderReinsertReplyBox(msgId);
119118
if (!this.view.sendBtnModule.popover.choices.encrypt) {
120119
this.view.S.cached('replied_body').removeClass('pgp_secure');
@@ -125,13 +124,13 @@ export class ComposeRenderModule extends ViewModule<ComposeView> {
125124
this.view.S.cached('replied_body').css('width', ($('table#compose').width() || 500) - 30);
126125
this.view.S.cached('compose_table').css('display', 'none');
127126
this.view.S.cached('reply_msg_successful').find('div.replied_from').text(this.view.senderModule.getSender());
128-
this.view.S.cached('reply_msg_successful').find('div.replied_to span').text(msg.headers.To.replace(/,/g, ', '));
129-
if (msg.recipients.cc !== undefined && msg.recipients.cc.length > 0) {
130-
this.view.S.cached('reply_msg_successful').find('div.replied_cc span').text(msg.recipients.cc.join(', '));
127+
this.view.S.cached('reply_msg_successful').find('div.replied_to span').text(Str.formatEmailList(recipients.to || []));
128+
if (recipients.cc !== undefined && recipients.cc.length > 0) {
129+
this.view.S.cached('reply_msg_successful').find('div.replied_cc span').text(Str.formatEmailList(recipients.cc));
131130
$('.replied_cc').show();
132131
}
133-
if (msg.recipients.bcc !== undefined && msg.recipients.bcc.length > 0) {
134-
this.view.S.cached('reply_msg_successful').find('div.replied_bcc span').text(msg.recipients.bcc.join(', '));
132+
if (recipients.bcc !== undefined && recipients.bcc.length > 0) {
133+
this.view.S.cached('reply_msg_successful').find('div.replied_bcc span').text(Str.formatEmailList(recipients.bcc));
135134
$('.replied_bcc').show();
136135
}
137136
const repliedBodyEl = this.view.S.cached('reply_msg_successful').find('div.replied_body');
@@ -141,7 +140,7 @@ export class ComposeRenderModule extends ViewModule<ComposeView> {
141140
this.renderReplySuccessMimeAttachments(this.view.inputModule.extractAttachments());
142141
} else {
143142
Xss.sanitizeRender(repliedBodyEl, Str.escapeTextAsRenderableHtml(this.view.inputModule.extract('text', 'input_text', 'SKIP-ADDONS')));
144-
this.renderReplySuccessAttachments(msg.attachments, msgId, this.view.sendBtnModule.popover.choices.encrypt);
143+
this.renderReplySuccessAttachments(attachments, msgId, this.view.sendBtnModule.popover.choices.encrypt);
145144
}
146145
const t = new Date();
147146
const time = ((t.getHours() !== 12) ? (t.getHours() % 12) : 12) + ':' + (t.getMinutes() < 10 ? '0' : '') + t.getMinutes() + ((t.getHours() >= 12) ? ' PM ' : ' AM ') + '(0 minutes ago)';

0 commit comments

Comments
 (0)