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 @@ -12,7 +12,7 @@ import { Catch } from '../../../js/common/platform/catch.js';
import { ComposerUserError } from './compose-err-module.js';
import { ComposeSendBtnPopoverModule } from './compose-send-btn-popover-module.js';
import { GeneralMailFormatter } from './formatters/general-mail-formatter.js';
import { GmailRes } from '../../../js/common/api/email-provider/gmail/gmail-parser.js';
import { GmailParser, GmailRes } from '../../../js/common/api/email-provider/gmail/gmail-parser.js';
import { KeyInfo } from '../../../js/common/core/crypto/key.js';
import { getUniqueRecipientEmails, SendBtnTexts } from './compose-types.js';
import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js';
Expand Down Expand Up @@ -216,7 +216,19 @@ export class ComposeSendBtnModule extends ViewModule<ComposeView> {
}
BrowserMsg.send.notificationShow(this.view.parentTabId, { notification: `Your ${this.view.isReplyBox ? 'reply' : 'message'} has been sent.` });
BrowserMsg.send.focusBody(this.view.parentTabId); // Bring focus back to body so Gmails shortcuts will work
await this.view.draftModule.draftDelete();
const operations = [this.view.draftModule.draftDelete()];
if (msg.externalId) {
operations.push((async (externalId, id) => {
const gmailMsg = await this.view.emailProvider.msgGet(id, 'metadata');
const messageId = GmailParser.findHeader(gmailMsg, 'message-id');
if (messageId) {
await this.view.acctServer.messageGatewayUpdate(externalId, messageId);
} else {
Catch.report('Failed to extract Message-ID of sent message');
}
})(msg.externalId, msgSentRes.id));
}
await Promise.all(operations);
this.isSendMessageInProgress = false;
if (this.view.isReplyBox) {
this.view.renderModule.renderReplySuccess(msg, msgSentRes.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
// - flowcrypt.com/api (consumers and customers without on-prem setup), or
// - FlowCrypt Enterprise Server (enterprise customers with on-prem setup)
// It will be served to recipient through web
const msgUrl = await this.prepareAndUploadPwdEncryptedMsg(newMsg); // encrypted for pwd only, pubkeys ignored
const { url: msgUrl, externalId } = await this.prepareAndUploadPwdEncryptedMsg(newMsg); // encrypted for pwd only, pubkeys ignored
newMsg.pwd = undefined;
return await this.sendablePwdMsg(newMsg, pubkeys, msgUrl, signingPrv); // encrypted for pubkeys only, pwd ignored
return await this.sendablePwdMsg(newMsg, pubkeys, { msgUrl, externalId }, signingPrv); // encrypted for pubkeys only, pwd ignored
} else if (this.richtext) { // rich text: PGP/MIME - https://tools.ietf.org/html/rfc3156#section-4
// or S/MIME
return await this.sendableRichTextMsg(newMsg, pubkeys, signingPrv);
Expand All @@ -43,7 +43,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
}
};

private prepareAndUploadPwdEncryptedMsg = async (newMsg: NewMsgData): Promise<string> => {
private prepareAndUploadPwdEncryptedMsg = async (newMsg: NewMsgData): Promise<{ url: string; externalId?: string }> => {
// PGP/MIME + included attachments (encrypted for password only)
if (!newMsg.pwd) {
throw new Error('password unexpectedly missing');
Expand Down Expand Up @@ -77,26 +77,26 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
const { bodyWithReplyToken, replyToken } = await this.getPwdMsgSendableBodyWithOnlineReplyMsgToken(authInfo, newMsg);
const pgpMimeWithAttachments = await Mime.encode(bodyWithReplyToken, { Subject: newMsg.subject }, await this.view.attachmentsModule.attachment.collectAttachments());
const { data: pwdEncryptedWithAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAttachments), newMsg.pwd, []); // encrypted only for pwd, not signed
const { url } = await this.view.acctServer.messageUpload(
const { url, externalId } = await this.view.acctServer.messageUpload(
authInfo.uuid ? authInfo : undefined,
pwdEncryptedWithAttachments,
replyToken,
newMsg.from,
newMsg.recipients,
(p) => this.view.sendBtnModule.renderUploadProgress(p, 'FIRST-HALF'), // still need to upload to Gmail later, this request represents first half of progress
);
return url;
return { url, externalId };
};

private sendablePwdMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], msgUrl: string, signingPrv?: Key) => {
private sendablePwdMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], { msgUrl, externalId }: { msgUrl: string, externalId?: string }, signingPrv?: Key) => {
// encoded as: PGP/MIME-like structure but with attachments as external files due to email size limit (encrypted for pubkeys only)
const msgBody = this.richtext ? { 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml } : { 'text/plain': newMsg.plaintext };
const pgpMimeNoAttachments = await Mime.encode(msgBody, { Subject: newMsg.subject }, []); // no attachments, attached to email separately
const { data: pubEncryptedNoAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAttachments), undefined, pubs, signingPrv); // encrypted only for pubs
const attachments = this.createPgpMimeAttachments(pubEncryptedNoAttachments).
concat(await this.view.attachmentsModule.attachment.collectEncryptAttachments(pubs)); // encrypted only for pubs
const emailIntroAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(msgUrl);
return await SendableMsg.createPwdMsg(this.acctEmail, this.headers(newMsg), emailIntroAndLinkBody, attachments, { isDraft: this.isDraft });
return await SendableMsg.createPwdMsg(this.acctEmail, this.headers(newMsg), emailIntroAndLinkBody, attachments, { isDraft: this.isDraft, externalId });
};

private sendableSimpleTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: Key): Promise<SendableMsg> => {
Expand Down
9 changes: 8 additions & 1 deletion extension/js/common/api/account-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class AccountServer extends Api {
from: string,
recipients: ParsedRecipients,
progressCb: ProgressCb
): Promise<{ url: string }> => {
): Promise<{ url: string, externalId?: string }> => {
if (await this.isFesUsed()) {
const fes = new EnterpriseServer(this.acctEmail);
// Recipients are used to later cross-check replies from the web
Expand All @@ -70,6 +70,13 @@ export class AccountServer extends Api {
}
};

public messageGatewayUpdate = async (externalId: string, emailGatewayMessageId: string) => {
if (await this.isFesUsed()) {
const fes = new EnterpriseServer(this.acctEmail);
await fes.messageGatewayUpdate(externalId, emailGatewayMessageId);
}
};

public messageToken = async (fcAuth: FcUuidAuth): Promise<{ replyToken: string }> => {
if (await this.isFesUsed()) {
const fes = new EnterpriseServer(this.acctEmail);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type EventTag = 'compose' | 'decrypt' | 'setup' | 'settings' | 'import-pub' | 'i

export namespace FesRes {
export type ReplyToken = { replyToken: string };
export type MessageUpload = { url: string };
export type MessageUpload = { url: string; externalId: string };
export type ServiceInfo = { vendor: string, service: string, orgId: string, version: string, apiVersion: string };
export type ClientConfiguration = { clientConfiguration: DomainRulesJson };
}
Expand Down Expand Up @@ -135,6 +135,10 @@ export class EnterpriseServer extends Api {
);
};

public messageGatewayUpdate = async (externalId: string, emailGatewayMessageId: string) => {
await this.request<void>('POST', `/api/${this.apiVersion}/message/${externalId}/gateway`, await this.authHdr(), { emailGatewayMessageId });
};

public accountUpdate = async (profileUpdate: ProfileUpdate): Promise<BackendRes.FcAccountUpdate> => {
console.log('profile update ignored', profileUpdate);
throw new UnreportableError('Account update not implemented when using FlowCrypt Enterprise Server');
Expand Down
13 changes: 8 additions & 5 deletions extension/js/common/api/email-provider/sendable-msg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type SendableMsgHeaders = {
type SendableMsgOptions = {
type?: MimeEncodeType,
isDraft?: boolean;
externalId?: string; // id of pwd-protected message on FES
};

type SignMethod = (signable: string) => Promise<string>;
Expand Down Expand Up @@ -61,7 +62,7 @@ export class SendableMsg {
attachments: Attachment[],
options: SendableMsgOptions
): Promise<SendableMsg> => {
return await SendableMsg.createSendableMsg(acctEmail, headers, body, attachments, { type: undefined, isDraft: options.isDraft });
return await SendableMsg.createSendableMsg(acctEmail, headers, body, attachments, { type: undefined, isDraft: options.isDraft, externalId: options.externalId });
};

public static createPgpMime = async (acctEmail: string, headers: SendableMsgHeaders, attachments: Attachment[], options?: SendableMsgOptions): Promise<SendableMsg> => {
Expand All @@ -88,11 +89,11 @@ export class SendableMsg {
options: SendableMsgOptions
): Promise<SendableMsg> => {
const { from, recipients, subject, thread } = headers;
const { type, isDraft } = options;
return await SendableMsg.create(acctEmail, { from, recipients, subject, thread, body, attachments, type, isDraft });
const { type, isDraft, externalId } = options;
return await SendableMsg.create(acctEmail, { from, recipients, subject, thread, body, attachments, type, isDraft, externalId });
};

private static create = async (acctEmail: string, { from, recipients, subject, thread, body, attachments, type, isDraft }: SendableMsgDefinition): Promise<SendableMsg> => {
private static create = async (acctEmail: string, { from, recipients, subject, thread, body, attachments, type, isDraft, externalId }: SendableMsgDefinition): Promise<SendableMsg> => {
const primaryKi = await KeyStore.getFirstRequired(acctEmail);
const headers: Dict<string> = {};
if (primaryKi && KeyUtil.getKeyType(primaryKi.private) === 'openpgp') {
Expand All @@ -108,7 +109,8 @@ export class SendableMsg {
body || {},
attachments || [],
thread,
type
type,
externalId
);
};

Expand All @@ -123,6 +125,7 @@ export class SendableMsg {
public attachments: Attachment[],
public thread: string | undefined,
public type: MimeEncodeType,
public externalId?: string, // for binding a password-protected message
) {
const allEmails = [...recipients.to || [], ...recipients.cc || [], ...recipients.bcc || []];
if (!allEmails.length && !isDraft) {
Expand Down
11 changes: 10 additions & 1 deletion test/source/mock/fes/fes-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,19 @@ export const mockFesEndpoints: HandlersDefinition = {
expect(body).to.contain('"cc":[]');
expect(body).to.contain('"bcc":["Mr Bcc <bcc@example.com>"]');
expect(body).to.contain('"from":"user@standardsubdomainfes.test:8001"');
return { 'url': `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID` };
return { 'url': `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID`, 'externalId': 'FES-MOCK-EXTERNAL-ID' };
}
throw new HttpClientErr('Not Found', 404);
},
'/api/v1/message/FES-MOCK-EXTERNAL-ID/gateway': async ({ body }, req) => {
if (req.headers.host === standardFesUrl && req.method === 'POST') {
// test: `compose - user@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal`
authenticate(req, 'oidc');
expect(body).to.match(/{"emailGatewayMessageId":"<(.+)@standardsubdomainfes.test:8001>"}/);
return {};
}
throw new HttpClientErr('Not Found', 404);
}
};

const authenticate = (req: IncomingMessage, type: 'oidc' | 'fes'): string => {
Expand Down
11 changes: 7 additions & 4 deletions test/source/mock/google/google-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class GoogleData {
const msgCopy = JSON.parse(JSON.stringify(m)) as GmailMsg;
if (format === 'raw') {
if (!msgCopy.raw) {
throw new Error(`MOCK: format=raw missing data for message id ${m.id}. Solution: add them to ./test/source/mock/data/acct.json`);
throw new Error(`MOCK: format=raw missing data for message id ${m.id}. Solution: add them to ./test/source/mock/data/google/exported-messages`);
}
} else {
msgCopy.raw = undefined;
Expand Down Expand Up @@ -177,7 +177,7 @@ export class GoogleData {
}
}

public storeSentMessage = (parsedMail: ParsedMail, base64Msg: string): string => {
public storeSentMessage = (parsedMail: ParsedMail, base64Msg: string, id: string): string => {
let bodyContentAtt: { data: string; size: number; filename?: string; id: string } | undefined;
for (const attachment of parsedMail.attachments || []) {
const attId = Util.lousyRandom();
Expand All @@ -197,12 +197,15 @@ export class GoogleData {
throw new Error('MOCK storeSentMessage: no parsedMail body, no appropriate bodyContentAtt');
}
const barebonesGmailMsg: GmailMsg = { // todo - could be improved - very barebones
id: `msg_id_${Util.lousyRandom()}`,
id,
threadId: null, // tslint:disable-line:no-null-keyword
historyId: '',
labelIds: ['SENT' as GmailMsg$labelId],
payload: {
headers: [{ name: 'Subject', value: parsedMail.subject || '' }],
headers: [
{ name: 'Subject', value: parsedMail.subject || '' },
{ name: 'Message-ID', value: parsedMail.messageId || '' }
],
body
},
raw: base64Msg
Expand Down
6 changes: 4 additions & 2 deletions test/source/mock/google/google-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AddressObject, ParsedMail } from 'mailparser';
import { TestBySubjectStrategyContext } from './strategies/send-message-strategy';
import { UnsuportableStrategyError } from './strategies/strategy-base';
import { oauth } from '../lib/oauth';
import { Util } from '../../util';

type DraftSaveModel = { message: { raw: string, threadId: string } };

Expand Down Expand Up @@ -217,16 +218,17 @@ export const mockGoogleEndpoints: HandlersDefinition = {
if (parsedReq.body && typeof parsedReq.body === 'string') {
const parseResult = await parseMultipartDataAsMimeMsg(parsedReq.body);
await validateMimeMsg(acct, parseResult.mimeMsg, parseResult.threadId);
const id = `msg_id_${Util.lousyRandom()}`;
try {
const testingStrategyContext = new TestBySubjectStrategyContext(parseResult.mimeMsg.subject || '');
await testingStrategyContext.test(parseResult.mimeMsg, parseResult.base64);
await testingStrategyContext.test(parseResult.mimeMsg, parseResult.base64, id);
} catch (e) {
if (!(e instanceof UnsuportableStrategyError)) { // No such strategy for test
throw e; // todo - should start throwing unsupported test strategies too, else changing subject will cause incomplete testing
// todo - should stop calling it "strategy", better just "SentMessageTest" or similar
}
}
return { id: 'fakesendid', labelIds: ['SENT'], threadId: parseResult.threadId };
return { id, labelIds: ['SENT'], threadId: parseResult.threadId };
}
}
throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`);
Expand Down
11 changes: 6 additions & 5 deletions test/source/mock/google/strategies/send-message-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { testConstants } from '../../../tests/tooling/consts.js';

// TODO: Make a better structure of ITestMsgStrategy. Because this class doesn't test anything, it only saves message in the Mock
class SaveMessageInStorageStrategy implements ITestMsgStrategy {
public test = async (mimeMsg: ParsedMail, base64Msg: string) => {
(await GoogleData.withInitializedData(mimeMsg.from!.value[0].address!)).storeSentMessage(mimeMsg, base64Msg);
public test = async (mimeMsg: ParsedMail, base64Msg: string, id: string) => {
(await GoogleData.withInitializedData(mimeMsg.from!.value[0].address!)).storeSentMessage(mimeMsg, base64Msg, id);
};
}

Expand All @@ -39,7 +39,7 @@ class PwdEncryptedMessageWithFlowCryptComApiTestStrategy implements ITestMsgStra
}

class PwdEncryptedMessageWithFesIdTokenTestStrategy implements ITestMsgStrategy {
public test = async (mimeMsg: ParsedMail) => {
public test = async (mimeMsg: ParsedMail, base64Msg: string, id: string) => {
const expectedSenderEmail = 'user@standardsubdomainfes.test:8001';
expect(mimeMsg.from!.text).to.equal(`First Last <${expectedSenderEmail}>`);
expect((mimeMsg.to as AddressObject).text).to.equal('Mr To <to@example.com>');
Expand All @@ -53,6 +53,7 @@ class PwdEncryptedMessageWithFesIdTokenTestStrategy implements ITestMsgStrategy
if (!mimeMsg.text?.includes('Follow this link to open it')) {
throw new HttpClientErr(`Error: cannot find pwd encrypted open link prompt in ${mimeMsg.text}`);
}
await new SaveMessageInStorageStrategy().test(mimeMsg, base64Msg, id);
};
}

Expand Down Expand Up @@ -239,7 +240,7 @@ export class TestBySubjectStrategyContext {
}
}

public test = async (mimeMsg: ParsedMail, base64Msg: string) => {
await this.strategy.test(mimeMsg, base64Msg);
public test = async (mimeMsg: ParsedMail, base64Msg: string, id: string) => {
await this.strategy.test(mimeMsg, base64Msg, id);
};
}
2 changes: 1 addition & 1 deletion test/source/mock/google/strategies/strategy-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { ParsedMail } from 'mailparser';

export interface ITestMsgStrategy {
test(mimeMsg: ParsedMail, base64Msg: string): Promise<void>;
test(mimeMsg: ParsedMail, base64Msg: string, id: string): Promise<void>;
}

export class UnsuportableStrategyError extends Error { }