Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class ComposeDraftModule extends ViewModule<ComposeView> {
const msgData = await this.view.inputModule.extractAll();
const { pubkeys } = await this.view.storageModule.collectSingleFamilyKeys([], msgData.from.email, true);
msgData.pwd = undefined; // not needed for drafts
const sendable = await new EncryptedMsgMailFormatter(this.view, true).sendableNonPwdMsg(msgData, pubkeys);
const sendable = await new EncryptedMsgMailFormatter(this.view, true).encryptSendableNonPwdMsg(msgData, pubkeys);
if (this.view.replyParams?.inReplyTo) {
sendable.headers.References = this.view.replyParams.inReplyTo;
sendable.headers['In-Reply-To'] = this.view.replyParams.inReplyTo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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 { getUniqueRecipientEmails, NewMsgData } from '../compose-types.js';
import { EmailParts, Str, Value } from '../../../../js/common/core/common.js';
import { Str, 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';
import { Buf } from '../../../../js/common/core/buf.js';
Expand All @@ -21,10 +21,9 @@ import { Xss } from '../../../../js/common/platform/xss.js';
import { AcctStore } from '../../../../js/common/platform/store/acct-store.js';
import { SmimeKey } from '../../../../js/common/core/crypto/smime/smime-key.js';
import { PgpHash } from '../../../../js/common/core/crypto/pgp/pgp-hash.js';
import { UploadedMessageData } from '../../../../js/common/api/account-server.js';
import { UploadedMessageResponse } from '../../../../js/common/api/account-server.js';
import { ParsedKeyInfo } from '../../../../js/common/core/crypto/key-store-util.js';
import { MultipleMessages } from './general-mail-formatter.js';
import { Api, RecipientType } from '../../../../js/common/api/shared/api.js';

/**
* this type must be kept in sync with FES UI code, changes must be backwards compatible
Expand All @@ -44,9 +43,9 @@ type ReplyInfoRaw = {
export class EncryptedMsgMailFormatter extends BaseMailFormatter {
public sendableMsgs = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingKey?: ParsedKeyInfo): Promise<MultipleMessages> => {
if (newMsg.pwd && !this.isDraft) {
return await this.formatSendablePwdMsgs(newMsg, pubkeys, signingKey);
return await this.formatSendablePwdMsg(newMsg, pubkeys, signingKey);
} else {
const msg = await this.sendableNonPwdMsg(newMsg, pubkeys, signingKey?.key);
const msg = await this.encryptSendableNonPwdMsg(newMsg, pubkeys, signingKey?.key);
return {
senderKi: signingKey?.keyInfo,
msgs: [msg],
Expand All @@ -58,7 +57,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
}
};

public sendableNonPwdMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingPrv?: Key): Promise<SendableMsg> => {
public encryptSendableNonPwdMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingPrv?: Key): Promise<SendableMsg> => {
if (!this.isDraft) {
// S/MIME drafts are currently formatted with inline armored data
const x509certs = pubkeys.map(entry => entry.pubkey).filter(pub => pub.family === 'x509');
Expand Down Expand Up @@ -92,70 +91,42 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
});
};

private formatSendablePwdMsgs = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingKey?: ParsedKeyInfo) => {
private formatSendablePwdMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingKey?: ParsedKeyInfo) => {
// password-protected message, temporarily uploaded (already encrypted) to:
// - flowcrypt.com/api (consumers and customers without on-prem setup), or
// - FlowCrypt Enterprise Server (enterprise customers with on-prem setup)
// - FlowCrypt External Service at fes.example.com (enterprise customers with on-prem setup)
// It will be served to recipient through web
const uploadedMessageData = await this.prepareAndUploadPwdEncryptedMsg(newMsg); // encrypted for pwd only, pubkeys ignored
// pwdRecipients that have their personal link
const individualPwdRecipients = Object.keys(uploadedMessageData.emailToExternalIdAndUrl ?? {}).filter(email => !pubkeys.some(p => p.email === email));
const legacyPwdRecipients: { [type in RecipientType]?: EmailParts[] } = {};
const uploadedMessageResponse = await this.prepareEncryptAndUploadPwdEncryptedMsg(newMsg); // encrypted for pwd only, pubkeys ignored
newMsg.pwd = undefined;
const encryptedAttachments = await this.view.attachmentsModule.attachment.collectEncryptAttachments(pubkeys);
const pubkeyRecipients: { [type in RecipientType]?: EmailParts[] } = {};
for (const [sendingType, value] of Object.entries(newMsg.recipients)) {
if (Api.isRecipientHeaderNameType(sendingType)) {
pubkeyRecipients[sendingType] = value?.filter(emailPart => pubkeys.some(p => p.email === emailPart.email));
legacyPwdRecipients[sendingType] = value?.filter(
emailPart => !pubkeys.some(p => p.email === emailPart.email) && !individualPwdRecipients.includes(emailPart.email)
);
}
}
const msgs: SendableMsg[] = [];
// pubkey recipients get one combined message. If there are not pubkey recpients, only password - protected messages will be sent
if (pubkeyRecipients.to?.length || pubkeyRecipients.cc?.length || pubkeyRecipients.bcc?.length) {
const uniquePubkeyRecipientToAndCCs = Value.arr.unique(
(pubkeyRecipients.to || []).concat(pubkeyRecipients.cc || []).map(recipient => recipient.email.toLowerCase())
);
// pubkey recipients should be able to reply to "to" and "cc" pwd recipients
const replyToForMessageSentToPubkeyRecipients = (newMsg.recipients.to ?? [])
.concat(newMsg.recipients.cc ?? [])
.filter(recipient => !uniquePubkeyRecipientToAndCCs.includes(recipient.email.toLowerCase()));
const pubkeyMsgData = {
...newMsg,
recipients: pubkeyRecipients,
// brackets are required for test emails like '@test:8001'
replyTo: replyToForMessageSentToPubkeyRecipients.length
? `${Str.formatEmailList([newMsg.from, ...replyToForMessageSentToPubkeyRecipients], true)}`
: undefined,
};
msgs.push(await this.sendableNonPwdMsg(pubkeyMsgData, pubkeys, signingKey?.key));
}
// adding individual messages for each recipient that doesn't have a pubkey
for (const recipientEmail of individualPwdRecipients) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { url, externalId } = uploadedMessageData.emailToExternalIdAndUrl![recipientEmail];
const foundParsedRecipient = (newMsg.recipients.to ?? [])
.concat(newMsg.recipients.cc ?? [])
.concat(newMsg.recipients.bcc ?? [])
.find(r => r.email.toLowerCase() === recipientEmail.toLowerCase());
// todo: since a message is allowed to have only `cc` or `bcc` without `to`, should we preserve the original placement(s) of the recipient?
const individualMsgData = { ...newMsg, recipients: { to: [foundParsedRecipient ?? { email: recipientEmail }] } };
msgs.push(await this.sendablePwdMsg(individualMsgData, pubkeys, { msgUrl: url, externalId }, signingKey?.key));
}
if (legacyPwdRecipients.to?.length || legacyPwdRecipients.cc?.length || legacyPwdRecipients.bcc?.length) {
const legacyPwdMsgData = { ...newMsg, recipients: legacyPwdRecipients };
msgs.push(await this.sendablePwdMsg(legacyPwdMsgData, pubkeys, { msgUrl: uploadedMessageData.url }, signingKey?.key));
}
// We used to support sending individual messages to each of the password recipients, each with unique link
// as per https://github.com/FlowCrypt/flowcrypt-browser/issues/4348. We later reverted that behavior in
// https://github.com/FlowCrypt/flowcrypt-browser/issues/4870. Therefore currently it's a single message
// again even though the code could support multiple messages. If by 2024 there is still support for multiple
// messages that is unused, then this can be refactored back to a single message
// (`SendableMsg[]` to `SendableMsg` and so on, including error handling which is much simpler when there is
// just one message to send)
const msg = await this.sendableCombinedPubkeyMsgWithoutAttachedFilesWithLinkToUploadedPwdMsg(
newMsg,
pubkeys,
{ msgUrl: uploadedMessageResponse.url, externalId: uploadedMessageResponse.externalId },
signingKey?.key
);
// the above message has pgp/mime encrypted content that was attached as a set of pgp/mime attachments,
// but doesn't have the actual attachments the user has attached (they were uploaded to FES/backend but weren't
// attached to the message itself). We are adding the attachments here.
const pubkeyEncryptedAttachments = await this.view.attachmentsModule.attachment.collectEncryptAttachments(pubkeys);
msg.attachments = msg.attachments.concat(pubkeyEncryptedAttachments);
return {
senderKi: signingKey?.keyInfo,
msgs,
renderSentMessage: { recipients: newMsg.recipients, attachments: encryptedAttachments },
msgs: [msg],
renderSentMessage: {
recipients: newMsg.recipients,
attachments: pubkeyEncryptedAttachments
},
};
};

private prepareAndUploadPwdEncryptedMsg = async (newMsg: NewMsgData): Promise<UploadedMessageData> => {
private prepareEncryptAndUploadPwdEncryptedMsg = async (newMsg: NewMsgData): Promise<UploadedMessageResponse> => {
// PGP/MIME + included attachments (encrypted for password only)
if (!newMsg.pwd) {
throw new Error('password unexpectedly missing');
Expand Down Expand Up @@ -192,17 +163,19 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
{ Subject: newMsg.subject }, // eslint-disable-line @typescript-eslint/naming-convention
await this.view.attachmentsModule.attachment.collectAttachments()
);
const { data: pwdEncryptedWithAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAttachments), newMsg.pwd, []); // encrypted only for pwd, not signed
// encrypted only for pwd, not signed
const { data: pwdEncryptedWithAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAttachments), newMsg.pwd, []);
return await this.view.acctServer.messageUpload(
pwdEncryptedWithAttachments,
replyToken,
newMsg.from.email, // todo: Str.formatEmailWithOptionalName?
newMsg.recipients,
p => this.view.sendBtnModule.renderUploadProgress(p, 'FIRST-HALF') // still need to upload to Gmail later, this request represents first half of progress
// still need to upload to Gmail later, this request represents first half of progress
p => this.view.sendBtnModule.renderUploadProgress(p, 'FIRST-HALF')
);
};

private sendablePwdMsg = async (
private sendableCombinedPubkeyMsgWithoutAttachedFilesWithLinkToUploadedPwdMsg = async (
newMsg: NewMsgData,
pubs: PubkeyResult[],
{ msgUrl, externalId }: { msgUrl: string; externalId?: string },
Expand Down
7 changes: 3 additions & 4 deletions extension/js/common/api/account-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import { ParsedRecipients } from './email-provider/email-provider-api.js';
import { BackendAuthErr } from './shared/api-error.js';
import { Api, ProgressCb } from './shared/api.js';

export type UploadedMessageData = {
export type UploadedMessageResponse = {
url: string; // both FES and FlowCryptComApi
externalId?: string; // legacy FES
emailToExternalIdAndUrl?: { [email: string]: { url: string; externalId: string } }; // FES only
externalId?: string; // FES
};

/**
Expand Down Expand Up @@ -59,7 +58,7 @@ export class AccountServer extends Api {
from: string,
recipients: ParsedRecipients,
progressCb: ProgressCb
): Promise<UploadedMessageData> => {
): Promise<UploadedMessageResponse> => {
if (await this.isFesUsed()) {
const fes = new EnterpriseServer(this.acctEmail);
// Recipients are used to later cross-check replies from the web
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ type EventTag = 'compose' | 'decrypt' | 'setup' | 'settings' | 'import-pub' | 'i
export namespace FesRes {
export type ReplyToken = { replyToken: string };
export type MessageUpload = {
url: string; // LEGACY
externalId: string; // LEGACY
emailToExternalIdAndUrl?: { [email: string]: { url: string; externalId: string } };
url: string;
externalId: string;
};
export type ServiceInfo = { vendor: string; service: string; orgId: string; version: string; apiVersion: string };
export type ClientConfiguration = { clientConfiguration: ClientConfigurationJson };
Expand Down
Loading