From 447f1dc1ee3ce8fb10acc3e9323fe4325d51a018 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 7 Feb 2022 15:47:23 +0000 Subject: [PATCH 01/11] use contact name in RecipientElement for sending --- .../compose-modules/compose-err-module.ts | 1 - .../compose-modules/compose-input-module.ts | 17 +- .../compose-my-pubkey-module.ts | 3 +- .../compose-recipients-module.ts | 176 +++++++++--------- .../compose-send-btn-module.ts | 51 +++-- .../compose-modules/compose-sender-module.ts | 2 +- .../compose-modules/compose-storage-module.ts | 15 +- .../elements/compose-modules/compose-types.ts | 17 +- .../encrypted-mail-msg-formatter.ts | 2 +- .../formatters/general-mail-formatter.ts | 3 +- .../formatters/signed-msg-mail-formatter.ts | 5 +- .../settings/modules/backup-manual-module.ts | 2 +- extension/css/cryptup.css | 4 - extension/js/common/api/account-server.ts | 4 +- .../api/account-servers/enterprise-server.ts | 12 +- .../api/email-provider/email-provider-api.ts | 2 + .../common/api/email-provider/gmail/gmail.ts | 2 +- .../common/api/email-provider/sendable-msg.ts | 17 +- extension/js/common/api/shared/api.ts | 7 +- extension/js/common/core/common.ts | 5 + extension/js/common/core/crypto/key.ts | 12 +- .../js/common/platform/store/contact-store.ts | 32 ++-- test/source/mock/fes/fes-endpoints.ts | 4 +- .../strategies/send-message-strategy.ts | 9 +- test/source/platform/store/contact-store.ts | 4 +- test/source/tests/compose.ts | 8 + 26 files changed, 212 insertions(+), 204 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-err-module.ts b/extension/chrome/elements/compose-modules/compose-err-module.ts index 2475e8c9cc3..64e75854586 100644 --- a/extension/chrome/elements/compose-modules/compose-err-module.ts +++ b/extension/chrome/elements/compose-modules/compose-err-module.ts @@ -22,7 +22,6 @@ class ComposerNotReadyError extends ComposerUserError { } export class ComposerResetBtnTrigger extends Error { } export const PUBKEY_LOOKUP_RESULT_FAIL: 'fail' = 'fail'; -export const PUBKEY_LOOKUP_RESULT_WRONG: 'wrong' = 'wrong'; export class ComposeErrModule extends ViewModule { diff --git a/extension/chrome/elements/compose-modules/compose-input-module.ts b/extension/chrome/elements/compose-modules/compose-input-module.ts index 149b01eebab..4e297fe7192 100644 --- a/extension/chrome/elements/compose-modules/compose-input-module.ts +++ b/extension/chrome/elements/compose-modules/compose-input-module.ts @@ -2,11 +2,11 @@ 'use strict'; -import { NewMsgData, RecipientElement } from './compose-types.js'; +import { NewMsgData, ValidRecipientElement } from './compose-types.js'; import { CursorEvent, SquireEditor, WillPasteEvent } from '../../../types/squire.js'; import { Catch } from '../../../js/common/platform/catch.js'; -import { Recipients } from '../../../js/common/api/email-provider/email-provider-api.js'; +import { ParsedRecipients } from '../../../js/common/api/email-provider/email-provider-api.js'; import { Str } from '../../../js/common/core/common.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { ViewModule } from '../../../js/common/view-module.js'; @@ -61,8 +61,7 @@ export class ComposeInputModule extends ViewModule { }; public extractAll = (): NewMsgData => { - const recipientElements = this.view.recipientsModule.getRecipients(); - const recipients = this.mapRecipients(recipientElements); + const recipients = this.mapRecipients(this.view.recipientsModule.getValidRecipients()); const subject = this.view.isReplyBox && this.view.replyParams ? this.view.replyParams.subject : String($('#input_subject').val() || ''); const plaintext = this.view.inputModule.extract('text', 'input_text'); const plainhtml = this.view.inputModule.extract('html', 'input_text'); @@ -212,18 +211,18 @@ export class ComposeInputModule extends ViewModule { this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); }; - private mapRecipients = (recipients: RecipientElement[]) => { - const result: Recipients = { to: [], cc: [], bcc: [] }; + private mapRecipients = (recipients: ValidRecipientElement[]): ParsedRecipients => { + const result: ParsedRecipients = { to: [], cc: [], bcc: [] }; for (const recipient of recipients) { switch (recipient.sendingType) { case "to": - result.to!.push(recipient.email); + result.to!.push({ email: recipient.email, name: recipient.name }); break; case "cc": - result.cc!.push(recipient.email); + result.cc!.push({ email: recipient.email, name: recipient.name }); break; case "bcc": - result.bcc!.push(recipient.email); + result.bcc!.push({ email: recipient.email, name: recipient.name }); break; } } diff --git a/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts b/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts index fa42e14b7d3..fe9f78cc00b 100644 --- a/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts +++ b/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts @@ -47,8 +47,7 @@ export class ComposeMyPubkeyModule extends ViewModule { return; } const myDomain = Str.getDomainFromEmailAddress(senderEmail); - const foreignRecipients = this.view.recipientsModule.getRecipients().map(r => r.email) - .filter(Boolean) + const foreignRecipients = this.view.recipientsModule.getValidRecipients().map(r => r.email) .filter(email => myDomain !== Str.getDomainFromEmailAddress(email)); if (foreignRecipients.length > 0) { if (!Array.isArray(cached)) { diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index aa4bef10065..165b019d8e9 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -3,11 +3,11 @@ 'use strict'; import { ChunkedCb, EmailProviderContact, RecipientType } from '../../../js/common/api/shared/api.js'; -import { KeyUtil, PubkeyInfo } from '../../../js/common/core/crypto/key.js'; -import { PUBKEY_LOOKUP_RESULT_FAIL, PUBKEY_LOOKUP_RESULT_WRONG } from './compose-err-module.js'; +import { ContactInfoWithSortedPubkeys, KeyUtil, PubkeyInfo } from '../../../js/common/core/crypto/key.js'; +import { PUBKEY_LOOKUP_RESULT_FAIL } from './compose-err-module.js'; import { ProviderContactsQuery, Recipients } from '../../../js/common/api/email-provider/email-provider-api.js'; -import { RecipientElement, RecipientStatus } from './compose-types.js'; -import { Str } from '../../../js/common/core/common.js'; +import { RecipientElement, RecipientStatus, ValidRecipientElement } from './compose-types.js'; +import { EmailParts, Str } from '../../../js/common/core/common.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Bm, BrowserMsg } from '../../../js/common/browser/browser-msg.js'; import { Catch } from '../../../js/common/platform/catch.js'; @@ -94,13 +94,24 @@ export class ComposeRecipientsModule extends ViewModule { return this.addedRecipients; }; - public validateEmails = (uncheckedEmails: string[]): { valid: string[], invalid: string[] } => { - const valid: string[] = []; + public getValidRecipients = (): ValidRecipientElement[] => { + const validRecipients: ValidRecipientElement[] = []; + for (const recipient of this.addedRecipients) { + if (recipient.email) { + const email = recipient.email; + validRecipients.push({ ...recipient, email }); + } + } + return validRecipients; + }; + + public validateEmails = (uncheckedEmails: string[]): { valid: EmailParts[], invalid: string[] } => { + const valid: EmailParts[] = []; const invalid: string[] = []; for (const email of uncheckedEmails) { - const parsed = Str.parseEmail(email).email; - if (parsed) { - valid.push(parsed); + const parsed = Str.parseEmail(email); + if (parsed.email) { + valid.push({ email: parsed.email, name: parsed.name }); } else { invalid.push(email); } @@ -117,17 +128,17 @@ export class ComposeRecipientsModule extends ViewModule { uncheckedEmails = uncheckedEmails || String(input.val()).split(/,/g); this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - emails to check(${uncheckedEmails.join(',')})`); const validationResult = this.validateEmails(uncheckedEmails); - let recipientsToEvaluate: RecipientElement[] = []; + let recipientsToEvaluate: ValidRecipientElement[] = []; const container = input.parent(); if (validationResult.valid.length) { this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - valid emails(${validationResult.valid.join(',')})`); - recipientsToEvaluate = this.createRecipientsElements(container, validationResult.valid, sendingType, RecipientStatus.EVALUATING); + recipientsToEvaluate = this.createRecipientsElements(container, validationResult.valid, sendingType, RecipientStatus.EVALUATING) as ValidRecipientElement[]; } const invalidEmails = validationResult.invalid.filter(em => !!em); // remove empty strings this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - invalid emails(${validationResult.invalid.join(',')})`); if (force && invalidEmails.length) { this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - force add invalid recipients`); - recipientsToEvaluate = [...recipientsToEvaluate, ...this.createRecipientsElements(container, invalidEmails, sendingType, RecipientStatus.WRONG)]; + this.createRecipientsElements(container, invalidEmails.map(invalid => { return { invalid }; }), sendingType, RecipientStatus.WRONG); input.val(''); } else { this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - setting inputTo with invalid emails`); @@ -140,18 +151,29 @@ export class ComposeRecipientsModule extends ViewModule { this.view.errModule.debug(`parseRenderRecipients(force: ${force}).3`); this.view.sizeModule.resizeInput(input); this.view.errModule.debug(`parseRenderRecipients(force: ${force}).4`); + } else { + this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); } } }; public addRecipients = async (recipients: Recipients, triggerCallback: boolean = true) => { - let newRecipients: RecipientElement[] = []; + const newRecipients: ValidRecipientElement[] = []; for (const [key, value] of Object.entries(recipients)) { if (['to', 'cc', 'bcc'].includes(key)) { const sendingType = key as RecipientType; if (value?.length) { const recipientsContainer = this.view.S.cached('input_addresses_container_outer').find(`#input-container-${sendingType}`); - newRecipients = newRecipients.concat(this.createRecipientsElements(recipientsContainer, value, sendingType, RecipientStatus.EVALUATING)); + for (const email of value) { + const parsed = Str.parseEmail(email); + if (parsed.email) { + newRecipients.push(...this.createRecipientsElements(recipientsContainer, + [{ email: parsed.email, name: parsed.name }], + sendingType, RecipientStatus.EVALUATING) as ValidRecipientElement[]); + } else { + this.createRecipientsElements(recipientsContainer, [{ invalid: email }], sendingType, RecipientStatus.WRONG); + } + } this.view.S.cached('input_addresses_container_outer').find(`#input-container-${sendingType}`).css('display', ''); this.view.sizeModule.resizeInput(this.view.S.cached('input_addresses_container_outer').find(`#input-container-${sendingType} input`)); } @@ -197,7 +219,7 @@ export class ComposeRecipientsModule extends ViewModule { await this.view.recipientsModule.setEmailsPreview(this.getRecipients()); }; - public reEvaluateRecipients = async (recipients: RecipientElement[]) => { + public reEvaluateRecipients = async (recipients: ValidRecipientElement[]) => { for (const recipient of recipients) { $(recipient.element).empty().removeClass(); Xss.sanitizeAppend(recipient.element, `${Xss.escape(recipient.email)} ${Ui.spinner('green')}`); @@ -205,7 +227,7 @@ export class ComposeRecipientsModule extends ViewModule { await this.evaluateRecipients(recipients); }; - public evaluateRecipients = async (recipientEls: RecipientElement[], triggerCallback: boolean = true) => { + public evaluateRecipients = async (recipientEls: ValidRecipientElement[], triggerCallback: boolean = true) => { this.view.errModule.debug(`evaluateRecipients`); $('body').attr('data-test-state', 'working'); for (const recipientEl of recipientEls) { @@ -214,14 +236,9 @@ export class ComposeRecipientsModule extends ViewModule { recipientEl.evaluating = (async () => { this.view.errModule.debug(`evaluateRecipients.evaluat.recipient.email(${String(recipientEl.email)})`); this.view.errModule.debug(`evaluateRecipients.evaluating.recipient.status(${recipientEl.status})`); - if (recipientEl.status === RecipientStatus.WRONG) { - this.view.errModule.debug(`evaluateRecipients.evaluating: exiting because WRONG`); - await this.renderPubkeyResult(recipientEl, 'wrong'); - } else { - this.view.errModule.debug(`evaluateRecipients.evaluating: calling getUpToDatePubkeys`); - const pubkeys = await this.view.storageModule.getUpToDatePubkeys(recipientEl.email); - await this.renderPubkeyResult(recipientEl, pubkeys); - } + this.view.errModule.debug(`evaluateRecipients.evaluating: calling getUpToDatePubkeys`); + const info = await this.view.storageModule.getUpToDatePubkeys(recipientEl.email); + this.renderPubkeyResult(recipientEl, info); // Clear promise when after finished // todo - it would be better if we could avoid doing this, eg // recipient.evaluating would be a bool @@ -268,7 +285,7 @@ export class ComposeRecipientsModule extends ViewModule { while (container.width()! <= maxWidth && orderedRecipients.length >= processed + 1) { const recipient = orderedRecipients[processed]; const escapedTitle = Xss.escape(recipient.element.getAttribute('title') || ''); - const emailHtml = ``; + const emailHtml = ``; $(emailHtml).insertBefore(rest); // xss-escaped processed++; } @@ -359,16 +376,17 @@ export class ComposeRecipientsModule extends ViewModule { }; public reRenderRecipientFor = async (email: string): Promise => { - if (!this.addedRecipients.some(r => r.email === email)) { + const validRecipients = this.getValidRecipients().filter(r => r.email === email); + if (!validRecipients.length) { return; } const emailAndPubkeys = await ContactStore.getOneWithAllPubkeys(undefined, email); - for (const recipient of this.addedRecipients.filter(r => r.email === email)) { + for (const recipient of validRecipients) { this.view.errModule.debug(`re-rendering recipient: ${email}`); - await this.renderPubkeyResult(recipient, emailAndPubkeys ? emailAndPubkeys.sortedPubkeys : []); - this.view.recipientsModule.showHideCcAndBccInputsIfNeeded(); - await this.view.recipientsModule.setEmailsPreview(this.getRecipients()); + this.renderPubkeyResult(recipient, emailAndPubkeys); } + this.view.recipientsModule.showHideCcAndBccInputsIfNeeded(); + await this.view.recipientsModule.setEmailsPreview(this.getRecipients()); }; private inputsBlurHandler = async (target: HTMLElement, e: JQuery.Event) => { @@ -435,11 +453,11 @@ export class ComposeRecipientsModule extends ViewModule { }; private addTheirPubkeyClickHandler = () => { - const noPgpRecipients = this.addedRecipients.filter(r => r.element.className.includes('no_pgp')); + const noPgpRecipients = this.getValidRecipients().filter(r => r.element.className.includes('no_pgp')); this.view.renderModule.renderAddPubkeyDialog(noPgpRecipients.map(r => r.email)); clearInterval(this.addedPubkeyDbLookupInterval); // todo - get rid of Catch.set_interval. just supply tabId and wait for direct callback this.addedPubkeyDbLookupInterval = Catch.setHandledInterval(async () => { - const recipientsHasPgp: RecipientElement[] = []; + const recipientsHasPgp: ValidRecipientElement[] = []; for (const recipient of noPgpRecipients) { const [contact] = await ContactStore.get(undefined, [recipient.email]); if (contact && contact.hasPgp) { @@ -677,8 +695,6 @@ export class ComposeRecipientsModule extends ViewModule { contactItems.removeClass('active'); $(this).addClass('active'); }); - this.view.S.cached('contacts').find('ul li.auth_contacts').click(this.view.setHandler(() => - this.authContacts(this.view.acctEmail), this.view.errModule.handle(`authorize contact search`))); const offset = input.offset()!; const inputToPadding = parseInt(input.css('padding-left')); let leftOffset: number; @@ -734,12 +750,14 @@ export class ComposeRecipientsModule extends ViewModule { this.hideContacts(); }; - private createRecipientsElements = (container: JQuery, emails: string[], sendingType: RecipientType, status: RecipientStatus): RecipientElement[] => { - const result = []; - for (const rawEmail of emails) { - const { email } = Str.parseEmail(rawEmail); + private createRecipientsElements = (container: JQuery, + emails: { email?: string, name?: string, invalid?: string }[], + sendingType: RecipientType, + status: RecipientStatus): RecipientElement[] => { + const result: RecipientElement[] = []; + for (const { email, name, invalid } of emails) { const recipientId = this.generateRecipientId(); - const recipientsHtml = `${Xss.escape(email || rawEmail)} ${Ui.spinner('green')}`; + const recipientsHtml = `${Xss.escape(email || invalid || '')} ${Ui.spinner('green')}`; Xss.sanitizeAppend(container.find('.recipients'), recipientsHtml); const element = document.getElementById(recipientId); if (element) { // if element wasn't created this means that Composer is used by another component @@ -754,7 +772,11 @@ export class ComposeRecipientsModule extends ViewModule { } }, this.view.errModule.handle('remove recipient with keyboard'))); this.addDraggableEvents(element); - const recipient = { email: email || rawEmail, element, id: recipientId, sendingType, status: email ? status : RecipientStatus.WRONG }; + const recipient = { email, name, invalid, element, id: recipientId, sendingType, status: email ? status : RecipientStatus.WRONG }; + if (recipient.status === RecipientStatus.WRONG) { + this.renderPubkeyResult(recipient, undefined); + } + // todo: display name if available this.addedRecipients.push(recipient); result.push(recipient); } @@ -769,18 +791,16 @@ export class ComposeRecipientsModule extends ViewModule { } const toLookupNoPubkeys = new Set(); for (const input of newContacts) { - // todo: create and use a lighter method in ContactStore that doesn't return any keys? const storedContact = await ContactStore.getOneWithAllPubkeys(undefined, input.email); - if (storedContact) { - if (!storedContact.info.name && input.name) { - await ContactStore.update(undefined, input.email, { name: input.name }); - } - } else if (!this.failedLookupEmails.includes(input.email)) { + if (storedContact?.info.name && input.name) { + await ContactStore.update(undefined, input.email, { name: input.name }); + } + if ((!storedContact || !storedContact.sortedPubkeys.length) && !this.failedLookupEmails.includes(input.email)) { toLookupNoPubkeys.add(input); } } await Promise.all(Array.from(toLookupNoPubkeys).map(c => this.view.storageModule - .updateLocalPubkeysFromRemote([], c.email, c.name || undefined) + .updateLocalPubkeysFromRemote([], c.email) .catch(() => this.failedLookupEmails.push(c.email)) )); }; @@ -804,43 +824,26 @@ export class ComposeRecipientsModule extends ViewModule { return 1; }; - private authContacts = async (acctEmail: string) => { - const connectToGoogleRecipientLine = this.addedRecipients[this.addedRecipients.length - 1]; - this.view.S.cached('input_to').val(connectToGoogleRecipientLine.email); - this.removeRecipient(connectToGoogleRecipientLine.element); - const authRes = await GoogleAuth.newAuthPopup({ acctEmail, scopes: GoogleAuth.defaultScopes('contacts') }); - if (authRes.result === 'Success') { - this.googleContactsSearchEnabled = true; - this.canReadEmails = true; - this.view.scopes.readContacts = true; - this.view.scopes.read = true; - await this.searchContacts(this.view.S.cached('input_to')); - } else if (authRes.result === 'Denied' || authRes.result === 'Closed') { - await Ui.modal.error('FlowCrypt needs this permission to search your contacts on Gmail. Without it, FlowCrypt will keep a separate contact list.'); - } else { - await Ui.modal.error(Lang.general.somethingWentWrongTryAgain(!!this.view.fesUrl)); - } - }; - + // todo: I guess we can combine this with reRenderRecipientFor private checkReciepientsKeys = async () => { - for (const recipientEl of this.addedRecipients.filter( + for (const recipientEl of this.getValidRecipients().filter( r => r.element.className.includes('no_pgp'))) { const email = $(recipientEl).text().trim(); const dbContacts = await ContactStore.getOneWithAllPubkeys(undefined, email); if (dbContacts && dbContacts.sortedPubkeys && dbContacts.sortedPubkeys.length) { recipientEl.element.classList.remove('no_pgp'); - await this.renderPubkeyResult(recipientEl, dbContacts.sortedPubkeys); + this.renderPubkeyResult(recipientEl, dbContacts); } } }; - private renderPubkeyResult = async ( - recipient: RecipientElement, sortedPubkeyInfos: PubkeyInfo[] | 'fail' | 'wrong' + private renderPubkeyResult = ( + recipient: RecipientElement, info: ContactInfoWithSortedPubkeys | undefined | 'fail' ) => { - // console.log(`>>>> renderPubkeyResult: ${JSON.stringify(sortedPubkeyInfos)}`); + // console.log(`>>>> renderPubkeyResult: ${JSON.stringify(info)}`); const el = recipient.element; - this.view.errModule.debug(`renderPubkeyResult.email(${recipient.email})`); - this.view.errModule.debug(`renderPubkeyResult.contact(${JSON.stringify(sortedPubkeyInfos)})`); + this.view.errModule.debug(`renderPubkeyResult.email(${recipient.email || recipient.invalid})`); + this.view.errModule.debug(`renderPubkeyResult.contact(${JSON.stringify(info)})`); $(el).children('img, i').remove(); const contentHtml = 'close' + 'close'; @@ -848,7 +851,11 @@ export class ComposeRecipientsModule extends ViewModule { .find('img.close-icon') .click(this.view.setHandler(target => this.removeRecipient(target.parentElement!), this.view.errModule.handle('remove recipient'))); $(el).removeClass(['failed', 'wrong', 'has_pgp', 'no_pgp', 'expired']); - if (sortedPubkeyInfos === PUBKEY_LOOKUP_RESULT_FAIL) { + if (recipient.status === RecipientStatus.WRONG) { + this.view.errModule.debug(`renderPubkeyResult: Setting email to wrong / misspelled in harsh mode: ${recipient.invalid}`); + $(el).attr('title', 'This email address looks misspelled. Please try again.'); + $(el).addClass("wrong"); + } else if (info === PUBKEY_LOOKUP_RESULT_FAIL) { recipient.status = RecipientStatus.FAILED; $(el).attr('title', 'Failed to load, click to retry'); $(el).addClass("failed"); @@ -856,12 +863,10 @@ export class ComposeRecipientsModule extends ViewModule { ''); $(el).find('.action_retry_pubkey_fetch').click(this.view.setHandler(async () => await this.refreshRecipients(), this.view.errModule.handle('refresh recipient'))); $(el).find('.remove-reciepient').click(this.view.setHandler(element => this.removeRecipient(element.parentElement!), this.view.errModule.handle('remove recipient'))); - } else if (sortedPubkeyInfos === PUBKEY_LOOKUP_RESULT_WRONG) { - recipient.status = RecipientStatus.WRONG; - this.view.errModule.debug(`renderPubkeyResult: Setting email to wrong / misspelled in harsh mode: ${recipient.email}`); - $(el).attr('title', 'This email address looks misspelled. Please try again.'); - $(el).addClass("wrong"); - } else if (sortedPubkeyInfos.length) { + } else if (info && info.sortedPubkeys.length) { + if (info.info.name) { + recipient.name = info.info.name; // todo: render the name + } // New logic: // 1. Keys are sorted in a special way. // 2. If there is at least one key: @@ -869,30 +874,33 @@ export class ComposeRecipientsModule extends ViewModule { // - else if first key is revoked, then REVOKED. // - else EXPIRED. // 3. Otherwise NO_PGP. - const firstKeyInfo = sortedPubkeyInfos[0]; + const firstKeyInfo = info.sortedPubkeys[0]; if (!firstKeyInfo.revoked && !KeyUtil.expired(firstKeyInfo.pubkey)) { recipient.status = RecipientStatus.HAS_PGP; $(el).addClass('has_pgp'); Xss.sanitizePrepend(el, ''); - $(el).attr('title', 'Does use encryption\n\n' + this.formatPubkeysHintText(sortedPubkeyInfos)); + $(el).attr('title', 'Does use encryption\n\n' + this.formatPubkeysHintText(info.sortedPubkeys)); } else if (firstKeyInfo.revoked) { recipient.status = RecipientStatus.REVOKED; $(el).addClass("revoked"); Xss.sanitizePrepend(el, ''); $(el).attr('title', 'Does use encryption but their public key is revoked. ' + 'You should ask them to send you an updated public key.\n\n' + - this.formatPubkeysHintText(sortedPubkeyInfos)); + this.formatPubkeysHintText(info.sortedPubkeys)); } else { recipient.status = RecipientStatus.EXPIRED; $(el).addClass("expired"); Xss.sanitizePrepend(el, ''); $(el).attr('title', 'Does use encryption but their public key is expired. ' + 'You should ask them to send you an updated public key.\n\n' + - this.formatPubkeysHintText(sortedPubkeyInfos)); + this.formatPubkeysHintText(info.sortedPubkeys)); } } else { recipient.status = RecipientStatus.NO_PGP; $(el).addClass("no_pgp"); + if (info?.info.name) { + recipient.name = info.info.name; // todo: render the name + } Xss.sanitizePrepend(el, ''); $(el).attr('title', 'Could not verify their encryption setup. You can encrypt the message with a password below. Alternatively, add their pubkey.'); } @@ -938,7 +946,7 @@ export class ComposeRecipientsModule extends ViewModule { }; private refreshRecipients = async () => { - const failedRecipients = this.addedRecipients.filter(r => r.element.className.includes('failed')); + const failedRecipients = this.getValidRecipients().filter(r => r.element.className.includes('failed')); await this.reEvaluateRecipients(failedRecipients); }; diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts index f7e2bde8e16..639afe20af7 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts @@ -16,13 +16,13 @@ import { GmailRes } from '../../../js/common/api/email-provider/gmail/gmail-pars import { KeyInfo } from '../../../js/common/core/crypto/key.js'; import { SendBtnTexts } from './compose-types.js'; import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js'; -import { Str } from '../../../js/common/core/common.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; -import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; +import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; +import { Str, Value } from '../../../js/common/core/common.js'; export class ComposeSendBtnModule extends ViewModule { @@ -112,7 +112,8 @@ export class ComposeSendBtnModule extends ViewModule { this.view.S.cached('send_btn_note').text(''); const newMsgData = this.view.inputModule.extractAll(); await this.view.errModule.throwIfFormValsInvalid(newMsgData); - await ContactStore.update(undefined, Array.prototype.concat.apply([], Object.values(newMsgData.recipients)), { lastUse: Date.now() }); + const emails = Value.arr.unique(Object.values(newMsgData.recipients).reduce((a, b) => a.concat(b), []).filter(x => x.email).map(x => x.email)); + await ContactStore.update(undefined, emails, { lastUse: Date.now() }); const msgObj = await GeneralMailFormatter.processNewMsg(this.view, newMsgData); if (msgObj) { await this.finalizeSendableMsg(msgObj); @@ -146,7 +147,7 @@ export class ComposeSendBtnModule extends ViewModule { if (this.view.myPubkeyModule.shouldAttach() && senderKi) { // todo: report on undefined? msg.attachments.push(Attachment.keyinfoAsPubkeyAttachment(senderKi)); } - await this.addNamesToMsg(msg); + msg.from = await this.addNameToEmail(msg.from); }; private extractInlineImagesToAttachments = (html: string) => { @@ -224,30 +225,24 @@ export class ComposeSendBtnModule extends ViewModule { } }; - private addNamesToMsg = async (msg: SendableMsg): Promise => { + private addNameToEmail = async (email: string): Promise => { + const parsedEmail = Str.parseEmail(email); + if (!parsedEmail.email) { + throw new Error(`Recipient email ${email} is not valid`); + } + if (parsedEmail.name) { + return Str.formatEmailWithOptionalName({ email: parsedEmail.email, name: parsedEmail.name }); + } const { sendAs } = await AcctStore.get(this.view.acctEmail, ['sendAs']); - const addNameToEmail = async (emails: string[]): Promise => { - return await Promise.all(emails.map(async email => { - let name: string | undefined; - if (sendAs && sendAs[email]?.name) { - name = sendAs[email].name!; - } else { - const [contact] = await ContactStore.get(undefined, [email]); - if (contact?.name) { - name = contact.name; - } - } - const fixedEmail = Str.parseEmail(email).email; - if (!fixedEmail) { - throw new Error(`Recipient email ${email} is not valid`); - } - return name ? `${Str.rmSpecialCharsKeepUtf(name, 'ALLOW-SOME')} <${fixedEmail}>` : fixedEmail; - })); - }; - msg.recipients.to = await addNameToEmail(msg.recipients.to || []); - msg.recipients.cc = await addNameToEmail(msg.recipients.cc || []); - msg.recipients.bcc = await addNameToEmail(msg.recipients.bcc || []); - msg.from = (await addNameToEmail([msg.from]))[0]; + let name: string | undefined; + if (sendAs && sendAs[email]?.name) { + name = sendAs[email].name!; + } else { + const [contact] = await ContactStore.get(undefined, [email]); + if (contact?.name) { + name = contact.name; + } + } + return Str.formatEmailWithOptionalName({ email: parsedEmail.email, name }); }; - } diff --git a/extension/chrome/elements/compose-modules/compose-sender-module.ts b/extension/chrome/elements/compose-modules/compose-sender-module.ts index 41d34f84240..5ec8b21d05f 100644 --- a/extension/chrome/elements/compose-modules/compose-sender-module.ts +++ b/extension/chrome/elements/compose-modules/compose-sender-module.ts @@ -78,7 +78,7 @@ export class ComposeSenderModule extends ViewModule { }; private actionInputFromChangeHanlder = async () => { - await this.view.recipientsModule.reEvaluateRecipients(this.view.recipientsModule.getRecipients()); + await this.view.recipientsModule.reEvaluateRecipients(this.view.recipientsModule.getValidRecipients()); await this.view.recipientsModule.setEmailsPreview(this.view.recipientsModule.getRecipients()); this.view.footerModule.onFooterUpdated(await this.view.footerModule.getFooterFromStorage(this.view.senderModule.getSender())); }; diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index c22d6080881..0bdc54e5000 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -3,7 +3,7 @@ 'use strict'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; -import { KeyInfo, KeyUtil, Key, PubkeyInfo, PubkeyResult } from '../../../js/common/core/crypto/key.js'; +import { KeyInfo, KeyUtil, Key, PubkeyInfo, PubkeyResult, ContactInfoWithSortedPubkeys } from '../../../js/common/core/crypto/key.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Assert } from '../../../js/common/assert.js'; import { Catch, UnreportableError } from '../../../js/common/platform/catch.js'; @@ -133,7 +133,7 @@ export class ComposeStorageModule extends ViewModule { */ public getUpToDatePubkeys = async ( email: string - ): Promise => { + ): Promise => { this.view.errModule.debug(`getUpToDatePubkeys.email(${email})`); const storedContact = await ContactStore.getOneWithAllPubkeys(undefined, email); this.view.errModule.debug(`getUpToDatePubkeys.storedContact.sortedPubkeys.length(${storedContact?.sortedPubkeys.length})`); @@ -145,7 +145,7 @@ export class ComposeStorageModule extends ViewModule { // an async method to update them this.updateLocalPubkeysFromRemote(storedContact.sortedPubkeys, email) .catch(ApiErr.reportIfSignificant); - return storedContact.sortedPubkeys; + return storedContact; } this.view.errModule.debug(`getUpToDatePubkeys.bestKey not usable, refreshing sync`); try { // no valid keys found, query synchronously, then return result @@ -157,7 +157,7 @@ export class ComposeStorageModule extends ViewModule { const updatedContact = await ContactStore.getOneWithAllPubkeys(undefined, email); this.view.errModule.debug(`getUpToDatePubkeys.updatedContact.sortedPubkeys.length(${updatedContact?.sortedPubkeys.length})`); this.view.errModule.debug(`getUpToDatePubkeys.updatedContact(${updatedContact})`); - return updatedContact?.sortedPubkeys ?? []; + return updatedContact; }; /** @@ -167,9 +167,7 @@ export class ComposeStorageModule extends ViewModule { * newer versions of public keys we already have (compared by fingerprint), then we * update the public keys we already have. */ - public updateLocalPubkeysFromRemote = async ( - storedPubkeys: PubkeyInfo[], email: string, name?: string - ): Promise => { + public updateLocalPubkeysFromRemote = async (storedPubkeys: PubkeyInfo[], email: string): Promise => { if (!email) { throw Error("Empty email"); } @@ -178,9 +176,6 @@ export class ComposeStorageModule extends ViewModule { if (await compareAndSavePubkeysToStorage(email, lookupResult.pubkeys, storedPubkeys)) { await this.view.recipientsModule.reRenderRecipientFor(email); } - if (name) { // update name - await ContactStore.update(undefined, email, { name }); - } } catch (e) { if (!ApiErr.isNetErr(e) && !ApiErr.isServerErr(e)) { Catch.reportErr(e); diff --git a/extension/chrome/elements/compose-modules/compose-types.ts b/extension/chrome/elements/compose-modules/compose-types.ts index c3c586308ce..3364bb9cdb7 100644 --- a/extension/chrome/elements/compose-modules/compose-types.ts +++ b/extension/chrome/elements/compose-modules/compose-types.ts @@ -3,8 +3,9 @@ 'use strict'; import { RecipientType } from '../../../js/common/api/shared/api.js'; -import { Recipients } from '../../../js/common/api/email-provider/email-provider-api.js'; +import { ParsedRecipients } from '../../../js/common/api/email-provider/email-provider-api.js'; import { KeyInfo, PubkeyResult } from '../../../js/common/core/crypto/key.js'; +import { EmailParts } from '../../../js/common/core/common.js'; export enum RecipientStatus { EVALUATING, @@ -16,8 +17,7 @@ export enum RecipientStatus { FAILED } -export interface RecipientElement { - email: string; +interface RecipientElementBase { sendingType: RecipientType; element: HTMLElement; id: string; @@ -25,6 +25,15 @@ export interface RecipientElement { evaluating?: Promise; } +export interface RecipientElement extends RecipientElementBase { + email?: string; + name?: string; + invalid?: string; +} + +export interface ValidRecipientElement extends RecipientElementBase, EmailParts { +} + export type MessageToReplyOrForward = { headers: { references: string, @@ -42,7 +51,7 @@ export type CollectKeysResult = { pubkeys: PubkeyResult[], emailsWithoutPubkeys: export type PopoverOpt = 'encrypt' | 'sign' | 'richtext'; export type PopoverChoices = { [key in PopoverOpt]: boolean }; -export type NewMsgData = { recipients: Recipients, subject: string, plaintext: string, plainhtml: string, pwd: string | undefined, from: string }; +export type NewMsgData = { recipients: ParsedRecipients, subject: string, plaintext: string, plainhtml: string, pwd: string | undefined, from: string }; export class SendBtnTexts { public static readonly BTN_ENCRYPT_AND_SEND: string = "Encrypt and Send"; diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index ee127ebb4a8..112ab935cd3 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -157,7 +157,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async ( authInfo: FcUuidAuth, newMsgData: NewMsgData ): Promise<{ bodyWithReplyToken: SendableMsgBody, replyToken: string }> => { - const recipients = Array.prototype.concat.apply([], Object.values(newMsgData.recipients)); + const recipients = Value.arr.unique(Object.values(newMsgData.recipients).reduce((a, b) => a.concat(b), []).map(x => x.email)); try { const response = await this.view.acctServer.messageToken(authInfo); const infoDiv = Ui.e('div', { diff --git a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts index a4db792668b..fdc9caa5c4a 100644 --- a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts @@ -9,13 +9,14 @@ import { PlainMsgMailFormatter } from './plain-mail-msg-formatter.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; import { SignedMsgMailFormatter } from './signed-msg-mail-formatter.js'; import { ComposeView } from '../../compose.js'; +import { Value } from '../../../../js/common/core/common.js'; export class GeneralMailFormatter { // returns undefined in case user cancelled decryption of the signing key public static processNewMsg = async (view: ComposeView, newMsgData: NewMsgData): Promise<{ msg: SendableMsg, senderKi: KeyInfo | undefined } | undefined> => { const choices = view.sendBtnModule.popover.choices; - const recipientsEmails = Array.prototype.concat.apply([], Object.values(newMsgData.recipients).filter(arr => !!arr)) as string[]; + const recipientsEmails = Value.arr.unique(Object.values(newMsgData.recipients).reduce((a, b) => a.concat(b), []).map(x => x.email)); if (!choices.encrypt && !choices.sign) { // plain return { senderKi: undefined, msg: await new PlainMsgMailFormatter(view).sendableMsg(newMsgData) }; } diff --git a/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts b/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts index 43f6c7e29b4..ad1033b663b 100644 --- a/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts @@ -11,6 +11,7 @@ import { MsgUtil } from '../../../../js/common/core/crypto/pgp/msg-util.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js'; import { ContactStore } from '../../../../js/common/platform/store/contact-store.js'; +import { Value } from '../../../../js/common/core/common.js'; export class SignedMsgMailFormatter extends BaseMailFormatter { @@ -40,8 +41,8 @@ export class SignedMsgMailFormatter extends BaseMailFormatter { // Removing them here will prevent Gmail from screwing up the signature newMsg.plaintext = newMsg.plaintext.split('\n').map(l => l.replace(/\s+$/g, '')).join('\n').trim(); const signedData = await MsgUtil.sign(signingPrv, newMsg.plaintext); - const allContacts = [...newMsg.recipients.to || [], ...newMsg.recipients.cc || [], ...newMsg.recipients.bcc || []]; - ContactStore.update(undefined, allContacts, { lastUse: Date.now() }).catch(Catch.reportErr); + const recipients = Value.arr.unique(Object.values(newMsg.recipients).reduce((a, b) => a.concat(b), []).map(x => x.email)); + ContactStore.update(undefined, recipients, { lastUse: Date.now() }).catch(Catch.reportErr); return await SendableMsg.createInlineArmored(this.acctEmail, this.headers(newMsg), signedData, attachments); } // pgp/mime detached signature - it must be signed later, while being mime-encoded diff --git a/extension/chrome/settings/modules/backup-manual-module.ts b/extension/chrome/settings/modules/backup-manual-module.ts index 832e2158482..a9327e8d483 100644 --- a/extension/chrome/settings/modules/backup-manual-module.ts +++ b/extension/chrome/settings/modules/backup-manual-module.ts @@ -44,7 +44,7 @@ export class BackupManualActionModule extends ViewModule { public doBackupOnEmailProvider = async (armoredKey: string) => { const emailMsg = String(await $.get({ url: '/chrome/emails/email_intro.template.htm', dataType: 'html' })); const emailAttachments = [this.asBackupFile(armoredKey)]; - const headers = { from: this.view.acctEmail, recipients: { to: [this.view.acctEmail] }, subject: GMAIL_RECOVERY_EMAIL_SUBJECTS[0] }; + const headers = { from: this.view.acctEmail, recipients: { to: [{ email: this.view.acctEmail }] }, subject: GMAIL_RECOVERY_EMAIL_SUBJECTS[0] }; const msg = await SendableMsg.createPlain(this.view.acctEmail, headers, { 'text/html': emailMsg }, emailAttachments); if (this.view.emailProvider === 'gmail') { return await this.view.gmail.msgSend(msg); diff --git a/extension/css/cryptup.css b/extension/css/cryptup.css index 051c099aaea..9c038cbef90 100644 --- a/extension/css/cryptup.css +++ b/extension/css/cryptup.css @@ -2125,10 +2125,6 @@ div#contacts ul li:not(.loading) { div#contacts ul li.active:not(.loading) { background: #d6d6d6; } -div#contacts ul li.auth_contacts { - background: #fff !important; - height: 24px; -} div#contacts .button { padding: 4px 20px; } div#contacts div.allow-google-contact-search { diff --git a/extension/js/common/api/account-server.ts b/extension/js/common/api/account-server.ts index e1353ad466a..3a8a1e0717a 100644 --- a/extension/js/common/api/account-server.ts +++ b/extension/js/common/api/account-server.ts @@ -5,7 +5,7 @@ import { isFesUsed } from '../helpers.js'; import { EnterpriseServer } from './account-servers/enterprise-server.js'; import { BackendRes, FcUuidAuth, FlowCryptComApi, ProfileUpdate } from './account-servers/flowcrypt-com-api.js'; -import { Recipients } from './email-provider/email-provider-api.js'; +import { ParsedRecipients } from './email-provider/email-provider-api.js'; import { Api, ProgressCb } from './shared/api.js'; /** @@ -56,7 +56,7 @@ export class AccountServer extends Api { encrypted: Uint8Array, replyToken: string, from: string, - recipients: Recipients, + recipients: ParsedRecipients, progressCb: ProgressCb ): Promise<{ url: string }> => { if (await this.isFesUsed()) { diff --git a/extension/js/common/api/account-servers/enterprise-server.ts b/extension/js/common/api/account-servers/enterprise-server.ts index 069900e540e..9a3336cf72a 100644 --- a/extension/js/common/api/account-servers/enterprise-server.ts +++ b/extension/js/common/api/account-servers/enterprise-server.ts @@ -8,12 +8,12 @@ import { Api, ProgressCb, ReqMethod } from '../shared/api.js'; import { AcctStore } from '../../platform/store/acct-store.js'; import { BackendRes, ProfileUpdate } from './flowcrypt-com-api.js'; -import { Dict } from '../../core/common.js'; +import { Dict, Str } from '../../core/common.js'; import { ErrorReport, UnreportableError } from '../../platform/catch.js'; import { ApiErr, BackendAuthErr } from '../shared/api-error.js'; import { FLAVOR, InMemoryStoreKeys } from '../../core/const.js'; import { Attachment } from '../../core/attachment.js'; -import { Recipients } from '../email-provider/email-provider-api.js'; +import { ParsedRecipients } from '../email-provider/email-provider-api.js'; import { Buf } from '../../core/buf.js'; import { DomainRulesJson } from '../../org-rules.js'; import { InMemoryStore } from '../../platform/store/in-memory-store.js'; @@ -108,7 +108,7 @@ export class EnterpriseServer extends Api { encrypted: Uint8Array, associateReplyToken: string, from: string, - recipients: Recipients, + recipients: ParsedRecipients, progressCb: ProgressCb ): Promise => { const content = new Attachment({ @@ -122,9 +122,9 @@ export class EnterpriseServer extends Api { data: Buf.fromUtfStr(JSON.stringify({ associateReplyToken, from, - to: recipients.to || [], - cc: recipients.cc || [], - bcc: recipients.bcc || [] + to: (recipients.to || []).map(Str.formatEmailWithOptionalName), + cc: (recipients.cc || []).map(Str.formatEmailWithOptionalName), + bcc: (recipients.bcc || []).map(Str.formatEmailWithOptionalName) })) }); const multipartBody = { content, details }; diff --git a/extension/js/common/api/email-provider/email-provider-api.ts b/extension/js/common/api/email-provider/email-provider-api.ts index 75ffff49436..dc152a02995 100644 --- a/extension/js/common/api/email-provider/email-provider-api.ts +++ b/extension/js/common/api/email-provider/email-provider-api.ts @@ -10,8 +10,10 @@ import { KeyInfo } from '../../core/crypto/key.js'; import { GmailRes } from './gmail/gmail-parser.js'; import { GmailResponseFormat } from './gmail/gmail.js'; import { SendableMsg } from './sendable-msg.js'; +import { EmailParts } from '../../core/common.js'; export type Recipients = { to?: string[], cc?: string[], bcc?: string[] }; +export type ParsedRecipients = { to?: EmailParts[], cc?: EmailParts[], bcc?: EmailParts[] }; export type ProviderContactsQuery = { substring: string }; export type ReplyParams = { diff --git a/extension/js/common/api/email-provider/gmail/gmail.ts b/extension/js/common/api/email-provider/gmail/gmail.ts index dd1137214f4..86c9a4a725c 100644 --- a/extension/js/common/api/email-provider/gmail/gmail.ts +++ b/extension/js/common/api/email-provider/gmail/gmail.ts @@ -384,7 +384,7 @@ export class Gmail extends EmailProviderApi implements EmailProviderInterface { } } const rawValidEmails = rawParsedResults.filter(r => r.address && Str.isEmailValid(r.address)); - const newValidResults: EmailProviderContact[] = await Promise.all(rawValidEmails.map((a) => { return { email: a.address!, name: a.name } as EmailProviderContact; })); + const newValidResults: EmailProviderContact[] = await Promise.all(rawValidEmails.map((a) => { return { email: a.address!, name: a.name }; })); const uniqueNewValidResults: EmailProviderContact[] = []; for (const newValidRes of newValidResults) { if (allResults.map(c => c.email).indexOf(newValidRes.email) === -1) { diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index 9ae52541f72..52ad232e212 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -6,16 +6,14 @@ import { Dict, Str } from '../../core/common.js'; import { Mime, MimeEncodeType, SendableMsgBody } from '../../core/mime.js'; import { Attachment } from '../../core/attachment.js'; import { Buf } from '../../core/buf.js'; -import { RecipientType } from '../shared/api.js'; import { KeyStore } from '../../platform/store/key-store.js'; import { KeyUtil } from '../../core/crypto/key.js'; - -type Recipients = { to?: string[], cc?: string[], bcc?: string[] }; +import { ParsedRecipients } from './email-provider-api.js'; type SendableMsgHeaders = { headers?: Dict; from: string; - recipients: Recipients; + recipients: ParsedRecipients; subject: string; thread?: string; }; @@ -119,7 +117,7 @@ export class SendableMsg { public headers: Dict, isDraft: boolean, public from: string, - public recipients: Recipients, + public recipients: ParsedRecipients, public subject: string, public body: SendableMsgBody, public attachments: Attachment[], @@ -130,7 +128,7 @@ export class SendableMsg { if (!allEmails.length && !isDraft) { throw new Error('The To: field is empty. Please add recipients and try again'); } - const invalidEmails = allEmails.filter(email => !Str.isEmailValid(email)); + const invalidEmails = allEmails.filter(email => !Str.isEmailValid(email.email)); if (invalidEmails.length) { throw new InvalidRecipientError(`The To: field contains invalid emails: ${invalidEmails.join(', ')}\n\nPlease check recipients and try again.`); } @@ -138,11 +136,10 @@ export class SendableMsg { public toMime = async () => { this.headers.From = this.from; - for (const recipientTypeStr of Object.keys(this.recipients)) { - const recipientType = recipientTypeStr as RecipientType; - if (this.recipients[recipientType] && this.recipients[recipientType]!.length) { + for (const [recipientType, value] of Object.entries(this.recipients)) { + if (value && value!.length) { // todo - properly escape/encode this header using emailjs - this.headers[recipientType[0].toUpperCase() + recipientType.slice(1)] = this.recipients[recipientType]!.map(h => h.replace(/[,]/g, '')).join(','); + this.headers[recipientType[0].toUpperCase() + recipientType.slice(1)] = value.map(h => Str.formatEmailWithOptionalName(h).replace(/[,]/g, '')).join(','); } } this.headers.Subject = this.subject; diff --git a/extension/js/common/api/shared/api.ts b/extension/js/common/api/shared/api.ts index 9224551eccd..4eb3a70c044 100644 --- a/extension/js/common/api/shared/api.ts +++ b/extension/js/common/api/shared/api.ts @@ -8,7 +8,7 @@ import { Attachment } from '../../core/attachment.js'; import { BrowserMsg } from '../../browser/browser-msg.js'; import { Buf } from '../../core/buf.js'; import { Catch } from '../../platform/catch.js'; -import { Dict } from '../../core/common.js'; +import { Dict, EmailParts } from '../../core/common.js'; import { Env } from '../../browser/env.js'; import { secureRandomBytes } from '../../platform/util.js'; import { ApiErr, AjaxErr } from './api-error.js'; @@ -17,10 +17,7 @@ export type ReqFmt = 'JSON' | 'FORM' | 'TEXT'; export type RecipientType = 'to' | 'cc' | 'bcc'; type ResFmt = 'json' | 'xhr'; export type ReqMethod = 'POST' | 'GET' | 'DELETE' | 'PUT'; -export type EmailProviderContact = { - email: string; - name?: string | null; -}; +export type EmailProviderContact = EmailParts; type ProviderContactsResults = { new: EmailProviderContact[], all: EmailProviderContact[] }; type RawAjaxErr = { // getAllResponseHeaders?: () => any, diff --git a/extension/js/common/core/common.ts b/extension/js/common/core/common.ts index 986ac4cc50f..0884a817a5d 100644 --- a/extension/js/common/core/common.ts +++ b/extension/js/common/core/common.ts @@ -9,6 +9,7 @@ export type Dict = { [key: string]: T; }; export type UrlParam = string | number | null | undefined | boolean | string[]; export type UrlParams = Dict; export type PromiseCancellation = { cancel: boolean }; +export type EmailParts = { email: string, name?: string }; export class Str { @@ -47,6 +48,10 @@ export class Str { return str.replace(/[.~!$%^*=?]/gi, ''); }; + public static formatEmailWithOptionalName = ({ email, name }: EmailParts): string => { + return name ? `${Str.rmSpecialCharsKeepUtf(name, 'ALLOW-SOME')} <${email}>` : email; + }; + public static prettyPrint = (obj: any) => { return (typeof obj === 'object') ? JSON.stringify(obj, undefined, 2).replace(/ /g, ' ').replace(/\n/g, '
') : String(obj); }; diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index 90fd3e6515b..52c3c139756 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -10,6 +10,7 @@ import { opgp } from './pgp/openpgpjs-custom.js'; import { OpenPGPKey } from './pgp/openpgp-key.js'; import { SmimeKey } from './smime/smime-key.js'; import { MsgBlock } from '../msg-block.js'; +import { EmailParts } from '../common.js'; /** * This is a common Key interface for both OpenPGP and X.509 keys. @@ -49,11 +50,11 @@ export type PubkeyResult = { pubkey: Key, email: string, isMine: boolean }; export type Contact = { email: string; - name: string | null; + name?: string; pubkey: Key | undefined; hasPgp: 0 | 1; fingerprint: string | null; - lastUse: number | null; + // lastUse: number | null; pubkeyLastCheck: number | null; expiresOn: number | null; revoked: boolean; @@ -80,6 +81,13 @@ export interface PubkeyInfo { revoked: boolean; } +export type ContactInfo = EmailParts; + +export interface ContactInfoWithSortedPubkeys { + info: ContactInfo, + sortedPubkeys: PubkeyInfoWithLastCheck[] +} + export interface PubkeyInfoWithLastCheck extends PubkeyInfo { lastCheck?: number | undefined; } diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index d8508592870..5da36565d97 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -3,8 +3,8 @@ import { AbstractStore } from './abstract-store.js'; import { Catch } from '../catch.js'; import { BrowserMsg } from '../../browser/browser-msg.js'; -import { DateUtility, Str, Value } from '../../core/common.js'; -import { Key, Contact, KeyUtil, PubkeyInfo, PubkeyInfoWithLastCheck } from '../../core/crypto/key.js'; +import { DateUtility, EmailParts, Str, Value } from '../../core/common.js'; +import { Key, Contact, KeyUtil, PubkeyInfo, ContactInfoWithSortedPubkeys, ContactInfo } from '../../core/crypto/key.js'; // tslint:disable:no-null-keyword @@ -39,9 +39,7 @@ export type ContactV4 = { revocations: Revocation[] }; -export type ContactPreview = { - email: string; - name: string | null; +export type ContactPreview = EmailParts & { hasPgp: 0 | 1; lastUse: number | null; }; @@ -62,11 +60,6 @@ type ContactUpdateParsed = { type DbContactFilter = { hasPgp?: boolean, substring?: string, limit?: number }; -type EmailWithSortedPubkeys = { - info: Email, // todo: convert to a model class, exclude unnecessary fields like searchable - sortedPubkeys: PubkeyInfoWithLastCheck[] -}; - const x509postfix = "-X509"; /** @@ -111,12 +104,12 @@ export class ContactStore extends AbstractStore { }); }; - public static previewObj = ({ email, name }: { email: string, name?: string | null }): ContactPreview => { + public static previewObj = ({ email, name }: EmailParts): ContactPreview => { const validEmail = Str.parseEmail(email).email; if (!validEmail) { throw new Error(`Cannot handle the contact because email is not valid: ${email}`); } - return { email: validEmail, name: name || null, hasPgp: 0, lastUse: null }; + return { email: validEmail, name, hasPgp: 0, lastUse: null }; }; /** @@ -206,7 +199,7 @@ export class ContactStore extends AbstractStore { }; public static getOneWithAllPubkeys = async (db: IDBDatabase | undefined, email: string): - Promise => { + Promise => { if (!db) { // relay op through background process // tslint:disable-next-line:no-unsafe-any return await BrowserMsg.send.bg.await.db({ f: 'getOneWithAllPubkeys', args: [email] }); @@ -244,7 +237,10 @@ export class ContactStore extends AbstractStore { }, reject); }); - return emailEntity ? { info: emailEntity, sortedPubkeys: await ContactStore.sortKeys(pubkeys, revocations) } : undefined; + return emailEntity ? { + info: { email: emailEntity.email, name: emailEntity.name || undefined }, + sortedPubkeys: await ContactStore.sortKeys(pubkeys, revocations) + } : undefined; }; // todo: return parsed and with applied revocation @@ -630,17 +626,13 @@ export class ContactStore extends AbstractStore { return { fingerprint: key?.id ?? null, expiresOn: DateUtility.asNumber(key?.expiration) }; }; - private static toContactFromKey = (email: Email | undefined, key: Key | undefined, lastCheck: number | undefined | null, revokedExternally: boolean): Contact | undefined => { - if (!email) { - return; - } + private static toContactFromKey = (email: ContactInfo, key: Key | undefined, lastCheck: number | undefined | null, revokedExternally: boolean): Contact | undefined => { const safeKey = revokedExternally ? undefined : key; return { email: email.email, name: email.name, pubkey: safeKey, hasPgp: safeKey ? 1 : 0, - lastUse: email.lastUse, pubkeyLastCheck: lastCheck ?? null, ...ContactStore.getKeyAttributes(key), revoked: revokedExternally || Boolean(key?.revoked) @@ -648,6 +640,6 @@ export class ContactStore extends AbstractStore { }; private static toContactPreview = (result: Email): ContactPreview => { - return { email: result.email, name: result.name, hasPgp: result.fingerprints.length > 0 ? 1 : 0, lastUse: result.lastUse }; + return { email: result.email, name: result.name || undefined, hasPgp: result.fingerprints.length > 0 ? 1 : 0, lastUse: result.lastUse }; }; } diff --git a/test/source/mock/fes/fes-endpoints.ts b/test/source/mock/fes/fes-endpoints.ts index 63711d10046..d7150c782dc 100644 --- a/test/source/mock/fes/fes-endpoints.ts +++ b/test/source/mock/fes/fes-endpoints.ts @@ -59,9 +59,9 @@ export const mockFesEndpoints: HandlersDefinition = { authenticate(req, 'oidc'); expect(body).to.contain('-----BEGIN PGP MESSAGE-----'); expect(body).to.contain('"associateReplyToken":"mock-fes-reply-token"'); - expect(body).to.contain('"to":["to@example.com"]'); + expect(body).to.contain('"to":["Mr To "]'); expect(body).to.contain('"cc":[]'); - expect(body).to.contain('"bcc":["bcc@example.com"]'); + expect(body).to.contain('"bcc":["Mr Bcc "]'); expect(body).to.contain('"from":"user@standardsubdomainfes.test:8001"'); return { 'url': `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID` }; } diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index 5a7f44c32bf..2fce70d42ac 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -40,12 +40,11 @@ class PwdEncryptedMessageWithFlowCryptComApiTestStrategy implements ITestMsgStra class PwdEncryptedMessageWithFesIdTokenTestStrategy implements ITestMsgStrategy { public test = async (mimeMsg: ParsedMail) => { - const senderEmail = Str.parseEmail(mimeMsg.from!.text).email; const expectedSenderEmail = 'user@standardsubdomainfes.test:8001'; - if (senderEmail !== expectedSenderEmail) { - throw new HttpClientErr(`Unexpected sender email ${senderEmail}, expecting ${expectedSenderEmail}`); - } - if (!mimeMsg.text?.includes(`${senderEmail} has sent you a password-encrypted email`)) { + expect(mimeMsg.from!.text).to.equal(`First Last <${expectedSenderEmail}>`); + expect((mimeMsg.to as AddressObject).text).to.equal('Mr To '); + expect((mimeMsg.bcc as AddressObject).text).to.equal('Mr Bcc '); + if (!mimeMsg.text?.includes(`${expectedSenderEmail} has sent you a password-encrypted email`)) { throw new HttpClientErr(`Error checking sent text in:\n\n${mimeMsg.text}`); } if (!mimeMsg.text?.includes('http://fes.standardsubdomainfes.test:8001/message/FES-MOCK-MESSAGE-ID')) { diff --git a/test/source/platform/store/contact-store.ts b/test/source/platform/store/contact-store.ts index e52edf59962..8a5c7efc188 100644 --- a/test/source/platform/store/contact-store.ts +++ b/test/source/platform/store/contact-store.ts @@ -61,7 +61,7 @@ export class ContactStore { } }; - private static obj = async ({ email, name, pubkey, lastUse, lastCheck }: any): Promise => { + private static obj = async ({ email, name, pubkey, lastCheck }: any): Promise => { if (!pubkey) { return { email, @@ -69,7 +69,6 @@ export class ContactStore { pubkey: undefined, hasPgp: 0, // number because we use it for sorting fingerprint: null, - lastUse: lastUse || null, pubkeyLastCheck: null, expiresOn: null, revoked: false @@ -82,7 +81,6 @@ export class ContactStore { pubkey: pk, hasPgp: 1, // number because we use it for sorting fingerprint: pk.id, - lastUse, pubkeyLastCheck: lastCheck, revoked: pk.revoked } as Contact; diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 797d070b45a..3ed98d55e51 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1535,6 +1535,14 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); await SetupPageRecipe.manualEnter(settingsPage, 'flowcrypt.test.key.used.pgp', { submitPubkey: false, usedPgpBefore: false }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + // add names to contacts + const dbPage = await browser.newPage(t, TestUrls.extension('chrome/dev/ci_unit_test.htm')); + await dbPage.page.evaluate(async () => { + const db = await (window as any).ContactStore.dbOpen(); + await (window as any).ContactStore.update(db, 'to@example.com', { name: 'Mr To' }); + await (window as any).ContactStore.update(db, 'bcc@example.com', { name: 'Mr Bcc' }); + }); + await dbPage.close(); const subject = 'PWD encrypted message with FES - ID TOKEN'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'user@standardsubdomainfes.test:8001'); await ComposePageRecipe.fillMsg(composePage, { to: 'to@example.com', bcc: 'bcc@example.com' }, subject); From 5b0913861d6cf57fb1def8fa0f47010694802848 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 9 Feb 2022 18:40:46 +0000 Subject: [PATCH 02/11] fix --- .../chrome/elements/compose-modules/compose-recipients-module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index 165b019d8e9..f5ddfa888ee 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -153,6 +153,7 @@ export class ComposeRecipientsModule extends ViewModule { this.view.errModule.debug(`parseRenderRecipients(force: ${force}).4`); } else { this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); + $('body').attr('data-test-state', 'ready'); } } }; From ec7102564ce737621e167333e20683c72f79f2a2 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 10 Feb 2022 12:01:45 +0000 Subject: [PATCH 03/11] fixed unit test --- test/source/tests/browser-unit-tests/unit-ContactStore.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/source/tests/browser-unit-tests/unit-ContactStore.js b/test/source/tests/browser-unit-tests/unit-ContactStore.js index e1ce29e775c..79d90a56036 100644 --- a/test/source/tests/browser-unit-tests/unit-ContactStore.js +++ b/test/source/tests/browser-unit-tests/unit-ContactStore.js @@ -267,9 +267,6 @@ BROWSER_UNIT_TEST_NAME(`ContactStore saves and returns dates as numbers`); const lastUse = pubkeyLastCheck + 1000; await ContactStore.update(undefined, email, { pubkey: testConstants.expiredPub, pubkeyLastCheck, lastUse }); const [loaded] = await ContactStore.get(undefined, [email]); - if (typeof loaded.lastUse !== 'number') { - throw Error(`lastUse was expected to be a number, but got ${typeof loaded.lastUse}`); - } if (typeof loaded.pubkeyLastCheck !== 'number') { throw Error(`pubkeyLastCheck was expected to be a number, but got ${typeof loaded.pubkeyLastCheck}`); } From c0914b3fbb76bd356d544f6d5037ca4aefc72acd Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 10 Feb 2022 12:20:20 +0000 Subject: [PATCH 04/11] deadlock fix --- .../compose-modules/compose-draft-module.ts | 2 +- .../compose-recipients-module.ts | 29 +++++++++---------- .../compose-modules/compose-render-module.ts | 4 +-- .../compose-modules/compose-sender-module.ts | 1 - .../compose-modules/compose-size-module.ts | 4 +-- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts index 330c9f99bdd..5cf2b6023cd 100644 --- a/extension/chrome/elements/compose-modules/compose-draft-module.ts +++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts @@ -274,7 +274,7 @@ export class ComposeDraftModule extends ViewModule { }; private fillAndRenderDraftHeaders = async (decoded: MimeContent) => { - await this.view.recipientsModule.addRecipientsAndShowPreview({ to: decoded.to, cc: decoded.cc, bcc: decoded.bcc }); + 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/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index f5ddfa888ee..8379e033e94 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -214,10 +214,10 @@ export class ComposeRecipientsModule extends ViewModule { this.view.S.cached('contacts').children().not('ul').remove(); }; - public addRecipientsAndShowPreview = async (recipients: Recipients) => { + public addRecipientsAndShowPreview = (recipients: Recipients) => { this.view.recipientsModule.addRecipients(recipients).catch(Catch.reportErr); this.view.recipientsModule.showHideCcAndBccInputsIfNeeded(); - await this.view.recipientsModule.setEmailsPreview(this.getRecipients()); + this.view.recipientsModule.setEmailsPreview(); }; public reEvaluateRecipients = async (recipients: ValidRecipientElement[]) => { @@ -252,6 +252,7 @@ export class ComposeRecipientsModule extends ViewModule { callback(recipientEls); } } + this.setEmailsPreview(); $('body').attr('data-test-state', 'ready'); this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); }; @@ -260,12 +261,9 @@ export class ComposeRecipientsModule extends ViewModule { * Generate content for emails preview in some container * when recipient inputs are collapsed. * e.g. 'test@test.com, test2@test.com [3 more]' - * - * @param container - HTMLElement where emails have to be inserted - * @param recipients - Recipients that should be previewed */ - public setEmailsPreview = async (recipients: RecipientElement[]): Promise => { - const orderedRecipients = recipients.sort(this.orderRecipientsBySendingType); + public setEmailsPreview = (): void => { + const orderedRecipients = this.getRecipients().sort(this.orderRecipientsBySendingType); if (orderedRecipients.length) { this.view.S.cached('recipients_placeholder').find('.placeholder').css('display', 'none'); } else { @@ -275,10 +273,12 @@ export class ComposeRecipientsModule extends ViewModule { } const container = this.view.S.cached('recipients_placeholder').find('.email_preview'); if (orderedRecipients.find(r => r.status === RecipientStatus.EVALUATING)) { - container.append(`Loading Reciepients ${Ui.spinner('green')}`); // xss-direct - await Promise.all(orderedRecipients.filter(r => r.evaluating).map(r => r.evaluating!)); - container.find('r_loader').remove(); + if (container.find('r_loader').length === 0) { + container.append(`Loading Recipients ${Ui.spinner('green')}`); // xss-direct + } + return; } + container.find('r_loader').remove(); Xss.sanitizeRender(container, ' more'); const maxWidth = container.parent().width()! - this.view.S.cached('container_cc_bcc_buttons').width()!; const rest = container.find('.rest'); @@ -333,7 +333,7 @@ export class ComposeRecipientsModule extends ViewModule { this.showHideCcAndBccInputsIfNeeded(); this.view.S.cached('input_addresses_container_outer').addClass('invisible'); this.view.S.cached('recipients_placeholder').css('display', 'flex'); - await this.setEmailsPreview(this.addedRecipients); + this.setEmailsPreview(); this.hideContacts(); this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); } @@ -386,8 +386,8 @@ export class ComposeRecipientsModule extends ViewModule { this.view.errModule.debug(`re-rendering recipient: ${email}`); this.renderPubkeyResult(recipient, emailAndPubkeys); } - this.view.recipientsModule.showHideCcAndBccInputsIfNeeded(); - await this.view.recipientsModule.setEmailsPreview(this.getRecipients()); + this.showHideCcAndBccInputsIfNeeded(); + this.setEmailsPreview(); }; private inputsBlurHandler = async (target: HTMLElement, e: JQuery.Event) => { @@ -466,7 +466,6 @@ export class ComposeRecipientsModule extends ViewModule { clearInterval(this.addedPubkeyDbLookupInterval); recipientsHasPgp.push(recipient); await this.evaluateRecipients(recipientsHasPgp); - await this.setEmailsPreview(this.getRecipients()); } } }, 1000); @@ -844,7 +843,7 @@ export class ComposeRecipientsModule extends ViewModule { // console.log(`>>>> renderPubkeyResult: ${JSON.stringify(info)}`); const el = recipient.element; this.view.errModule.debug(`renderPubkeyResult.email(${recipient.email || recipient.invalid})`); - this.view.errModule.debug(`renderPubkeyResult.contact(${JSON.stringify(info)})`); + // this.view.errModule.debug(`renderPubkeyResult.contact(${JSON.stringify(info)})`); $(el).children('img, i').remove(); const contentHtml = 'close' + 'close'; diff --git a/extension/chrome/elements/compose-modules/compose-render-module.ts b/extension/chrome/elements/compose-modules/compose-render-module.ts index 91f61bd8fb8..284468a1452 100644 --- a/extension/chrome/elements/compose-modules/compose-render-module.ts +++ b/extension/chrome/elements/compose-modules/compose-render-module.ts @@ -60,7 +60,7 @@ export class ComposeRenderModule extends ViewModule { } else { this.view.S.cached('body').css('overflow', 'hidden'); // do not enable this for replies or automatic resize won't work await this.renderComposeTable(); - await this.view.recipientsModule.setEmailsPreview(this.view.recipientsModule.getRecipients()); + this.view.recipientsModule.setEmailsPreview(); } this.view.sendBtnModule.resetSendBtn(); await this.view.sendBtnModule.popover.render(); @@ -70,7 +70,7 @@ export class ComposeRenderModule extends ViewModule { public renderReplyMsgComposeTable = async (): Promise => { this.view.S.cached('prompt').css({ display: 'none' }); this.view.recipientsModule.showHideCcAndBccInputsIfNeeded(); - await this.view.recipientsModule.setEmailsPreview(this.view.recipientsModule.getRecipients()); + this.view.recipientsModule.setEmailsPreview(); await this.renderComposeTable(); if (this.view.replyParams) { const thread = await this.view.emailProvider.threadGet(this.view.threadId, 'metadata'); diff --git a/extension/chrome/elements/compose-modules/compose-sender-module.ts b/extension/chrome/elements/compose-modules/compose-sender-module.ts index 5ec8b21d05f..0390b61bde9 100644 --- a/extension/chrome/elements/compose-modules/compose-sender-module.ts +++ b/extension/chrome/elements/compose-modules/compose-sender-module.ts @@ -79,7 +79,6 @@ export class ComposeSenderModule extends ViewModule { private actionInputFromChangeHanlder = async () => { await this.view.recipientsModule.reEvaluateRecipients(this.view.recipientsModule.getValidRecipients()); - await this.view.recipientsModule.setEmailsPreview(this.view.recipientsModule.getRecipients()); this.view.footerModule.onFooterUpdated(await this.view.footerModule.getFooterFromStorage(this.view.senderModule.getSender())); }; diff --git a/extension/chrome/elements/compose-modules/compose-size-module.ts b/extension/chrome/elements/compose-modules/compose-size-module.ts index 275d8f0ff89..aeee7c2c8df 100644 --- a/extension/chrome/elements/compose-modules/compose-size-module.ts +++ b/extension/chrome/elements/compose-modules/compose-size-module.ts @@ -125,7 +125,7 @@ export class ComposeSizeModule extends ViewModule { this.resizeComposeBox(); this.setInputTextHeightManuallyIfNeeded(true); if (this.view.S.cached('recipients_placeholder').is(':visible')) { - await this.view.recipientsModule.setEmailsPreview(this.view.recipientsModule.getRecipients()); + this.view.recipientsModule.setEmailsPreview(); } }; @@ -156,7 +156,7 @@ export class ComposeSizeModule extends ViewModule { this.view.S.cached('icon_popout').attr('src', '/img/svgs/maximize.svg').attr('title', 'Full screen'); } if (this.view.S.cached('recipients_placeholder').is(':visible')) { - await this.view.recipientsModule.setEmailsPreview(this.view.recipientsModule.getRecipients()); + this.view.recipientsModule.setEmailsPreview(); } this.composeWindowIsMaximized = !this.composeWindowIsMaximized; }; From 4e75641f491619b56175c9fd45849cbf4f2e574a Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Fri, 11 Feb 2022 19:16:49 +0000 Subject: [PATCH 05/11] restored some code and re-organized saving retrieved pubkeys --- .../compose-modules/compose-recipients-module.ts | 11 ++++++----- .../compose-modules/compose-storage-module.ts | 6 ++++-- extension/js/common/shared.ts | 10 +++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index 8379e033e94..73d8c343d33 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -792,15 +792,16 @@ export class ComposeRecipientsModule extends ViewModule { const toLookupNoPubkeys = new Set(); for (const input of newContacts) { const storedContact = await ContactStore.getOneWithAllPubkeys(undefined, input.email); - if (storedContact?.info.name && input.name) { - await ContactStore.update(undefined, input.email, { name: input.name }); - } - if ((!storedContact || !storedContact.sortedPubkeys.length) && !this.failedLookupEmails.includes(input.email)) { + if (storedContact) { + if (!storedContact.info.name && input.name) { + await ContactStore.update(undefined, input.email, { name: input.name }); + } + } else if (!this.failedLookupEmails.includes(input.email)) { toLookupNoPubkeys.add(input); } } await Promise.all(Array.from(toLookupNoPubkeys).map(c => this.view.storageModule - .updateLocalPubkeysFromRemote([], c.email) + .updateLocalPubkeysFromRemote([], c.email, c.name) .catch(() => this.failedLookupEmails.push(c.email)) )); }; diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index 0bdc54e5000..639553346b0 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -167,13 +167,15 @@ export class ComposeStorageModule extends ViewModule { * newer versions of public keys we already have (compared by fingerprint), then we * update the public keys we already have. */ - public updateLocalPubkeysFromRemote = async (storedPubkeys: PubkeyInfo[], email: string): Promise => { + public updateLocalPubkeysFromRemote = async ( + storedPubkeys: PubkeyInfo[], email: string, name?: string + ): Promise => { if (!email) { throw Error("Empty email"); } try { const lookupResult = await this.view.pubLookup.lookupEmail(email); - if (await compareAndSavePubkeysToStorage(email, lookupResult.pubkeys, storedPubkeys)) { + if (await compareAndSavePubkeysToStorage({ email, name }, lookupResult.pubkeys, storedPubkeys)) { await this.view.recipientsModule.reRenderRecipientFor(email); } } catch (e) { diff --git a/extension/js/common/shared.ts b/extension/js/common/shared.ts index 8e4aea31d1c..b65d4e24854 100644 --- a/extension/js/common/shared.ts +++ b/extension/js/common/shared.ts @@ -2,21 +2,25 @@ 'use strict'; +import { EmailParts } from './core/common.js'; import { KeyUtil, PubkeyInfo } from './core/crypto/key.js'; import { ContactStore } from './platform/store/contact-store.js'; /** * Save fetched keys if they are newer versions of public keys we already have (compared by fingerprint) */ -export const compareAndSavePubkeysToStorage = async (email: string, fetchedPubkeys: string[], storedPubkeys: PubkeyInfo[]): Promise => { +export const compareAndSavePubkeysToStorage = async ({ email, name }: EmailParts, fetchedPubkeys: string[], storedPubkeys: PubkeyInfo[]): Promise => { let updated = false; for (const fetched of await Promise.all(fetchedPubkeys.map(KeyUtil.parse))) { const stored = storedPubkeys.find(p => KeyUtil.identityEquals(p.pubkey, fetched))?.pubkey; if (!stored || KeyUtil.isFetchedNewer({ fetched, stored })) { - await ContactStore.update(undefined, email, { pubkey: fetched, pubkeyLastCheck: Date.now() }); + await ContactStore.update(undefined, email, { pubkey: fetched, pubkeyLastCheck: Date.now(), name }); updated = true; } } + if (!updated && name) { + await ContactStore.update(undefined, email, { name }); + } return updated; }; @@ -28,6 +32,6 @@ export const saveFetchedPubkeysIfNewerThanInStorage = async ({ email, pubkeys }: return false; } const storedContact = await ContactStore.getOneWithAllPubkeys(undefined, email); - return await compareAndSavePubkeysToStorage(email, pubkeys, storedContact?.sortedPubkeys ?? []); + return await compareAndSavePubkeysToStorage({ email }, pubkeys, storedContact?.sortedPubkeys ?? []); }; From d54063121c0979cd653ef2812db75cb25cbe5369 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 19 Feb 2022 16:38:46 +0000 Subject: [PATCH 06/11] display recipient names --- .../compose-modules/compose-recipients-module.ts | 9 +++++---- extension/css/cryptup.css | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index ceb9e74dcdd..026e106d333 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -757,7 +757,7 @@ export class ComposeRecipientsModule extends ViewModule { const result: RecipientElement[] = []; for (const { email, name, invalid } of emails) { const recipientId = this.generateRecipientId(); - const recipientsHtml = `${Xss.escape(email || invalid || '')} ${Ui.spinner('green')}`; + const recipientsHtml = `${Xss.escape(name || '')}${Xss.escape(email || invalid || '')} ${Ui.spinner('green')}`; Xss.sanitizeAppend(container.find('.recipients'), recipientsHtml); const element = document.getElementById(recipientId); if (element) { // if element wasn't created this means that Composer is used by another component @@ -776,7 +776,6 @@ export class ComposeRecipientsModule extends ViewModule { if (recipient.status === RecipientStatus.WRONG) { this.renderPubkeyResult(recipient, undefined); } - // todo: display name if available this.addedRecipients.push(recipient); result.push(recipient); } @@ -866,7 +865,8 @@ export class ComposeRecipientsModule extends ViewModule { $(el).find('.remove-reciepient').click(this.view.setHandler(element => this.removeRecipient(element.parentElement!), this.view.errModule.handle('remove recipient'))); } else if (info && info.sortedPubkeys.length) { if (info.info.name) { - recipient.name = info.info.name; // todo: render the name + recipient.name = info.info.name; + $(el).find('.recipient-name').text(Xss.escape(info.info.name)); } // New logic: // 1. Keys are sorted in a special way. @@ -900,7 +900,8 @@ export class ComposeRecipientsModule extends ViewModule { recipient.status = RecipientStatus.NO_PGP; $(el).addClass("no_pgp"); if (info?.info.name) { - recipient.name = info.info.name; // todo: render the name + recipient.name = info.info.name; + $(el).find('.recipient-name').text(Xss.escape(info.info.name)); } Xss.sanitizePrepend(el, ''); $(el).attr('title', 'Could not verify their encryption setup. You can encrypt the message with a password below. Alternatively, add their pubkey.'); diff --git a/extension/css/cryptup.css b/extension/css/cryptup.css index b7b0578aec5..a31b0fe3a35 100644 --- a/extension/css/cryptup.css +++ b/extension/css/cryptup.css @@ -1564,6 +1564,15 @@ table#compose td.recipients-inputs > div#input_addresses_container div .recipien table#compose td.recipients-inputs > div#input_addresses_container div .recipients > span.failed { border: 1px dashed #a44; } table#compose td.recipients-inputs > div#input_addresses_container div .recipients > span.failed .close-icon { opacity: 1; } +.recipient-name { + font-weight: bold; + margin: 2px 4px 0 0; +} + +.recipient-name:empty { + margin: 0; +} + table#compose td.recipients-inputs > div#input_addresses_container div .recipients i.drag-cursor { width: 2px; height: 20px; From 51446f9babd61a5aa56f089c20f208e996bf5333 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 20 Feb 2022 10:48:52 +0000 Subject: [PATCH 07/11] fixed tests --- .../compose-recipients-module.ts | 4 +- test/source/tests/compose.ts | 58 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index 026e106d333..6c65b596f5f 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -757,7 +757,9 @@ export class ComposeRecipientsModule extends ViewModule { const result: RecipientElement[] = []; for (const { email, name, invalid } of emails) { const recipientId = this.generateRecipientId(); - const recipientsHtml = `${Xss.escape(name || '')}${Xss.escape(email || invalid || '')} ${Ui.spinner('green')}`; + const recipientsHtml = `` + + `${Xss.escape(name || '')}` + + `${Xss.escape(email || invalid || '')} ${Ui.spinner('green')}`; Xss.sanitizeAppend(container.find('.recipients'), recipientsHtml); const element = document.getElementById(recipientId); if (element) { // if element wasn't created this means that Composer is used by another component diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 1e1df9370a3..d709ae39a36 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -7,7 +7,7 @@ import { Config, Util } from './../util'; import { writeFileSync } from 'fs'; import { AvaContext } from './tooling'; import { ComposePageRecipe } from './page-recipe/compose-page-recipe'; -import { Dict } from './../core/common'; +import { Dict, EmailParts } from './../core/common'; import { GoogleData } from './../mock/google/google-data'; import { InboxPageRecipe } from './page-recipe/inbox-page-recipe'; import { OauthPageRecipe } from './page-recipe/oauth-page-recipe'; @@ -567,7 +567,11 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true }); await composePage.waitAndClick('@action-accept-reply-all-prompt', { delay: 2 }); await ComposePageRecipe.fillMsg(composePage, { bcc: "test@email.com" }, undefined, undefined, undefined, 'reply'); - await expectRecipientElements(composePage, { to: ['censored@email.com'], cc: ['censored@email.com'], bcc: ['test@email.com'] }); + await expectRecipientElements(composePage, { + to: [{ email: 'censored@email.com' }], + cc: [{ email: 'censored@email.com' }], + bcc: [{ email: 'test@email.com' }] + }); await Util.sleep(3); await ComposePageRecipe.sendAndClose(composePage, { password: 'test-pass' }); })); @@ -683,7 +687,11 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te testWithBrowser('compatibility', async (t, browser) => { const appendUrl = 'draftId=draft-1'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl }); - await expectRecipientElements(composePage, { to: ['flowcryptcompatibility@gmail.com'], cc: ['flowcrypt.compatibility@gmail.com'], bcc: ['human@flowcrypt.com'] }); + await expectRecipientElements(composePage, { + to: [{ email: 'flowcryptcompatibility@gmail.com', name: 'First Last' }], + cc: [{ email: 'flowcrypt.compatibility@gmail.com', name: 'First Last' }], + bcc: [{ email: 'human@flowcrypt.com' }] + }); const subjectElem = await composePage.waitAny('@input-subject'); expect(await PageRecipe.getElementPropertyJson(subjectElem, 'value')).to.equal('Test Draft - New Message'); expect((await composePage.read('@input-body')).trim()).to.equal('Testing Drafts (Do not delete)'); @@ -705,7 +713,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await settingsPage.close(); const appendUrl = 'draftId=17c041fd27858466'; const composePage = await ComposePageRecipe.openStandalone(t, browser, acctEmail, { appendUrl }); - await expectRecipientElements(composePage, { to: ['smime@recipient.com'] }); + await expectRecipientElements(composePage, { to: [{ email: 'smime@recipient.com' }] }); const subjectElem = await composePage.waitAny('@input-subject'); expect(await PageRecipe.getElementPropertyJson(subjectElem, 'value')).to.equal('Test S/MIME Encrypted Draft'); expect((await composePage.read('@input-body')).trim()).to.equal('test text'); @@ -755,7 +763,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const appendUrl = 'threadId=16cfa9001baaac0a&skipClickPrompt=___cu_false___&ignoreDraft=___cu_false___&replyMsgId=16cfa9001baaac0a&draftId=draft-3'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true, skipClickPropt: true }); await composePage.waitAndClick('@action-show-container-cc-bcc-buttons'); - await expectRecipientElements(composePage, { to: ['flowcryptcompatibility@gmail.com'] }); + await expectRecipientElements(composePage, { to: [{ email: 'flowcryptcompatibility@gmail.com', name: 'First Last' }] }); expect(await composePage.read('@input-body')).to.include('Test Draft Reply (Do not delete, tests is using this draft)'); })); @@ -764,7 +772,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const replyMismatchPage = await browser.newPage(t, 'chrome/elements/compose.htm?account_email=flowcrypt.compatibility%40gmail.com&parent_tab_id=0&debug=___cu_true___&frameId=none&' + params); await replyMismatchPage.waitForSelTestState('ready'); await Util.sleep(3); - await expectRecipientElements(replyMismatchPage, { to: ['censored@email.com'], cc: [], bcc: [] }); + await expectRecipientElements(replyMismatchPage, { to: [{ email: 'censored@email.com' }], cc: [], bcc: [] }); expect(await replyMismatchPage.read('@input-body')).to.include('I was not able to read your encrypted message because it was encrypted for a wrong key.'); await replyMismatchPage.waitAll('.qq-upload-file'); await ComposePageRecipe.sendAndClose(replyMismatchPage); @@ -855,14 +863,16 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await composePage.press('Enter'); await composePage.waitAndType(`@input-to`, 'human3@flowcrypt.com'); await composePage.press('Enter'); - await expectRecipientElements(composePage, { to: ['human1@flowcrypt.com', 'human2@flowcrypt.com', 'human3@flowcrypt.com'] }); + await expectRecipientElements(composePage, { + to: [{ email: 'human1@flowcrypt.com' }, { email: 'human2@flowcrypt.com' }, { email: 'human3@flowcrypt.com' }] + }); // delete recipient with Backspace when #input_to is focued await composePage.press('Backspace'); - await expectRecipientElements(composePage, { to: ['human1@flowcrypt.com', 'human2@flowcrypt.com'] }); + await expectRecipientElements(composePage, { to: [{ email: 'human1@flowcrypt.com' }, { email: 'human2@flowcrypt.com' }] }); // delete recipient with Delete when it's focused await composePage.waitAndFocus('@recipient_0'); await composePage.press('Delete'); - await expectRecipientElements(composePage, { to: ['human2@flowcrypt.com'] }); + await expectRecipientElements(composePage, { to: [{ email: 'human2@flowcrypt.com' }] }); // delete recipient with Backspace when it's focused await composePage.waitAndFocus('@recipient_1'); await composePage.press('Backspace'); @@ -1511,35 +1521,44 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const appendUrl = 'threadId=17d02296bccd4c5c&skipClickPrompt=___cu_false___&ignoreDraft=___cu_false___&replyMsgId=17d02296bccd4c5c'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true }); await composePage.waitAndClick('@encrypted-reply', { delay: 1 }); - await expectRecipientElements(composePage, { to: ['flowcrypt.compatibility@gmail.com', 'vladimir@flowcrypt.com'], cc: [], bcc: [] }); + await expectRecipientElements(composePage, { + to: [{ email: 'flowcrypt.compatibility@gmail.com', name: 'First Last' }, { email: 'vladimir@flowcrypt.com' }], + cc: [], bcc: [] + }); })); ava.default('compose - reply - subject starts with Re:', testWithBrowser('compatibility', async (t, browser) => { const appendUrl = 'threadId=17d02296bccd4c5d&skipClickPrompt=___cu_false___&ignoreDraft=___cu_false___&replyMsgId=17d02296bccd4c5d'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true }); await composePage.waitAndClick('@encrypted-reply', { delay: 1 }); - await expectRecipientElements(composePage, { to: ['vladimir@flowcrypt.com'], cc: [], bcc: [] }); + await expectRecipientElements(composePage, { to: [{ email: 'vladimir@flowcrypt.com' }], cc: [], bcc: [] }); })); ava.default('compose - reply - from !== acctEmail', testWithBrowser('compatibility', async (t, browser) => { const appendUrl = 'threadId=17d02268f01c7e40&skipClickPrompt=___cu_false___&ignoreDraft=___cu_false___&replyMsgId=17d02268f01c7e40'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true }); await composePage.waitAndClick('@encrypted-reply', { delay: 1 }); - await expectRecipientElements(composePage, { to: ['limon.monte@gmail.com'], cc: [], bcc: [] }); + await expectRecipientElements(composePage, { to: [{ email: 'limon.monte@gmail.com' }], cc: [], bcc: [] }); })); ava.default('compose - reply all - from === acctEmail', testWithBrowser('compatibility', async (t, browser) => { const appendUrl = 'threadId=17d02296bccd4c5c&skipClickPrompt=___cu_false___&ignoreDraft=___cu_false___&replyMsgId=17d02296bccd4c5c'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true }); await composePage.waitAndClick('@action-accept-reply-all-prompt', { delay: 1 }); - await expectRecipientElements(composePage, { to: ['flowcrypt.compatibility@gmail.com', 'vladimir@flowcrypt.com'], cc: ['limon.monte@gmail.com'], bcc: ['sweetalert2@gmail.com'] }); + await expectRecipientElements(composePage, { + to: [{ email: 'flowcrypt.compatibility@gmail.com' }, { email: 'vladimir@flowcrypt.com' }], + cc: [{ email: 'limon.monte@gmail.com' }], bcc: [{ email: 'sweetalert2@gmail.com' }] + }); })); ava.default('compose - reply all - from !== acctEmail', testWithBrowser('compatibility', async (t, browser) => { const appendUrl = 'threadId=17d02268f01c7e40&skipClickPrompt=___cu_false___&ignoreDraft=___cu_false___&replyMsgId=17d02268f01c7e40'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true }); await composePage.waitAndClick('@action-accept-reply-all-prompt', { delay: 1 }); - await expectRecipientElements(composePage, { to: ['limon.monte@gmail.com', 'vladimir@flowcrypt.com'], cc: ['limon.monte@gmail.com'], bcc: [] }); + await expectRecipientElements(composePage, { + to: [{ email: 'limon.monte@gmail.com' }, { email: 'vladimir@flowcrypt.com' }], + cc: [{ email: 'limon.monte@gmail.com' }], bcc: [] + }); })); /** @@ -1657,16 +1676,19 @@ const clickTripleDotAndExpectQuoteToLoad = async (composePage: Controllable, tex expect(await composePage.read('@input-body')).to.include(textToInclude); }; -export const expectRecipientElements = async (controllable: ControllablePage, expected: { to?: string[], cc?: string[], bcc?: string[] }) => { +export const expectRecipientElements = async (controllable: ControllablePage, expected: { to?: EmailParts[], cc?: EmailParts[], bcc?: EmailParts[] }) => { for (const type of ['to', 'cc', 'bcc']) { - const expectedEmails: string[] | undefined = (expected as Dict)[type] || undefined; // tslint:disable-line:no-unsafe-any + const expectedEmails: EmailParts[] | undefined = (expected as Dict)[type] || undefined; // tslint:disable-line:no-unsafe-any if (expectedEmails) { const container = await controllable.waitAny(`@container-${type}`, { visible: false }); const recipientElements = await container.$$('.recipients > span'); expect(recipientElements.length).to.equal(expectedEmails.length); for (const recipientElement of recipientElements) { - const textContent = await PageRecipe.getElementPropertyJson(recipientElement, 'textContent'); - expect(expectedEmails).to.include(textContent.trim()); + const emailElement = await recipientElement.$('.recipient-email'); + const nameElement = await recipientElement.$('.recipient-name'); + const email = emailElement ? await PageRecipe.getElementPropertyJson(emailElement, 'textContent') : undefined; + const name = nameElement ? await PageRecipe.getElementPropertyJson(nameElement, 'textContent') : undefined; + expect(expectedEmails).to.deep.include(name ? { email, name } : { email }); } } } From a953c30710ce7a35f6a2760d2d996246d6891503 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 20 Feb 2022 14:11:58 +0000 Subject: [PATCH 08/11] fixed test --- test/source/tests/compose.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index d709ae39a36..c7dd71a9ec9 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1546,7 +1546,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility', { appendUrl, hasReplyPrompt: true }); await composePage.waitAndClick('@action-accept-reply-all-prompt', { delay: 1 }); await expectRecipientElements(composePage, { - to: [{ email: 'flowcrypt.compatibility@gmail.com' }, { email: 'vladimir@flowcrypt.com' }], + to: [{ email: 'flowcrypt.compatibility@gmail.com', name: 'First Last' }, { email: 'vladimir@flowcrypt.com' }], cc: [{ email: 'limon.monte@gmail.com' }], bcc: [{ email: 'sweetalert2@gmail.com' }] }); })); From 33b617999bc915a68752d6e85df912bd2d969619 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 20 Feb 2022 14:13:02 +0000 Subject: [PATCH 09/11] display name in email preview if available --- .../elements/compose-modules/compose-recipients-module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index 6c65b596f5f..c12baa552a3 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -286,7 +286,8 @@ export class ComposeRecipientsModule extends ViewModule { while (container.width()! <= maxWidth && orderedRecipients.length >= processed + 1) { const recipient = orderedRecipients[processed]; const escapedTitle = Xss.escape(recipient.element.getAttribute('title') || ''); - const emailHtml = ``; + const nameOrEmail = recipient.name || recipient.email || recipient.invalid || ''; + const emailHtml = ``; $(emailHtml).insertBefore(rest); // xss-escaped processed++; } From 3fbb73fce88533a44b62edf2c67cbf7bee778868 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 21 Feb 2022 15:43:45 +0000 Subject: [PATCH 10/11] less bold name --- extension/css/cryptup.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/css/cryptup.css b/extension/css/cryptup.css index a31b0fe3a35..dc167963c34 100644 --- a/extension/css/cryptup.css +++ b/extension/css/cryptup.css @@ -1565,7 +1565,7 @@ table#compose td.recipients-inputs > div#input_addresses_container div .recipien table#compose td.recipients-inputs > div#input_addresses_container div .recipients > span.failed .close-icon { opacity: 1; } .recipient-name { - font-weight: bold; + font-weight: 500; margin: 2px 4px 0 0; } From afcd69a8e69965756bf33107168c1ae05de4e23d Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 21 Feb 2022 16:08:15 +0000 Subject: [PATCH 11/11] refactorings --- .../compose-modules/compose-send-btn-module.ts | 10 +++++----- .../chrome/elements/compose-modules/compose-types.ts | 6 +++++- .../formatters/encrypted-mail-msg-formatter.ts | 4 ++-- .../formatters/general-mail-formatter.ts | 5 ++--- .../formatters/signed-msg-mail-formatter.ts | 5 ++--- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts index 639afe20af7..de8c45eb420 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts @@ -14,7 +14,7 @@ import { ComposeSendBtnPopoverModule } from './compose-send-btn-popover-module.j import { GeneralMailFormatter } from './formatters/general-mail-formatter.js'; import { GmailRes } from '../../../js/common/api/email-provider/gmail/gmail-parser.js'; import { KeyInfo } from '../../../js/common/core/crypto/key.js'; -import { SendBtnTexts } from './compose-types.js'; +import { getUniqueRecipientEmails, SendBtnTexts } from './compose-types.js'; import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { Xss } from '../../../js/common/platform/xss.js'; @@ -22,7 +22,7 @@ import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; -import { Str, Value } from '../../../js/common/core/common.js'; +import { Str } from '../../../js/common/core/common.js'; export class ComposeSendBtnModule extends ViewModule { @@ -112,7 +112,7 @@ export class ComposeSendBtnModule extends ViewModule { this.view.S.cached('send_btn_note').text(''); const newMsgData = this.view.inputModule.extractAll(); await this.view.errModule.throwIfFormValsInvalid(newMsgData); - const emails = Value.arr.unique(Object.values(newMsgData.recipients).reduce((a, b) => a.concat(b), []).filter(x => x.email).map(x => x.email)); + const emails = getUniqueRecipientEmails(newMsgData.recipients); await ContactStore.update(undefined, emails, { lastUse: Date.now() }); const msgObj = await GeneralMailFormatter.processNewMsg(this.view, newMsgData); if (msgObj) { @@ -147,7 +147,7 @@ export class ComposeSendBtnModule extends ViewModule { if (this.view.myPubkeyModule.shouldAttach() && senderKi) { // todo: report on undefined? msg.attachments.push(Attachment.keyinfoAsPubkeyAttachment(senderKi)); } - msg.from = await this.addNameToEmail(msg.from); + msg.from = await this.formatSenderEmailAsMimeString(msg.from); }; private extractInlineImagesToAttachments = (html: string) => { @@ -225,7 +225,7 @@ export class ComposeSendBtnModule extends ViewModule { } }; - private addNameToEmail = async (email: string): Promise => { + private formatSenderEmailAsMimeString = async (email: string): Promise => { const parsedEmail = Str.parseEmail(email); if (!parsedEmail.email) { throw new Error(`Recipient email ${email} is not valid`); diff --git a/extension/chrome/elements/compose-modules/compose-types.ts b/extension/chrome/elements/compose-modules/compose-types.ts index 3364bb9cdb7..92808d00a4e 100644 --- a/extension/chrome/elements/compose-modules/compose-types.ts +++ b/extension/chrome/elements/compose-modules/compose-types.ts @@ -5,7 +5,7 @@ import { RecipientType } from '../../../js/common/api/shared/api.js'; import { ParsedRecipients } from '../../../js/common/api/email-provider/email-provider-api.js'; import { KeyInfo, PubkeyResult } from '../../../js/common/core/crypto/key.js'; -import { EmailParts } from '../../../js/common/core/common.js'; +import { EmailParts, Value } from '../../../js/common/core/common.js'; export enum RecipientStatus { EVALUATING, @@ -61,3 +61,7 @@ export class SendBtnTexts { public static readonly BTN_WRONG_ENTRY: string = "Re-enter recipient.."; public static readonly BTN_SENDING: string = "Sending.."; } + +export const getUniqueRecipientEmails = (recipients: ParsedRecipients) => { + return Value.arr.unique(Object.values(recipients).reduce((a, b) => a.concat(b), []).filter(x => x.email).map(x => x.email)); +}; diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index 112ab935cd3..3747fc55771 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -5,7 +5,7 @@ import { BaseMailFormatter } from './base-mail-formatter.js'; import { ComposerResetBtnTrigger } from '../compose-err-module.js'; import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js'; -import { NewMsgData } from '../compose-types.js'; +import { getUniqueRecipientEmails, NewMsgData } from '../compose-types.js'; import { Str, Url, Value } from '../../../../js/common/core/common.js'; import { ApiErr } from '../../../../js/common/api/shared/api-error.js'; import { Attachment } from '../../../../js/common/core/attachment.js'; @@ -157,7 +157,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async ( authInfo: FcUuidAuth, newMsgData: NewMsgData ): Promise<{ bodyWithReplyToken: SendableMsgBody, replyToken: string }> => { - const recipients = Value.arr.unique(Object.values(newMsgData.recipients).reduce((a, b) => a.concat(b), []).map(x => x.email)); + const recipients = getUniqueRecipientEmails(newMsgData.recipients); try { const response = await this.view.acctServer.messageToken(authInfo); const infoDiv = Ui.e('div', { diff --git a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts index fdc9caa5c4a..14fb1eeee2e 100644 --- a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts @@ -4,19 +4,18 @@ import { EncryptedMsgMailFormatter } from './encrypted-mail-msg-formatter.js'; import { Key, KeyInfo } from "../../../../js/common/core/crypto/key.js"; -import { NewMsgData } from "../compose-types.js"; +import { getUniqueRecipientEmails, NewMsgData } from "../compose-types.js"; import { PlainMsgMailFormatter } from './plain-mail-msg-formatter.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; import { SignedMsgMailFormatter } from './signed-msg-mail-formatter.js'; import { ComposeView } from '../../compose.js'; -import { Value } from '../../../../js/common/core/common.js'; export class GeneralMailFormatter { // returns undefined in case user cancelled decryption of the signing key public static processNewMsg = async (view: ComposeView, newMsgData: NewMsgData): Promise<{ msg: SendableMsg, senderKi: KeyInfo | undefined } | undefined> => { const choices = view.sendBtnModule.popover.choices; - const recipientsEmails = Value.arr.unique(Object.values(newMsgData.recipients).reduce((a, b) => a.concat(b), []).map(x => x.email)); + const recipientsEmails = getUniqueRecipientEmails(newMsgData.recipients); if (!choices.encrypt && !choices.sign) { // plain return { senderKi: undefined, msg: await new PlainMsgMailFormatter(view).sendableMsg(newMsgData) }; } diff --git a/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts b/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts index ad1033b663b..d6acee95626 100644 --- a/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts @@ -5,13 +5,12 @@ import { BaseMailFormatter } from './base-mail-formatter.js'; import { BrowserWindow } from '../../../../js/common/browser/browser-window.js'; import { Catch } from '../../../../js/common/platform/catch.js'; -import { NewMsgData } from '../compose-types.js'; +import { getUniqueRecipientEmails, NewMsgData } from '../compose-types.js'; import { Key } from '../../../../js/common/core/crypto/key.js'; import { MsgUtil } from '../../../../js/common/core/crypto/pgp/msg-util.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js'; import { ContactStore } from '../../../../js/common/platform/store/contact-store.js'; -import { Value } from '../../../../js/common/core/common.js'; export class SignedMsgMailFormatter extends BaseMailFormatter { @@ -41,7 +40,7 @@ export class SignedMsgMailFormatter extends BaseMailFormatter { // Removing them here will prevent Gmail from screwing up the signature newMsg.plaintext = newMsg.plaintext.split('\n').map(l => l.replace(/\s+$/g, '')).join('\n').trim(); const signedData = await MsgUtil.sign(signingPrv, newMsg.plaintext); - const recipients = Value.arr.unique(Object.values(newMsg.recipients).reduce((a, b) => a.concat(b), []).map(x => x.email)); + const recipients = getUniqueRecipientEmails(newMsg.recipients); ContactStore.update(undefined, recipients, { lastUse: Date.now() }).catch(Catch.reportErr); return await SendableMsg.createInlineArmored(this.acctEmail, this.headers(newMsg), signedData, attachments); }