diff --git a/README.md b/README.md index fe36e774af0..0daca0861cb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # FlowCrypt: Encrypt Gmail with PGP - ## Users Get [FlowCrypt](https://flowcrypt.com/) browser extension at: https://flowcrypt.com/download diff --git a/extension/chrome/elements/composer/composer-atts.ts b/extension/chrome/elements/composer/composer-atts.ts index 2e7d59854a0..9d8762fca64 100644 --- a/extension/chrome/elements/composer/composer-atts.ts +++ b/extension/chrome/elements/composer/composer-atts.ts @@ -35,7 +35,7 @@ export class ComposerAtts extends ComposerComponent { private getMaxAttSizeAndOversizeNotice = async (): Promise => { const subscription = await Store.subscription(this.view.acctEmail); - if (!Rules.relaxSubscriptionRequirements(this.composer.sender.getSender()) && !subscription.active) { + if (!Rules.isPublicEmailProviderDomain(this.composer.sender.getSender()) && !subscription.active) { return { sizeMb: 5, size: 5 * 1024 * 1024, diff --git a/extension/chrome/elements/composer/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/composer/formatters/encrypted-mail-msg-formatter.ts index 254415f05d2..092d1cfb7c1 100644 --- a/extension/chrome/elements/composer/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/composer/formatters/encrypted-mail-msg-formatter.ts @@ -26,7 +26,6 @@ declare const openpgp: typeof OpenPGP; export class EncryptedMsgMailFormatter extends BaseMailFormatter implements MailFormatterInterface { private armoredPubkeys: PubkeyResult[]; - private FC_WEB_URL = 'https://flowcrypt.com'; // todo Send plain (not encrypted)uld use Api.url() private pgpMimeRootType = `multipart/encrypted; protocol="application/pgp-encrypted";`; private fcAdminCodes: string[] = []; @@ -185,7 +184,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter implements Mail const { short, admin_code } = await Backend.messageUpload(authInfo, encryptedBody['text/plain']!); const storage = await Store.getAcct(this.acctEmail, ['outgoing_language']); const lang = storage.outgoing_language || 'EN'; - const msgUrl = `${this.FC_WEB_URL}/${short}`; + const msgUrl = Backend.url('decrypt', short); const a = ` ${Lang.compose.openMsg[lang]} `; diff --git a/extension/chrome/elements/subscribe.ts b/extension/chrome/elements/subscribe.ts index ac73ad97930..f991cde1841 100644 --- a/extension/chrome/elements/subscribe.ts +++ b/extension/chrome/elements/subscribe.ts @@ -68,8 +68,7 @@ View.run(class SubscribeView extends View { private renderSubscriptionDetails = async () => { this.authInfo = await Store.authInfo(this.acctEmail); try { - await Backend.accountGet(this.authInfo); - await Backend.getSubscriptionWithoutLogin(this.acctEmail); + await Backend.accountGetAndUpdateLocalStore(this.authInfo); } catch (e) { if (Api.err.isAuthErr(e)) { Xss.sanitizeRender('#content', `Not logged in. ${Ui.retryLink()}`); diff --git a/extension/chrome/settings/index.htm b/extension/chrome/settings/index.htm index a139dc43ebb..308dd52b41e 100644 --- a/extension/chrome/settings/index.htm +++ b/extension/chrome/settings/index.htm @@ -110,7 +110,7 @@

Set up FlowCrypt

- + FlowCrypt, set up and backup your Keys @@ -121,7 +121,7 @@

Set up FlowCrypt

- + diff --git a/extension/chrome/settings/index.ts b/extension/chrome/settings/index.ts index f50fdb61c00..1bfd5b4aa02 100644 --- a/extension/chrome/settings/index.ts +++ b/extension/chrome/settings/index.ts @@ -53,11 +53,6 @@ View.run(class SettingsView extends View { render = async () => { $('#status-row #status_v').text(`v:${VERSION}`); - const rules = await Rules.newInstance(this.acctEmail); - if (!rules.canBackupKeys()) { - $('.show_settings_page[page="modules/backup.htm"]').parent().remove(); - $('.settings-icons-rows').css({ position: 'relative', left: '64px' }); // lost a button - center it again - } for (const webmailLName of await Env.webmails()) { $('.signin_button.' + webmailLName).css('display', 'inline-block'); } @@ -176,6 +171,11 @@ View.run(class SettingsView extends View { const storage = await Store.getAcct(this.acctEmail, ['setup_done', 'email_provider', 'picture']); const scopes = await Store.getScopes(this.acctEmail); if (storage.setup_done) { + const rules = await Rules.newInstance(this.acctEmail); + if (!rules.canBackupKeys()) { + $('.show_settings_page[page="modules/backup.htm"]').parent().remove(); + $('.settings-icons-rows').css({ position: 'relative', left: '64px' }); // lost a button - center it again + } this.checkGoogleAcct().catch(Catch.reportErr); this.checkFcAcctAndSubscriptionAndContactPage().catch(Catch.reportErr); if (storage.picture) { @@ -227,9 +227,9 @@ View.run(class SettingsView extends View { const authInfo = await Store.authInfo(this.acctEmail!); if (authInfo.uuid) { // have auth email set try { - const response = await Backend.accountUpdate(authInfo); + const response = await Backend.accountGetAndUpdateLocalStore(authInfo); $('#status-row #status_flowcrypt').text(`fc:ok`); - if (response?.result?.alias) { + if (response?.account?.alias) { statusContainer.find('.status-indicator-text').css('display', 'none'); statusContainer.find('.status-indicator').addClass('active'); } else { diff --git a/extension/chrome/settings/modules/account.ts b/extension/chrome/settings/modules/account.ts index bd2d2fef767..0224662baee 100644 --- a/extension/chrome/settings/modules/account.ts +++ b/extension/chrome/settings/modules/account.ts @@ -33,7 +33,7 @@ View.run(class AccountView extends View { try { const r = await Backend.getSubscriptionWithoutLogin(this.acctEmail); subscription = new Subscription(r.subscription); - await Backend.accountGet(authInfo); // here to test auth + await Backend.accountGetAndUpdateLocalStore(authInfo); // here to test auth } catch (e) { if (Api.err.isAuthErr(e) && subscription.level) { Settings.offerToLoginWithPopupShowModalOnErr(this.acctEmail, () => window.location.reload()); diff --git a/extension/chrome/settings/modules/contact_page.ts b/extension/chrome/settings/modules/contact_page.ts index f0cdc97b3c1..22278e35915 100644 --- a/extension/chrome/settings/modules/contact_page.ts +++ b/extension/chrome/settings/modules/contact_page.ts @@ -51,8 +51,8 @@ View.run(class ContactPageView extends View { render = async () => { Xss.sanitizeRender(this.S.cached('status'), 'Loading..' + Ui.spinner('green')); try { - const response = await Backend.accountUpdate(await this.authInfoPromise); - this.renderFields(response.result); + const response = await Backend.accountGetAndUpdateLocalStore(await this.authInfoPromise); + this.renderFields(response.account); } catch (e) { if (Api.err.isAuthErr(e)) { Settings.offerToLoginWithPopupShowModalOnErr(this.acctEmail, () => window.location.reload()); @@ -67,7 +67,7 @@ View.run(class ContactPageView extends View { this.S.cached('action_close').click(this.setHandler(() => this.onCloseHandler())); } - private renderFields = (result: BackendRes.FcAccountUpdate$result) => { + private renderFields = (result: BackendRes.FcAccount$info) => { if (result.alias) { const me = Backend.url('me', result.alias); const meEscaped = Xss.escape(me); diff --git a/extension/chrome/settings/modules/debug_api.ts b/extension/chrome/settings/modules/debug_api.ts index 0ae5021da87..ec3d3c11453 100644 --- a/extension/chrome/settings/modules/debug_api.ts +++ b/extension/chrome/settings/modules/debug_api.ts @@ -41,6 +41,7 @@ View.run(class DebugApiView extends View { 'notification_setup_needed_dismissed', 'email_provider', 'google_token_scopes', 'hide_message_password', 'sendAs', 'outgoing_language', 'full_name', 'cryptup_enabled', 'setup_done', 'setup_simple', 'is_newly_created_key', 'key_backup_method', 'key_backup_prompt', 'successfully_received_at_leat_one_message', 'notification_setup_done_seen', 'openid', + 'rules', 'subscription', 'use_rich_text', ]); this.renderCallRes('Local account storage', { acctEmail: this.acctEmail }, storage); } else { diff --git a/extension/chrome/settings/modules/security.ts b/extension/chrome/settings/modules/security.ts index f6a0eb783b2..2d6cecb9bb5 100644 --- a/extension/chrome/settings/modules/security.ts +++ b/extension/chrome/settings/modules/security.ts @@ -79,9 +79,9 @@ View.run(class SecurityView extends View { if (subscription.active) { Xss.sanitizeRender('.select_loader_container', Ui.spinner('green')); try { - const response = await Backend.accountUpdate(this.authInfo!); + const response = await Backend.accountGetAndUpdateLocalStore(this.authInfo!); $('.select_loader_container').text(''); - $('.default_message_expire').val(Number(response.result.default_message_expire).toString()).prop('disabled', false).css('display', 'inline-block'); + $('.default_message_expire').val(Number(response.account.default_message_expire).toString()).prop('disabled', false).css('display', 'inline-block'); $('.default_message_expire').change(this.setHandler(() => this.onDefaultExpireUserChange())); } catch (e) { if (Api.err.isAuthErr(e)) { diff --git a/extension/chrome/settings/setup.htm b/extension/chrome/settings/setup.htm index 2b920f12370..12713ab1906 100644 --- a/extension/chrome/settings/setup.htm +++ b/extension/chrome/settings/setup.htm @@ -58,7 +58,7 @@
- Go back + Go back Help icon diff --git a/extension/js/common/api/backend.ts b/extension/js/common/api/backend.ts index 86cd8173a47..70bcb097aee 100644 --- a/extension/js/common/api/backend.ts +++ b/extension/js/common/api/backend.ts @@ -12,6 +12,8 @@ import { Catch } from '../platform/catch.js'; import { Att } from '../core/att.js'; import { Ui } from '../browser/ui.js'; import { Buf } from '../core/buf.js'; +import { DomainRules } from '../rules.js'; +import { BACKEND_API_HOST } from '../core/const.js'; type SubscriptionLevel = 'pro' | null; type ProfileUpdate = { alias?: string, name?: string, photo?: string, intro?: string, web?: string, phone?: string, default_message_expire?: number }; @@ -29,8 +31,9 @@ export type AwsS3UploadItem = { baseUrl: string, fields: { key: string; file?: A export namespace BackendRes { export type FcHelpFeedback = { sent: boolean }; export type FcAccountLogin = { registered: boolean, verified: boolean, subscription: SubscriptionInfo }; - export type FcAccountUpdate$result = { alias: string, email: string, intro: string, name: string, photo: string, default_message_expire: number }; - export type FcAccountUpdate = { result: FcAccountUpdate$result, updated: boolean }; + export type FcAccount$info = { alias: string, email: string, intro: string, name: string, photo: string, default_message_expire: number }; + export type FcAccountGet = { account: FcAccount$info, subscription: SubscriptionInfo, domain_org_rules: DomainRules }; + export type FcAccountUpdate = { result: FcAccount$info, updated: boolean }; export type FcAccountSubscribe = { subscription: SubscriptionInfo }; export type FcAccountCheck = { email: string | null, subscription: SubscriptionInfo | null }; export type FcBlogPost = { title: string, date: string, url: string }; @@ -53,13 +56,13 @@ export class Backend extends Api { return Backend.apiCall(Backend.url('api'), path, vals, fmt, undefined, { 'api-version': '3', ...addHeaders }); } - public static url = (type: string, variable = '') => { + public static url = (type: 'api' | 'me' | 'pubkey' | 'decrypt' | 'web', resource = '') => { return ({ - 'api': 'https://flowcrypt.com/api/', - 'me': 'https://flowcrypt.com/me/' + variable, - 'pubkey': 'https://flowcrypt.com/pub/' + variable, - 'decrypt': 'https://flowcrypt.com/' + variable, - 'web': 'https://flowcrypt.com/', + api: BACKEND_API_HOST, + me: `https://flowcrypt.com/me/${resource}`, + pubkey: `https://flowcrypt.com/pub/${resource}`, + decrypt: `https://flowcrypt.com/${resource}`, + web: 'https://flowcrypt.com/', } as Dict)[type]; } @@ -115,17 +118,19 @@ export class Backend extends Api { return r; } - public static accountUpdate = async (fcAuth: FcUuidAuth, profileUpdate: ProfileUpdate = {}): Promise => { + public static accountUpdate = async (fcAuth: FcUuidAuth, profileUpdate: ProfileUpdate): Promise => { Backend.throwIfMissingUuid(fcAuth); - const r = await Backend.request('account/update', { + return await Backend.request('account/update', { ...fcAuth, ...profileUpdate }) as BackendRes.FcAccountUpdate; - return r; } - public static accountGet = (fcAuth: FcUuidAuth) => { - return Backend.accountUpdate(fcAuth, {}); + public static accountGetAndUpdateLocalStore = async (fcAuth: FcUuidAuth): Promise => { + Backend.throwIfMissingUuid(fcAuth); + const r = await Backend.request('account/get', fcAuth) as BackendRes.FcAccountGet; + await Store.setAcct(fcAuth.account, { rules: r.domain_org_rules, subscription: r.subscription }); + return r; } public static accountSubscribe = async (fcAuth: FcUuidAuth, product: string, method: string, paymentSourceToken?: string): Promise => { diff --git a/extension/js/common/api/google-auth.ts b/extension/js/common/api/google-auth.ts index f04ddb08078..4c141a56c50 100644 --- a/extension/js/common/api/google-auth.ts +++ b/extension/js/common/api/google-auth.ts @@ -17,6 +17,8 @@ import { tabsQuery, windowsCreate } from './chrome.js'; import { Buf } from '../core/buf.js'; import { GOOGLE_API_HOST, GOOGLE_OAUTH_SCREEN_HOST } from '../core/const.js'; import { GmailRes } from './email_provider/gmail/gmail-parser.js'; +import { Backend } from './backend.js'; +import { Rules } from '../rules.js'; type GoogleAuthTokenInfo = { issued_to: string, audience: string, scope: string, expires_in: number, access_type: 'offline' }; type GoogleAuthTokensResponse = { access_token: string, expires_in: number, refresh_token?: string, id_token: string, token_type: 'Bearer' }; @@ -135,6 +137,23 @@ export class GoogleAuth { Catch.reportErr(e); } } + if (authRes.result === 'Success') { + if (!authRes.id_token) { + return { result: 'Error', error: 'Grant was successful but missing id_token', acctEmail: authRes.acctEmail, id_token: undefined }; + } + if (!authRes.acctEmail) { + return { result: 'Error', error: 'Grant was successful but missing acctEmail', acctEmail: authRes.acctEmail, id_token: undefined }; + } + if (!Rules.isPublicEmailProviderDomain(authRes.acctEmail)) { + try { // users on @custom-domain.com must check with backend to look for org rules, if any + const uuid = Api.randomFortyHexChars(); + await Backend.loginWithOpenid(authRes.acctEmail, uuid, authRes.id_token); + await Backend.accountGetAndUpdateLocalStore({ account: authRes.acctEmail, uuid }); // will store org rules and subscription + } catch (e) { + return { result: 'Error', error: `Grant successful but error loging into fc account: ${String(e)}`, acctEmail: authRes.acctEmail, id_token: undefined }; + } + } + } return authRes; } diff --git a/extension/js/common/core/const.ts b/extension/js/common/core/const.ts index fbc8ec70cbb..a11e6a47200 100644 --- a/extension/js/common/core/const.ts +++ b/extension/js/common/core/const.ts @@ -6,6 +6,8 @@ export const VERSION = '[BUILD_REPLACEABLE_VERSION]'; export const GOOGLE_API_HOST = '[BUILD_REPLACEABLE_GOOGLE_API_HOST]'; export const GOOGLE_OAUTH_SCREEN_HOST = '[BUILD_REPLACEABLE_GOOGLE_OAUTH_SCREEN_HOST]'; export const GOOGLE_CONTACTS_API_HOST = '[BUILD_REPLACEABLE_GOOGLE_CONTACTS_API_HOST]'; +export const BACKEND_API_HOST = '[BUILD_REPLACEABLE_BACKEND_API_HOST]'; + /** * Only put constants below if: * - they are useful across web/extension/Nodejs environments, AND diff --git a/extension/js/common/platform/store.ts b/extension/js/common/platform/store.ts index eded833353e..13ddd0534fa 100644 --- a/extension/js/common/platform/store.ts +++ b/extension/js/common/platform/store.ts @@ -12,6 +12,7 @@ import { storageLocalSet, storageLocalGet, storageLocalRemove } from '../api/chr import { PgpClient } from '../api/keyserver.js'; import { GmailRes } from '../api/email_provider/gmail/gmail-parser.js'; import { GoogleAuth } from '../api/google-auth.js'; +import { DomainRules } from '../rules.js'; import { Env } from '../browser/env.js'; import { Ui } from '../browser/ui.js'; @@ -20,7 +21,7 @@ import { Ui } from '../browser/ui.js'; let KEY_CACHE: { [longidOrArmoredKey: string]: OpenPGP.key.Key } = {}; let KEY_CACHE_WIPE_TIMEOUT: number; -type SerializableTypes = FlatTypes | string[] | number[] | boolean[] | SubscriptionInfo; +type SerializableTypes = FlatTypes | string[] | number[] | boolean[] | SubscriptionInfo | DomainRules; type StoredReplyDraftMeta = string; // draftId type StoredComposeDraftMeta = { recipients: string[], subject: string, date: number }; type StoredAdminCode = { date: number, codes: string[] }; @@ -68,7 +69,7 @@ export type ContactUpdate = { pubkey_last_check?: number | null; }; export type Storable = FlatTypes | string[] | KeyInfo[] | Dict | Dict | Dict - | SubscriptionInfo | GmailRes.OpenId; + | SubscriptionInfo | GmailRes.OpenId | DomainRules; export type Serializable = SerializableTypes | SerializableTypes[] | Dict | Dict[]; export interface RawStore { @@ -129,6 +130,7 @@ export type AccountStore = { openid?: GmailRes.OpenId; subscription?: SubscriptionInfo; uuid?: string; + rules?: DomainRules; // temporary tmp_submit_main?: boolean; tmp_submit_all?: boolean; @@ -144,7 +146,7 @@ export type AccountIndex = 'keys' | 'notification_setup_needed_dismissed' | 'ema 'google_token_refresh' | 'hide_message_password' | 'addresses' | 'sendAs' | 'drafts_reply' | 'drafts_compose' | 'pubkey_sent_to' | 'full_name' | 'cryptup_enabled' | 'setup_done' | 'setup_simple' | 'is_newly_created_key' | 'key_backup_method' | 'key_backup_prompt' | 'successfully_received_at_leat_one_message' | 'notification_setup_done_seen' | 'picture' | - 'outgoing_language' | 'setup_date' | 'openid' | 'tmp_submit_main' | 'tmp_submit_all' | 'subscription' | 'uuid' | 'use_rich_text'; + 'outgoing_language' | 'setup_date' | 'openid' | 'tmp_submit_main' | 'tmp_submit_all' | 'subscription' | 'uuid' | 'use_rich_text' | 'rules'; export class Subscription implements SubscriptionInfo { active?: boolean; diff --git a/extension/js/common/rules.ts b/extension/js/common/rules.ts index d26373de429..89416ef0430 100644 --- a/extension/js/common/rules.ts +++ b/extension/js/common/rules.ts @@ -2,62 +2,78 @@ 'use strict'; -import { Str, Dict } from './core/common.js'; +import { Dict, Str } from './core/common.js'; import { Buf } from './core/buf.js'; +import { Store } from './platform/store.js'; -export type DomainRule = { flags: ('NO_PRV_CREATE' | 'NO_PRV_BACKUP' | 'STRICT_GDPR' | 'ALLOW_CUSTOM_KEYSERVER' | 'ENFORCE_ATTESTER_SUBMIT')[] }; +type DomainRules$flag = 'NO_PRV_CREATE' | 'NO_PRV_BACKUP' | 'ALLOW_CUSTOM_KEYSERVER' | 'ENFORCE_ATTESTER_SUBMIT'; +export type DomainRules = { + flags: DomainRules$flag[], + custom_keyserver_url?: string, +}; export class Rules { - private static digest = async (domain: string) => { - return Buf.fromUint8(new Uint8Array(await crypto.subtle.digest('SHA-1', Buf.fromUtfStr(domain)))).toBase64Str(); - } - - private other = 'other'; - private domainHash: string = this.other; - private rules: Dict = { - 'dFEm3KyalKGTGjpeA/Ar44IPUdE=': { flags: ['NO_PRV_CREATE', 'NO_PRV_BACKUP', 'STRICT_GDPR', 'ENFORCE_ATTESTER_SUBMIT'] }, // n - 'd3VLGOyz8vfFm/IM/gavrCpkWOw=': { flags: ['NO_PRV_CREATE', 'NO_PRV_BACKUP', 'STRICT_GDPR', 'ENFORCE_ATTESTER_SUBMIT'] }, // v - 'xKzI/nSDX4g2Wfgih9y0sYIguRU=': { flags: ['NO_PRV_BACKUP', 'ALLOW_CUSTOM_KEYSERVER'] }, // h - [this.other]: { flags: [] }, - }; - - public static newInstance = async (email?: string) => { - if (email && Str.isEmailValid(email)) { - const domain = email.split('@')[1]; - return new Rules(await Rules.digest(domain)); + public static newInstance = async (acctEmail: string): Promise => { + if (!Str.parseEmail(acctEmail).email) { + throw new Error(`Not a valid email:${acctEmail}`); } - return new Rules(); - } - - private constructor(domainHash?: string) { - if (domainHash && Object.keys(this.rules).includes(domainHash)) { - this.domainHash = domainHash; // known domain, else initialized to this.other + const storage = await Store.getAcct(acctEmail, ['rules']); + if (storage.rules) { + return new Rules(storage.rules); + } else { + const legacyHardCoded = await Rules.legacyHardCodedRules(acctEmail); + await Store.setAcct(acctEmail, { rules: legacyHardCoded }); + return new Rules(legacyHardCoded); } } - public static relaxSubscriptionRequirements = (emailAddr: string) => { + protected constructor(private domainRules: DomainRules) { } + + public static isPublicEmailProviderDomain = (emailAddr: string) => { return ['gmail.com', 'yahoo.com', 'outlook.com', 'live.com'].includes(emailAddr.split('@')[1] || 'NONE'); } - canCreateKeys = () => !this.rules[this.domainHash].flags.includes('NO_PRV_CREATE'); + canCreateKeys = () => { + return !this.domainRules.flags.includes('NO_PRV_CREATE'); + } - canBackupKeys = () => !this.rules[this.domainHash].flags.includes('NO_PRV_BACKUP'); + canBackupKeys = () => { + return !this.domainRules.flags.includes('NO_PRV_BACKUP'); + } - hasStrictGdpr = () => this.rules[this.domainHash].flags.includes('STRICT_GDPR'); + mustSubmitToAttester = () => { + return this.domainRules.flags.includes('ENFORCE_ATTESTER_SUBMIT'); + } - mustSubmitToAttester = () => this.rules[this.domainHash].flags.includes('ENFORCE_ATTESTER_SUBMIT'); + canUseCustomKeyserver = () => { + return this.domainRules.flags.includes('ALLOW_CUSTOM_KEYSERVER'); + } - canUseCustomKeyserver = () => this.rules[this.domainHash].flags.includes('ALLOW_CUSTOM_KEYSERVER'); + getCustomKeyserver = (): string | undefined => { + return this.canUseCustomKeyserver() ? this.domainRules.custom_keyserver_url : undefined; + } - /** - * temporarily hard coded for one domain until we have appropriate backend service for this - */ - getCustomKeyserver = () => { - if (this.domainHash === 'xKzI/nSDX4g2Wfgih9y0sYIguRU=') { - return Buf.fromBase64Str('aHR0cHM6Ly9za3MucG9kMDEuZmxlZXRzdHJlZXRvcHMuY29tLw==').toUtfStr(); + private static legacyHardCodedRules = async (acctEmail: string): Promise => { + const hardCodedRules: Dict = { + 'dFEm3KyalKGTGjpeA/Ar44IPUdE=': { // n + flags: ['NO_PRV_CREATE', 'NO_PRV_BACKUP', 'ENFORCE_ATTESTER_SUBMIT'] + }, + 'd3VLGOyz8vfFm/IM/gavrCpkWOw=': { // v + flags: ['NO_PRV_CREATE', 'NO_PRV_BACKUP', 'ENFORCE_ATTESTER_SUBMIT'] + }, + 'xKzI/nSDX4g2Wfgih9y0sYIguRU=': { // h + flags: ['NO_PRV_BACKUP', 'ALLOW_CUSTOM_KEYSERVER'], + custom_keyserver_url: Buf.fromBase64Str('aHR0cHM6Ly9za3MucG9kMDEuZmxlZXRzdHJlZXRvcHMuY29tLw==').toUtfStr() + }, + }; + const domain = acctEmail.split('@')[1]; + const sha1 = Buf.fromUint8(new Uint8Array(await crypto.subtle.digest('SHA-1', Buf.fromUtfStr(domain)))).toBase64Str(); + const foundHardCoded = hardCodedRules[sha1]; + if (foundHardCoded) { + return foundHardCoded; } - return undefined; + return { flags: [] }; } } diff --git a/test/samples/mock-data.ts b/test/samples/mock-data.ts index 9c4ca479060..319d371c4ca 100644 --- a/test/samples/mock-data.ts +++ b/test/samples/mock-data.ts @@ -1,127 +1,127 @@ -import { GmailMsg, GmailDraft } from '../source/mock/data'; +import { GmailMsg, GmailDraft } from '../source/mock/google/google-data'; type UserMessages = { - [email: string]: { - messages: GmailMsg[], - drafts: GmailDraft[] - } + [email: string]: { + messages: GmailMsg[], + drafts: GmailDraft[] + } }; // tslint:disable: max-line-length const data: UserMessages = { - 'flowcrypt.compatibility@gmail.com': { - drafts: [{ - "id": "draft-0", - "message": { - "id": "16eec6ebc087faa7", - "threadId": "16eec6ebc087faa7", - "labelIds": ["DRAFT"], - "snippet": "[cryptup:link:draft_compose:r304765387393056602] -----BEGIN PGP MESSAGE----- Version: FlowCrypt 7.3.6 Gmail Encryption Comment: Seamlessly send and receive encrypted email wcFMA0taL/zmLZUBAQ//", - "historyId": "1105694", - "internalDate": "1575924710000", - "payload": { - "mimeType": "multipart/mixed", - "headers": [{ - "name": "Received", "value": "from 717284730244 named unknown by gmailapi.google.com with HTTPREST; Mon, 9 Dec 2019 12:51:50 -0800" - }, - { - "name": "Content-Type", "value": "multipart/mixed; boundary=\"----sinikael-?=_1-15759247104200.4540311469610798\"" - }, - { - "name": "To", "value": "human@flowcrypt.com" - }, - { - "name": "From", "value": "flowcrypt.compatibility@gmail.com" - }, - { - "name": "Subject", "value": "Test Draft (testing tags)" - }, - { - "name": "Date", "value": "Mon, 9 Dec 2019 12:51:50 -0800" - }, - { - "name": "Message-Id", "value": "" - }, - { - "name": "MIME-Version", "value": "1.0" - }], - "parts": [{ - "headers": [{ "name": "Content-Type", "value": "text/plain" }, - { "name": "Content-Transfer-Encoding", "value": "quoted-printable" }], - "body": { - attachmentId: "", - "size": 1046, - "data": "W2NyeXB0dXA6bGluazpkcmFmdF9jb21wb3NlOnIzMDQ3NjUzODczOTMwNTY2MDJdDQoNCi0tLS0tQkVHSU4gUEdQIE1FU1NBR0UtLS0tLQ0KVmVyc2lvbjogRmxvd0NyeXB0IDcuMy42IEdtYWlsIEVuY3J5cHRpb24NCkNvbW1lbnQ6IFNlYW1sZXNzbHkgc2VuZCBhbmQgcmVjZWl2ZSBlbmNyeXB0ZWQgZW1haWwNCg0Kd2NGTUEwdGFML3ptTFpVQkFRLy9WMEtMd2ZPZVBVeS82OU5kZVhnR3Vtd013d0lDRWtQUXJPL24vaXp6DQpsdzltOWo4cGVhVE9UYWY5NkJFMTQvcXNyS0Vka3VoSzI5RTAxWjFNTGVMTDBLaXN5Mzh0WmRncTB3MWsNCnhpM2FlS1VSUVdMRE9vUWtmSzd4ZWNldGFMblBiZWp0dE4rcUUvQVZYWmZkSWR0WnNKVHUzY2pxQkJCUA0KSjUvMS9kQWFVbWJ6U29KaVcvT3NlcEtEdEw4ZnN4bUJMYlJYMXZMcS9aeEdDdWlyYlFKb1dFSi9lWFRvDQpxeVdiZ2h5NVNxNE1XMVo1TXBTelRyM2kxKzBmbTM2ZUFIaFRoS3l0N0V0VlRVeDJpYUhTck5iUXdQZGkNCmNPY290Z2Z1ZlhDTTRSUy9ZTURTNjJyUVJtcjJzenBtajJrSzhJQ2c1TXdtN3N0N3NBMWpyUk81Ry9pNA0KZEltQnNKQTBMSk50dkNmMGlWdWJZdGhhTEk1cTJoV2ltZ2xzNnBwMTlZYUhZVFI2Rm12RzNtTzZXL0xpDQpvSm1zZjE0RWprVlVLeGg0d2pmcGh2S1BQWU5ob0xIYzYzbFFEWTAvZ0hVclR4TzJvL0E2dzcyK2JvRkMNCkt6a3hJVGVrdDZzUHhGRnJWaDRRSURmSDdockthMjMzOTNMMEEvTWZuTmxrT3gxR2ZhTGsvMU1ROWxEcw0KcStES0ZDYUJIN1VhN0RXeFc0RnRiODZBK1ZhMW8weXN6Y2xWV1gyQmFRTURTYUdtSlpCZ282S1kxbFFrDQpzZnd5WHlzd0JZbmd0d0xnWllURUt5VnA0WEZJZmZDbFozK1lvMHVoNCtDRkVZY0s1MnhmUWZwVE5jbUoNCjl6b2JsOUxNV0NQbmlSZStFeHFiVGZMUHVudXVEOTVqdkhPZjFmZDEvc0RTU0FIbFhiWGUzWEhwSnNrRw0KU1EwaUJrNDh0ZE42OUR5OGVMSFRHV2NOd01xdW9TTnJMaGkxQ2tIN3hPYkJIOTlyVXVmaGtBa0pQUGx4DQpaNmhEcE10aTVEWlBXbjB1SlozcVRBPT0NCj1RM2xPDQotLS0tLUVORCBQR1AgTUVTU0FHRS0tLS0tDQo=" - } - }] - }, - "raw": "UmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCU1vbiwgOSBEZWMgMjAxOSAxMjo1MTo1MCAtMDgwMA0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7DQogYm91bmRhcnk9Ii0tLS1zaW5pa2FlbC0_PV8xLTE1NzU5MjQ3MTA0MjAwLjQ1NDAzMTE0Njk2MTA3OTgiDQpUbzogaHVtYW5AZmxvd2NyeXB0LmNvbQ0KRnJvbTogZmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpTdWJqZWN0OiBUZXN0IERyYWZ0ICh0ZXN0aW5nIHRhZ3MpDQpEYXRlOiBNb24sIDkgRGVjIDIwMTkgMTI6NTE6NTAgLTA4MDANCk1lc3NhZ2UtSWQ6IDxDQUtidUxUcW1pTUY9X0hqK0NpRkdESj1OS19TalRiPWtwQytoVFNqRExHWDlNNFFXRmdAbWFpbC5nbWFpbC5jb20-DQpNSU1FLVZlcnNpb246IDEuMA0KDQotLS0tLS1zaW5pa2FlbC0_PV8xLTE1NzU5MjQ3MTA0MjAwLjQ1NDAzMTE0Njk2MTA3OTgNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KDQpbY3J5cHR1cDpsaW5rOmRyYWZ0X2NvbXBvc2U6cjMwNDc2NTM4NzM5MzA1NjYwMl0NCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgNy4zLjYgR21haWwgRW5jcnlwdGlvbg0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3Y0ZNQTB0YUwvem1MWlVCQVEvL1YwS0x3Zk9lUFV5LzY5TmRlWGdHdW13TXd3SUNFa1BRck8vbi9penoNCmx3OW05ajhwZWFUT1RhZjk2QkUxNC9xc3JLRWRrdWhLMjlFMDFaMU1MZUxMMEtpc3kzOHRaZGdxMHcxaw0KeGkzYWVLVVJRV0xET29Ra2ZLN3hlY2V0YUxuUGJlanR0TitxRS9BVlhaZmRJZHRac0pUdTNjanFCQkJQDQpKNS8xL2RBYVVtYnpTb0ppVy9Pc2VwS0R0TDhmc3htQkxiUlgxdkxxL1p4R0N1aXJiUUpvV0VKL2VYVG8NCnF5V2JnaHk1U3E0TVcxWjVNcFN6VHIzaTErMGZtMzZlQUhoVGhLeXQ3RXRWVFV4MmlhSFNyTmJRd1BkaQ0KY09jb3RnZnVmWENNNFJTL1lNRFM2MnJRUm1yMnN6cG1qMmtLOElDZzVNd203c3Q3c0ExanJSTzVHL2k0DQpkSW1Cc0pBMExKTnR2Q2YwaVZ1Yll0aGFMSTVxMmhXaW1nbHM2cHAxOVlhSFlUUjZGbXZHM21PNlcvTGkNCm9KbXNmMTRFamtWVUt4aDR3amZwaHZLUFBZTmhvTEhjNjNsUURZMC9nSFVyVHhPMm8vQTZ3NzIrYm9GQw0KS3preElUZWt0NnNQeEZGclZoNFFJRGZIN2hyS2EyMzM5M0wwQS9NZm5ObGtPeDFHZmFMay8xTVE5bERzDQpxK0RLRkNhQkg3VWE3RFd4VzRGdGI4NkErVmExbzB5c3pjbFZXWDJCYVFNRFNhR21KWkJnbzZLWTFsUWsNCnNmd3lYeXN3QlluZ3R3TGdaWVRFS3lWcDRYRklmZkNsWjMrWW8wdWg0K0NGRVljSzUyeGZRZnBUTmNtSg0KOXpvYmw5TE1XQ1BuaVJlK0V4cWJUZkxQdW51dUQ5NWp2SE9mMWZkMS9zRFNTQUhsWGJYZTNYSHBKc2tHDQpTUTBpQms0OHRkTjY5RHk4ZUxIVEdXY053TXF1b1NOckxoaTFDa0g3eE9iQkg5OXJVdWZoa0FrSlBQbHgNClo2aERwTXRpNURaUFduMHVKWjNxVEE9M0Q9M0QNCj0zRFEzbE8NCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTc1OTI0NzEwNDIwMC40NTQwMzExNDY5NjEwNzk4LS0NCg==" + 'flowcrypt.compatibility@gmail.com': { + drafts: [{ + "id": "draft-0", + "message": { + "id": "16eec6ebc087faa7", + "threadId": "16eec6ebc087faa7", + "labelIds": ["DRAFT"], + "snippet": "[cryptup:link:draft_compose:r304765387393056602] -----BEGIN PGP MESSAGE----- Version: FlowCrypt 7.3.6 Gmail Encryption Comment: Seamlessly send and receive encrypted email wcFMA0taL/zmLZUBAQ//", + "historyId": "1105694", + "internalDate": "1575924710000", + "payload": { + "mimeType": "multipart/mixed", + "headers": [{ + "name": "Received", "value": "from 717284730244 named unknown by gmailapi.google.com with HTTPREST; Mon, 9 Dec 2019 12:51:50 -0800" + }, + { + "name": "Content-Type", "value": "multipart/mixed; boundary=\"----sinikael-?=_1-15759247104200.4540311469610798\"" + }, + { + "name": "To", "value": "human@flowcrypt.com" + }, + { + "name": "From", "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Subject", "value": "Test Draft (testing tags)" + }, + { + "name": "Date", "value": "Mon, 9 Dec 2019 12:51:50 -0800" + }, + { + "name": "Message-Id", "value": "" + }, + { + "name": "MIME-Version", "value": "1.0" + }], + "parts": [{ + "headers": [{ "name": "Content-Type", "value": "text/plain" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" }], + "body": { + attachmentId: "", + "size": 1046, + "data": "W2NyeXB0dXA6bGluazpkcmFmdF9jb21wb3NlOnIzMDQ3NjUzODczOTMwNTY2MDJdDQoNCi0tLS0tQkVHSU4gUEdQIE1FU1NBR0UtLS0tLQ0KVmVyc2lvbjogRmxvd0NyeXB0IDcuMy42IEdtYWlsIEVuY3J5cHRpb24NCkNvbW1lbnQ6IFNlYW1sZXNzbHkgc2VuZCBhbmQgcmVjZWl2ZSBlbmNyeXB0ZWQgZW1haWwNCg0Kd2NGTUEwdGFML3ptTFpVQkFRLy9WMEtMd2ZPZVBVeS82OU5kZVhnR3Vtd013d0lDRWtQUXJPL24vaXp6DQpsdzltOWo4cGVhVE9UYWY5NkJFMTQvcXNyS0Vka3VoSzI5RTAxWjFNTGVMTDBLaXN5Mzh0WmRncTB3MWsNCnhpM2FlS1VSUVdMRE9vUWtmSzd4ZWNldGFMblBiZWp0dE4rcUUvQVZYWmZkSWR0WnNKVHUzY2pxQkJCUA0KSjUvMS9kQWFVbWJ6U29KaVcvT3NlcEtEdEw4ZnN4bUJMYlJYMXZMcS9aeEdDdWlyYlFKb1dFSi9lWFRvDQpxeVdiZ2h5NVNxNE1XMVo1TXBTelRyM2kxKzBmbTM2ZUFIaFRoS3l0N0V0VlRVeDJpYUhTck5iUXdQZGkNCmNPY290Z2Z1ZlhDTTRSUy9ZTURTNjJyUVJtcjJzenBtajJrSzhJQ2c1TXdtN3N0N3NBMWpyUk81Ry9pNA0KZEltQnNKQTBMSk50dkNmMGlWdWJZdGhhTEk1cTJoV2ltZ2xzNnBwMTlZYUhZVFI2Rm12RzNtTzZXL0xpDQpvSm1zZjE0RWprVlVLeGg0d2pmcGh2S1BQWU5ob0xIYzYzbFFEWTAvZ0hVclR4TzJvL0E2dzcyK2JvRkMNCkt6a3hJVGVrdDZzUHhGRnJWaDRRSURmSDdockthMjMzOTNMMEEvTWZuTmxrT3gxR2ZhTGsvMU1ROWxEcw0KcStES0ZDYUJIN1VhN0RXeFc0RnRiODZBK1ZhMW8weXN6Y2xWV1gyQmFRTURTYUdtSlpCZ282S1kxbFFrDQpzZnd5WHlzd0JZbmd0d0xnWllURUt5VnA0WEZJZmZDbFozK1lvMHVoNCtDRkVZY0s1MnhmUWZwVE5jbUoNCjl6b2JsOUxNV0NQbmlSZStFeHFiVGZMUHVudXVEOTVqdkhPZjFmZDEvc0RTU0FIbFhiWGUzWEhwSnNrRw0KU1EwaUJrNDh0ZE42OUR5OGVMSFRHV2NOd01xdW9TTnJMaGkxQ2tIN3hPYkJIOTlyVXVmaGtBa0pQUGx4DQpaNmhEcE10aTVEWlBXbjB1SlozcVRBPT0NCj1RM2xPDQotLS0tLUVORCBQR1AgTUVTU0FHRS0tLS0tDQo=" } + }] }, - { - "id": "draft-1", - "message": { - "id": "16d6cbeb73bd2a9b", - "threadId": "16d6cbeb73bd2a9b", - "labelIds": ["DRAFT"], - "snippet": "[cryptup:link:draft_compose:r-8909860425873898730] -----BEGIN PGP MESSAGE----- Version: FlowCrypt 7.0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wcFMA0taL/", - "historyId": "1058042", - "internalDate": "1569487501000", - "payload": { - "mimeType": "multipart/mixed", - "headers": [ - { "name": "Received", "value": "from 717284730244 named unknown by gmailapi.google.com with HTTPREST; Thu, 26 Sep 2019 05:45:01 -0300" }, - { "name": "Content-Type", "value": "multipart/mixed; boundary=\"----sinikael-?=_1-15694875011360.5996051294330165\"" }, - { "name": "To", "value": "flowcryptcompatibility@gmail.com" }, { "name": "Cc", "value": "flowcrypt.compatibility@gmail.com" }, - { "name": "Bcc", "value": "human@flowcrypt.com" }, { "name": "From", "value": "flowcrypt.compatibility@gmail.com" }, - { "name": "Subject", "value": "Test Draft - New Message" }, { "name": "Date", "value": "Thu, 26 Sep 2019 05:45:01 -0300" }, - { "name": "Message-Id", "value": "" }, - { "name": "MIME-Version", "value": "1.0" }], - "parts": [ - { - "headers": [{ "name": "Content-Type", "value": "text/plain" }, - { "name": "Content-Transfer-Encoding", "value": "quoted-printable" }], - }] - }, - "raw": "UmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCVRodSwgMjYgU2VwIDIwMTkgMDU6NDU6MDEgLTAzMDANCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KIGJvdW5kYXJ5PSItLS0tc2luaWthZWwtPz1fMS0xNTY5NDg3NTAxMTM2MC41OTk2MDUxMjk0MzMwMTY1Ig0KVG86IGZsb3djcnlwdGNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpDYzogZmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpCY2M6IGh1bWFuQGZsb3djcnlwdC5jb20NCkZyb206IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KU3ViamVjdDogVGVzdCBEcmFmdCAtIE5ldyBNZXNzYWdlDQpEYXRlOiBUaHUsIDI2IFNlcCAyMDE5IDA1OjQ1OjAxIC0wMzAwDQpNZXNzYWdlLUlkOiA8Q0FLYnVMVHJEMEhoT2FiVk4wPXVRdmRScnhheDl4ZkpLc2RkRjFSS0hhNUxKakI5dGRnQG1haWwuZ21haWwuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY5NDg3NTAxMTM2MC41OTk2MDUxMjk0MzMwMTY1DQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KW2NyeXB0dXA6bGluazpkcmFmdF9jb21wb3NlOnItODkwOTg2MDQyNTg3Mzg5ODczMF0NCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgNy4wLjIgR21haWwgRW5jcnlwdGlvbg0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3Y0ZNQTB0YUwvem1MWlVCQVJBQWlSRE1nMWg1YjNJeEU5YmxmbVZyS2hwSis2WDYyZjlZclFDQ0FaK0oNCjdraUJWeFppaWgrbmYvK1RWREFpMXpLa083OEVxTEoyQ2Q4SlBmUmtYbnRtd2IwSldNaGE4a1lGcmZMbQ0KbUxYWGxwOVoxVGRldkhJa2RLdTUvY0kwOVArdHU3YjNKL0twRUI5d3dQN3FjSlBPdkdEekdHeWJ2eElEDQpTWG9iU1VxbjlkUGlxMC9tVTJHamVWMWMzQkVxSHVORWNEaVdwUE5wOHdQOEp2UE1uVUw0OTFEaExpVmENCk1keVJvaGI3YVUwZUx1ZW5URFpuQktaZXhlWEpkNklOY1h4dzRDTFlOOFRGc1N2emRTQWVJSm95QlZ6Tg0KQU93K1JTVHpUTFZhUGtZV1NZUzRFK2VRT25tcVR5V3A3SEtRa25FQ0J2SlA4YlI3NUo4dEhtUzdnOUUzDQpKV0thaVpVT2VHbkJJTURpOEFZMlNDN0luVi9kRzY2Ynhxb0lEMG83a293VWFwQXY3TDlGSm1HRTM3VUkNCnJFWnNkWmozUkIvUm9uSmV5OVRpSm51Rm1TUUllQkNxU09QN3ZOZ3l2d0lyRGJNOWhZaHFJd1FXRXNCUA0KblN5TzJDV3J1dnRuSUoyeHJHVGZDa3pkdEMrOG5tL3NJL3R6VnhsUlR1aW1RdE43UTc1ZlpBQzZhdlNNDQpoRWZmQTg4WVB2ZVpnd1VtK3pmTUEyZVBxQ0ZaZnNGV1ZjWjR3VXc0RGpXUkRwbE9Bd21MTGt5ZGZyQjkNCndWdlBaeU81d1F4ckc2ak82d3FzcVhsNDlpQVVWaU0zSE1NTkZqNzR1dUFSTUF1dVpCMytUVkJ5bnhsbw0KUWZvVXI1T0xFNUJKdGVDb0FNVEpYWVNCRGlHUlZvR3J1K3hjOXRLSnJMRFNWZ0VkTWtNWE12UDU3amlqDQpSUW9yb3Zkdm9YRjVlUlVadkhUU2JvTTRIYWZNS0lPdmFSd3E3S2RoQU1TNElIL1JZWVZnSW1DY3FRN2gNCjU3T3ZyUUxONmxSdnFDRHFXTW13Wk1GNFV5eEVqenJpc2d5dVQ5R3oNCj0zREtTdDcNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY5NDg3NTAxMTM2MC41OTk2MDUxMjk0MzMwMTY1LS0NCg==" - } + "raw": "UmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCU1vbiwgOSBEZWMgMjAxOSAxMjo1MTo1MCAtMDgwMA0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7DQogYm91bmRhcnk9Ii0tLS1zaW5pa2FlbC0_PV8xLTE1NzU5MjQ3MTA0MjAwLjQ1NDAzMTE0Njk2MTA3OTgiDQpUbzogaHVtYW5AZmxvd2NyeXB0LmNvbQ0KRnJvbTogZmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpTdWJqZWN0OiBUZXN0IERyYWZ0ICh0ZXN0aW5nIHRhZ3MpDQpEYXRlOiBNb24sIDkgRGVjIDIwMTkgMTI6NTE6NTAgLTA4MDANCk1lc3NhZ2UtSWQ6IDxDQUtidUxUcW1pTUY9X0hqK0NpRkdESj1OS19TalRiPWtwQytoVFNqRExHWDlNNFFXRmdAbWFpbC5nbWFpbC5jb20-DQpNSU1FLVZlcnNpb246IDEuMA0KDQotLS0tLS1zaW5pa2FlbC0_PV8xLTE1NzU5MjQ3MTA0MjAwLjQ1NDAzMTE0Njk2MTA3OTgNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KDQpbY3J5cHR1cDpsaW5rOmRyYWZ0X2NvbXBvc2U6cjMwNDc2NTM4NzM5MzA1NjYwMl0NCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgNy4zLjYgR21haWwgRW5jcnlwdGlvbg0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3Y0ZNQTB0YUwvem1MWlVCQVEvL1YwS0x3Zk9lUFV5LzY5TmRlWGdHdW13TXd3SUNFa1BRck8vbi9penoNCmx3OW05ajhwZWFUT1RhZjk2QkUxNC9xc3JLRWRrdWhLMjlFMDFaMU1MZUxMMEtpc3kzOHRaZGdxMHcxaw0KeGkzYWVLVVJRV0xET29Ra2ZLN3hlY2V0YUxuUGJlanR0TitxRS9BVlhaZmRJZHRac0pUdTNjanFCQkJQDQpKNS8xL2RBYVVtYnpTb0ppVy9Pc2VwS0R0TDhmc3htQkxiUlgxdkxxL1p4R0N1aXJiUUpvV0VKL2VYVG8NCnF5V2JnaHk1U3E0TVcxWjVNcFN6VHIzaTErMGZtMzZlQUhoVGhLeXQ3RXRWVFV4MmlhSFNyTmJRd1BkaQ0KY09jb3RnZnVmWENNNFJTL1lNRFM2MnJRUm1yMnN6cG1qMmtLOElDZzVNd203c3Q3c0ExanJSTzVHL2k0DQpkSW1Cc0pBMExKTnR2Q2YwaVZ1Yll0aGFMSTVxMmhXaW1nbHM2cHAxOVlhSFlUUjZGbXZHM21PNlcvTGkNCm9KbXNmMTRFamtWVUt4aDR3amZwaHZLUFBZTmhvTEhjNjNsUURZMC9nSFVyVHhPMm8vQTZ3NzIrYm9GQw0KS3preElUZWt0NnNQeEZGclZoNFFJRGZIN2hyS2EyMzM5M0wwQS9NZm5ObGtPeDFHZmFMay8xTVE5bERzDQpxK0RLRkNhQkg3VWE3RFd4VzRGdGI4NkErVmExbzB5c3pjbFZXWDJCYVFNRFNhR21KWkJnbzZLWTFsUWsNCnNmd3lYeXN3QlluZ3R3TGdaWVRFS3lWcDRYRklmZkNsWjMrWW8wdWg0K0NGRVljSzUyeGZRZnBUTmNtSg0KOXpvYmw5TE1XQ1BuaVJlK0V4cWJUZkxQdW51dUQ5NWp2SE9mMWZkMS9zRFNTQUhsWGJYZTNYSHBKc2tHDQpTUTBpQms0OHRkTjY5RHk4ZUxIVEdXY053TXF1b1NOckxoaTFDa0g3eE9iQkg5OXJVdWZoa0FrSlBQbHgNClo2aERwTXRpNURaUFduMHVKWjNxVEE9M0Q9M0QNCj0zRFEzbE8NCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTc1OTI0NzEwNDIwMC40NTQwMzExNDY5NjEwNzk4LS0NCg==" + } + }, + { + "id": "draft-1", + "message": { + "id": "16d6cbeb73bd2a9b", + "threadId": "16d6cbeb73bd2a9b", + "labelIds": ["DRAFT"], + "snippet": "[cryptup:link:draft_compose:r-8909860425873898730] -----BEGIN PGP MESSAGE----- Version: FlowCrypt 7.0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wcFMA0taL/", + "historyId": "1058042", + "internalDate": "1569487501000", + "payload": { + "mimeType": "multipart/mixed", + "headers": [ + { "name": "Received", "value": "from 717284730244 named unknown by gmailapi.google.com with HTTPREST; Thu, 26 Sep 2019 05:45:01 -0300" }, + { "name": "Content-Type", "value": "multipart/mixed; boundary=\"----sinikael-?=_1-15694875011360.5996051294330165\"" }, + { "name": "To", "value": "flowcryptcompatibility@gmail.com" }, { "name": "Cc", "value": "flowcrypt.compatibility@gmail.com" }, + { "name": "Bcc", "value": "human@flowcrypt.com" }, { "name": "From", "value": "flowcrypt.compatibility@gmail.com" }, + { "name": "Subject", "value": "Test Draft - New Message" }, { "name": "Date", "value": "Thu, 26 Sep 2019 05:45:01 -0300" }, + { "name": "Message-Id", "value": "" }, + { "name": "MIME-Version", "value": "1.0" }], + "parts": [ + { + "headers": [{ "name": "Content-Type", "value": "text/plain" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" }], + }] }, - { - "id": "draft-3", - "message": { - "id": "16cfb0e25821733b", - "threadId": "16cfb0e25821733b", - "labelIds": ["DRAFT"], - "snippet": "[cryptup:link:draft_reply:16cfa9001baaac0a] -----BEGIN PGP MESSAGE----- Version: FlowCrypt 6.9.9 Gmail Encryption Comment: Seamlessly send and receive encrypted email wcFMA0taL/zmLZUBAQ/", - "historyId": "1044100", - "internalDate": "1567580104000", - "payload": { - "mimeType": "multipart/mixed", - "headers": [ - { "name": "Received", "value": "from 717284730244 named unknown by gmailapi.google.com with HTTPREST; Tue, 3 Sep 2019 23:55:04 -0700" }, - { "name": "Content-Type", "value": "multipart/mixed; boundary=\"----sinikael-?=_1-15675801039490.1727867765228115\"" }, - { "name": "To", "value": "flowcryptcompatibility@gmail.com" }, - { "name": "From", "value": "flowcrypt.compatibility@gmail.com" }, - { "name": "Subject", "value": "Re: Test Draft Save" }, - { "name": "Date", "value": "Tue, 3 Sep 2019 23:55:04 -0700" }, - { "name": "Message-Id", "value": "" }, - { "name": "MIME-Version", "value": "1.0" } - ], - "parts": [ - { - "mimeType": "text/plain", - "filename": "", - "headers": [ - { "name": "Content-Type", "value": "text/plain" }, - { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } - ], - }] - }, - "raw": "UmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCVR1ZSwgMyBTZXAgMjAxOSAyMzo1NTowNCAtMDcwMA0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7DQogYm91bmRhcnk9Ii0tLS1zaW5pa2FlbC0_PV8xLTE1Njc1ODAxMDM5NDkwLjE3Mjc4Njc3NjUyMjgxMTUiDQpUbzogZmxvd2NyeXB0Y29tcGF0aWJpbGl0eUBnbWFpbC5jb20NCkZyb206IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KU3ViamVjdDogUmU6IFRlc3QgRHJhZnQgU2F2ZQ0KRGF0ZTogVHVlLCAzIFNlcCAyMDE5IDIzOjU1OjA0IC0wNzAwDQpNZXNzYWdlLUlkOiA8Q0FLYnVMVG9KaWlya0h1YVRfSERMcmRSMVlwR1NaZVpfNG56S0p1XzBmY2Fkdj11R2hnQG1haWwuZ21haWwuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY3NTgwMTAzOTQ5MC4xNzI3ODY3NzY1MjI4MTE1DQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KW2NyeXB0dXA6bGluazpkcmFmdF9yZXBseToxNmNmYTkwMDFiYWFhYzBhXQ0KDQotLS0tLUJFR0lOIFBHUCBNRVNTQUdFLS0tLS0NClZlcnNpb246IEZsb3dDcnlwdCA2LjkuOSBHbWFpbCBFbmNyeXB0aW9uDQpDb21tZW50OiBTZWFtbGVzc2x5IHNlbmQgYW5kIHJlY2VpdmUgZW5jcnlwdGVkIGVtYWlsDQoNCndjRk1BMHRhTC96bUxaVUJBUS84Q0NtWWl2TVk0ckhoMUZOTDcwM1NvNkZia3ZTdDRnMEU4MnJHVDZIQg0KWnM2Nm1VQzBVWWI0bUF1VW9IazYyQ1F6TmtqZjJxbXBnRUNtN1VBZXNFdVdQZFljZ0dhMm0xVmtTSzVHDQo3ajd0UGl0S2RXMVlQZzFPeDE5cnhGbkxZQ2h6QnVOL1BqQk02ZG92dVl4SnZGVzBudGN6MEZkbVFTL0MNCkNiVklRViszMVBFREg1YXd1Wit6WlFlM2hUM1diRVo4N3RRTW9ESFZSQzhjdGVhZ3NoVFZPbldhb3h6RA0Ka3ZGajVENUEwWDljUXJ6Q3JwSnhYZFZvTmJBUng0UmFQbTR0K3ZGN29lSU1SRjBYWWxSWExmRlJKejRNDQo3WmhTTlNsNWNHRndTaEZxWlRoN3JDWlkxZGduUGtxS21PSkQyMDFHMmd0MjF0amViWnJoc3Y5K1cxSm4NCk5PTUkzSzFSMlNKMlVNWG9yYnhIaG5TNjBvWGxUc0hUTkdUWDFZd1BGeElpWkJzdjc2OG1LZDhBY3liTg0KSXRJMVEyd1FFWlVJekxYdTlxZmJ3V2owMGFmNkM0YUdVcW5RMkJReE9CenFHbHU0TU4zUjg3SUJOYUpsDQpmKzdYSmFjaGYvWUpJa284SlN4cnR6T1BBRnlOcmxjWURVZHdhQzBhTEUwY09CVXFkRW9sQkxiY1FhdWUNCi9sbGRRSnJreVdFZnNTVk1qWWwyeEhRdXlnZGhleW9sOTlKdlM3Y0E5YkJLN3lteXdoMm1XdVpGY1NNMQ0KTUdBeXBBdE9wZEV2bnJjeVZ0TVEyWGVwcDFJMGJCVTJGRzNOVWhsemRzS3dHVVdPUTB0QWlLeUkxQ3hKDQppMzZVVC9ud25CdnRqcE1PeTFyQTNEbUFrZjNsbXA2SHAyRUJyMnc2YWdqU3dBWUJlamhUYXliVmhsY3QNCmZFNW5GaGZvSWtTWXh4S1dIaU5uK2dHVGJ3THRqbVl2L2U4dGI1UmxUdHdzM2QyTUZBcVZXVVUzRlU5VQ0KRVV3K0FaT2ozZExkcVNWYTVXMEpmQ21KcU5jS0Z1QlFQWkdlL0ZlRE5hS24wYkhUTHdlekNKSEhrbDdNDQpXWFNhYTViaUpkVzR6RTNFQU95c2dGNFRmTmg0Q0pWR0ZRM2puRjhPVDUrVy9MVFZkWUwrT3pqa092MWkNCi9Sdnd2WHhBYkFFVG03V09zMkxTZnhMVGdNQVY3b0lPLzhBc0F2VVpHaUszSlh0Mm0zdkZvaVZVQjJKdA0KUkFtSEtPakhaMHc9M0QNCj0zRDUyN1YNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY3NTgwMTAzOTQ5MC4xNzI3ODY3NzY1MjI4MTE1LS0NCg==" - } - }], - messages: [] - } + "raw": "UmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCVRodSwgMjYgU2VwIDIwMTkgMDU6NDU6MDEgLTAzMDANCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KIGJvdW5kYXJ5PSItLS0tc2luaWthZWwtPz1fMS0xNTY5NDg3NTAxMTM2MC41OTk2MDUxMjk0MzMwMTY1Ig0KVG86IGZsb3djcnlwdGNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpDYzogZmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpCY2M6IGh1bWFuQGZsb3djcnlwdC5jb20NCkZyb206IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KU3ViamVjdDogVGVzdCBEcmFmdCAtIE5ldyBNZXNzYWdlDQpEYXRlOiBUaHUsIDI2IFNlcCAyMDE5IDA1OjQ1OjAxIC0wMzAwDQpNZXNzYWdlLUlkOiA8Q0FLYnVMVHJEMEhoT2FiVk4wPXVRdmRScnhheDl4ZkpLc2RkRjFSS0hhNUxKakI5dGRnQG1haWwuZ21haWwuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY5NDg3NTAxMTM2MC41OTk2MDUxMjk0MzMwMTY1DQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KW2NyeXB0dXA6bGluazpkcmFmdF9jb21wb3NlOnItODkwOTg2MDQyNTg3Mzg5ODczMF0NCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgNy4wLjIgR21haWwgRW5jcnlwdGlvbg0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3Y0ZNQTB0YUwvem1MWlVCQVJBQWlSRE1nMWg1YjNJeEU5YmxmbVZyS2hwSis2WDYyZjlZclFDQ0FaK0oNCjdraUJWeFppaWgrbmYvK1RWREFpMXpLa083OEVxTEoyQ2Q4SlBmUmtYbnRtd2IwSldNaGE4a1lGcmZMbQ0KbUxYWGxwOVoxVGRldkhJa2RLdTUvY0kwOVArdHU3YjNKL0twRUI5d3dQN3FjSlBPdkdEekdHeWJ2eElEDQpTWG9iU1VxbjlkUGlxMC9tVTJHamVWMWMzQkVxSHVORWNEaVdwUE5wOHdQOEp2UE1uVUw0OTFEaExpVmENCk1keVJvaGI3YVUwZUx1ZW5URFpuQktaZXhlWEpkNklOY1h4dzRDTFlOOFRGc1N2emRTQWVJSm95QlZ6Tg0KQU93K1JTVHpUTFZhUGtZV1NZUzRFK2VRT25tcVR5V3A3SEtRa25FQ0J2SlA4YlI3NUo4dEhtUzdnOUUzDQpKV0thaVpVT2VHbkJJTURpOEFZMlNDN0luVi9kRzY2Ynhxb0lEMG83a293VWFwQXY3TDlGSm1HRTM3VUkNCnJFWnNkWmozUkIvUm9uSmV5OVRpSm51Rm1TUUllQkNxU09QN3ZOZ3l2d0lyRGJNOWhZaHFJd1FXRXNCUA0KblN5TzJDV3J1dnRuSUoyeHJHVGZDa3pkdEMrOG5tL3NJL3R6VnhsUlR1aW1RdE43UTc1ZlpBQzZhdlNNDQpoRWZmQTg4WVB2ZVpnd1VtK3pmTUEyZVBxQ0ZaZnNGV1ZjWjR3VXc0RGpXUkRwbE9Bd21MTGt5ZGZyQjkNCndWdlBaeU81d1F4ckc2ak82d3FzcVhsNDlpQVVWaU0zSE1NTkZqNzR1dUFSTUF1dVpCMytUVkJ5bnhsbw0KUWZvVXI1T0xFNUJKdGVDb0FNVEpYWVNCRGlHUlZvR3J1K3hjOXRLSnJMRFNWZ0VkTWtNWE12UDU3amlqDQpSUW9yb3Zkdm9YRjVlUlVadkhUU2JvTTRIYWZNS0lPdmFSd3E3S2RoQU1TNElIL1JZWVZnSW1DY3FRN2gNCjU3T3ZyUUxONmxSdnFDRHFXTW13Wk1GNFV5eEVqenJpc2d5dVQ5R3oNCj0zREtTdDcNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY5NDg3NTAxMTM2MC41OTk2MDUxMjk0MzMwMTY1LS0NCg==" + } + }, + { + "id": "draft-3", + "message": { + "id": "16cfb0e25821733b", + "threadId": "16cfb0e25821733b", + "labelIds": ["DRAFT"], + "snippet": "[cryptup:link:draft_reply:16cfa9001baaac0a] -----BEGIN PGP MESSAGE----- Version: FlowCrypt 6.9.9 Gmail Encryption Comment: Seamlessly send and receive encrypted email wcFMA0taL/zmLZUBAQ/", + "historyId": "1044100", + "internalDate": "1567580104000", + "payload": { + "mimeType": "multipart/mixed", + "headers": [ + { "name": "Received", "value": "from 717284730244 named unknown by gmailapi.google.com with HTTPREST; Tue, 3 Sep 2019 23:55:04 -0700" }, + { "name": "Content-Type", "value": "multipart/mixed; boundary=\"----sinikael-?=_1-15675801039490.1727867765228115\"" }, + { "name": "To", "value": "flowcryptcompatibility@gmail.com" }, + { "name": "From", "value": "flowcrypt.compatibility@gmail.com" }, + { "name": "Subject", "value": "Re: Test Draft Save" }, + { "name": "Date", "value": "Tue, 3 Sep 2019 23:55:04 -0700" }, + { "name": "Message-Id", "value": "" }, + { "name": "MIME-Version", "value": "1.0" } + ], + "parts": [ + { + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + }] + }, + "raw": "UmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCVR1ZSwgMyBTZXAgMjAxOSAyMzo1NTowNCAtMDcwMA0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7DQogYm91bmRhcnk9Ii0tLS1zaW5pa2FlbC0_PV8xLTE1Njc1ODAxMDM5NDkwLjE3Mjc4Njc3NjUyMjgxMTUiDQpUbzogZmxvd2NyeXB0Y29tcGF0aWJpbGl0eUBnbWFpbC5jb20NCkZyb206IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KU3ViamVjdDogUmU6IFRlc3QgRHJhZnQgU2F2ZQ0KRGF0ZTogVHVlLCAzIFNlcCAyMDE5IDIzOjU1OjA0IC0wNzAwDQpNZXNzYWdlLUlkOiA8Q0FLYnVMVG9KaWlya0h1YVRfSERMcmRSMVlwR1NaZVpfNG56S0p1XzBmY2Fkdj11R2hnQG1haWwuZ21haWwuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY3NTgwMTAzOTQ5MC4xNzI3ODY3NzY1MjI4MTE1DQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KW2NyeXB0dXA6bGluazpkcmFmdF9yZXBseToxNmNmYTkwMDFiYWFhYzBhXQ0KDQotLS0tLUJFR0lOIFBHUCBNRVNTQUdFLS0tLS0NClZlcnNpb246IEZsb3dDcnlwdCA2LjkuOSBHbWFpbCBFbmNyeXB0aW9uDQpDb21tZW50OiBTZWFtbGVzc2x5IHNlbmQgYW5kIHJlY2VpdmUgZW5jcnlwdGVkIGVtYWlsDQoNCndjRk1BMHRhTC96bUxaVUJBUS84Q0NtWWl2TVk0ckhoMUZOTDcwM1NvNkZia3ZTdDRnMEU4MnJHVDZIQg0KWnM2Nm1VQzBVWWI0bUF1VW9IazYyQ1F6TmtqZjJxbXBnRUNtN1VBZXNFdVdQZFljZ0dhMm0xVmtTSzVHDQo3ajd0UGl0S2RXMVlQZzFPeDE5cnhGbkxZQ2h6QnVOL1BqQk02ZG92dVl4SnZGVzBudGN6MEZkbVFTL0MNCkNiVklRViszMVBFREg1YXd1Wit6WlFlM2hUM1diRVo4N3RRTW9ESFZSQzhjdGVhZ3NoVFZPbldhb3h6RA0Ka3ZGajVENUEwWDljUXJ6Q3JwSnhYZFZvTmJBUng0UmFQbTR0K3ZGN29lSU1SRjBYWWxSWExmRlJKejRNDQo3WmhTTlNsNWNHRndTaEZxWlRoN3JDWlkxZGduUGtxS21PSkQyMDFHMmd0MjF0amViWnJoc3Y5K1cxSm4NCk5PTUkzSzFSMlNKMlVNWG9yYnhIaG5TNjBvWGxUc0hUTkdUWDFZd1BGeElpWkJzdjc2OG1LZDhBY3liTg0KSXRJMVEyd1FFWlVJekxYdTlxZmJ3V2owMGFmNkM0YUdVcW5RMkJReE9CenFHbHU0TU4zUjg3SUJOYUpsDQpmKzdYSmFjaGYvWUpJa284SlN4cnR6T1BBRnlOcmxjWURVZHdhQzBhTEUwY09CVXFkRW9sQkxiY1FhdWUNCi9sbGRRSnJreVdFZnNTVk1qWWwyeEhRdXlnZGhleW9sOTlKdlM3Y0E5YkJLN3lteXdoMm1XdVpGY1NNMQ0KTUdBeXBBdE9wZEV2bnJjeVZ0TVEyWGVwcDFJMGJCVTJGRzNOVWhsemRzS3dHVVdPUTB0QWlLeUkxQ3hKDQppMzZVVC9ud25CdnRqcE1PeTFyQTNEbUFrZjNsbXA2SHAyRUJyMnc2YWdqU3dBWUJlamhUYXliVmhsY3QNCmZFNW5GaGZvSWtTWXh4S1dIaU5uK2dHVGJ3THRqbVl2L2U4dGI1UmxUdHdzM2QyTUZBcVZXVVUzRlU5VQ0KRVV3K0FaT2ozZExkcVNWYTVXMEpmQ21KcU5jS0Z1QlFQWkdlL0ZlRE5hS24wYkhUTHdlekNKSEhrbDdNDQpXWFNhYTViaUpkVzR6RTNFQU95c2dGNFRmTmg0Q0pWR0ZRM2puRjhPVDUrVy9MVFZkWUwrT3pqa092MWkNCi9Sdnd2WHhBYkFFVG03V09zMkxTZnhMVGdNQVY3b0lPLzhBc0F2VVpHaUszSlh0Mm0zdkZvaVZVQjJKdA0KUkFtSEtPakhaMHc9M0QNCj0zRDUyN1YNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNTY3NTgwMTAzOTQ5MC4xNzI3ODY3NzY1MjI4MTE1LS0NCg==" + } + }], + messages: [] + } }; export default data; diff --git a/test/source/mock.ts b/test/source/mock.ts index d7fe770048d..a3c336240d2 100644 --- a/test/source/mock.ts +++ b/test/source/mock.ts @@ -1,5 +1,5 @@ -import { startGoogleApiMock } from './mock/google-api-mock'; +import { startAllApisMock } from './mock/all-apis-mock'; import { Config } from './util'; import * as request from 'fc-node-requests'; import { writeFileSync, existsSync } from 'fs'; @@ -26,7 +26,7 @@ export const mock = async (logger: (line: string) => void) => { } })); console.info(`checking mock data took ${(Date.now() - start) / 1000} seconds`); - return await startGoogleApiMock(logger); + return await startAllApisMock(logger); }; if (require.main === module) { diff --git a/test/source/mock/all-apis-mock.ts b/test/source/mock/all-apis-mock.ts new file mode 100644 index 00000000000..b8de4c2b1d6 --- /dev/null +++ b/test/source/mock/all-apis-mock.ts @@ -0,0 +1,27 @@ +/* © 2016-2018 FlowCrypt Limited. Limitations apply. Contact human@flowcrypt.com */ + +'use strict'; + +import { Api, Handlers } from './lib/api'; +import * as http from 'http'; +import { mockGoogleEndpoints } from './google/google-endpoints'; +import { mockBackendEndpoints } from './backend/backend-endpoints'; + +export type HandlersDefinition = Handlers<{ query: { [k: string]: string; }; body?: unknown; }, unknown>; + +export const startAllApisMock = async (logger: (line: string) => void) => { + class LoggedApi extends Api { + protected log = (req: http.IncomingMessage, res: http.ServerResponse, errRes?: Buffer) => { + if (req.url !== '/favicon.ico') { + logger(`${res.statusCode} ${req.method} ${req.url} | ${errRes ? errRes : ''}`); + } + } + } + const api = new LoggedApi<{ query: { [k: string]: string }, body?: unknown }, unknown>('google-mock', { + ...mockGoogleEndpoints, + ...mockBackendEndpoints, + '/favicon.ico': async () => '', + }); + await api.listen(8001); + return api; +}; diff --git a/test/source/mock/backend/backend-data.ts b/test/source/mock/backend/backend-data.ts new file mode 100644 index 00000000000..7e036dc2eaf --- /dev/null +++ b/test/source/mock/backend/backend-data.ts @@ -0,0 +1,56 @@ +import { OauthMock } from '../lib/oauth'; +import { Dict } from '../../core/common'; +import { HttpAuthErr } from '../lib/api'; + +// tslint:disable:no-null-keyword + +export class BackendData { + + private uuidsByAcctEmail: Dict = {}; + + constructor(private oauth: OauthMock) { } + + registerOrThrow = (acct: string, uuid: string, idToken: string) => { + if (!this.oauth.isIdTokenValid(idToken)) { + throw new HttpAuthErr(`Could not verify mock idToken: ${idToken}`); + } + if (!this.uuidsByAcctEmail[acct]) { + this.uuidsByAcctEmail[acct] = []; + } + this.uuidsByAcctEmail[acct].push(uuid); + } + + checkUuidOrThrow = (acct: string, uuid: string) => { + if (!(this.uuidsByAcctEmail[acct] || []).includes(uuid)) { + throw new HttpAuthErr(`Wrong mock uuid ${uuid} for acct ${acct}`); + } + } + + getAcctRow = (acct: string) => { + return { + 'email': acct, + 'alias': null, + 'name': 'mock name', + 'photo': null, + 'photo_circle': true, + 'web': null, + 'phone': null, + 'intro': null, + 'default_message_expire': 3, + 'token': '1234-mock-acct-token', + }; + } + + getSubscription = (acct: string) => { + return { level: null, expire: null, method: null, expired: null }; + } + + getOrgRules = (acct: string) => { + const domain = acct.split('@')[1]; + if (domain === 'org-rules-test.flowcrypt.com') { + return { "flags": ["NO_PRV_CREATE", "NO_PRV_BACKUP", "ENFORCE_ATTESTER_SUBMIT"] }; + } + return { 'flags': [] }; + } + +} diff --git a/test/source/mock/backend/backend-endpoints.ts b/test/source/mock/backend/backend-endpoints.ts new file mode 100644 index 00000000000..d8fb29afdcf --- /dev/null +++ b/test/source/mock/backend/backend-endpoints.ts @@ -0,0 +1,85 @@ +import { isPost } from '../lib/mock-util'; +import { HttpClientErr, HttpAuthErr } from '../lib/api'; +import { HandlersDefinition } from '../all-apis-mock'; +import { IncomingMessage } from 'http'; +import * as request from 'fc-node-requests'; +import { oauth } from '../lib/oauth'; +import { BackendData } from './backend-data'; +import { Dict } from '../../core/common'; + +const backendData = new BackendData(oauth); + +const fwdToRealBackend = async (parsed: any, req: IncomingMessage): Promise => { + delete req.headers.host; + delete req.headers['content-length']; + const forwarding: any = { headers: req.headers, url: `https://flowcrypt.com${req.url}` }; + if (req.url!.includes('message/upload')) { + forwarding.body = parsed.body; // FORM-DATA + const r = await request.post(forwarding); + return r.body; // already json-stringified for this call, maybe because backend doesn't return proper content-type + } + forwarding.json = parsed.body; // JSON + const r = await request.post(forwarding); + return JSON.stringify(r.body); +}; + +export const mockBackendEndpoints: HandlersDefinition = { + '/api/account/login': async ({ body }, req) => { + const parsed = throwIfNotPostWithAuth(body, req); + const idToken = req.headers.authorization?.replace(/^Bearer /, ''); + if (!idToken) { + throw new HttpClientErr('backend mock: Missing id_token'); + } + backendData.registerOrThrow(parsed.account, parsed.uuid, idToken); + return JSON.stringify({ + registered: true, + verified: true, + subscription: backendData.getSubscription(parsed.account), + }); + }, + '/api/account/get': async ({ body }, req) => { + const parsed = throwIfNotPostWithAuth(body, req); + backendData.checkUuidOrThrow(parsed.account, parsed.uuid); + return JSON.stringify({ + account: backendData.getAcctRow(parsed.account), + subscription: backendData.getSubscription(parsed.account), + domain_org_rules: backendData.getOrgRules(parsed.account), + }); + }, + '/api/account/update': async ({ body }, req) => { + const parsed = throwIfNotPostWithAuth(body, req); + throw new Error(`${req.url} mock not implemented`); + }, + '/api/account/subscribe': async ({ body }, req) => { + const parsed = throwIfNotPostWithAuth(body, req); + throw new Error(`${req.url} mock not implemented`); + }, + '/api/message/token': async ({ body }, req) => { + const parsed = throwIfNotPostWithAuth(body, req); + throw new Error(`${req.url} mock not implemented`); // will have to give fake token + }, + '/api/help/error': async ({ body }, req) => { + console.error(`/help/error`, body); // todo - fail tests if received any error + throw new Error(`${req.url} mock not implemented`); + }, + '/api/help/feedback': fwdToRealBackend, + '/api/message/presign_files': fwdToRealBackend, + '/api/message/confirm_files': fwdToRealBackend, + '/api/message/upload': fwdToRealBackend, + '/api/link/message': fwdToRealBackend, + '/api/link/me': fwdToRealBackend, +}; + +const throwIfNotPostWithAuth = (body: unknown, req: IncomingMessage) => { + const parsed = body as Dict; + if (!isPost(req)) { + throw new HttpClientErr('Backend mock calls must use POST method'); + } + if (!parsed.account) { + throw new HttpAuthErr('Backend mock call missing value: account'); + } + if (!parsed.uuid) { + throw new HttpAuthErr('Backend mock call missing value: uuid'); + } + return parsed; +}; diff --git a/test/source/mock/google-api-mock.ts b/test/source/mock/google-api-mock.ts deleted file mode 100644 index e4dd43391ab..00000000000 --- a/test/source/mock/google-api-mock.ts +++ /dev/null @@ -1,273 +0,0 @@ -/* © 2016-2018 FlowCrypt Limited. Limitations apply. Contact human@flowcrypt.com */ - -'use strict'; - -import { Api, HttpClientErr, Status } from './api'; -import { IncomingMessage } from 'http'; -import { OauthMock } from './oauth'; -import { Data } from './data'; -import { ParsedMail } from "mailparser"; -import * as http from 'http'; -import Parse, { ParseMsgResult } from '../util/parse'; -import { DraftSaveModel } from './types'; -import { TestBySubjectStrategyContext } from './strategies/send-message-strategy'; -import { UnsuportableStrategyError } from './strategies/strategy-base'; - -const oauth = new OauthMock(); - -const isGet = (r: IncomingMessage) => r.method === 'GET' || r.method === 'HEAD'; -const isPost = (r: IncomingMessage) => r.method === 'POST'; -const isPut = (r: IncomingMessage) => r.method === 'PUT'; -const isDelete = (r: IncomingMessage) => r.method === 'DELETE'; -const parseResourceId = (url: string) => url.match(/\/([a-zA-Z0-9\-_]+)(\?|$)/)![1]; -const allowedRecipients: Array = ['flowcrypt.compatibility@gmail.com', 'human+manualcopypgp@flowcrypt.com', - 'censored@email.com', 'test@email.com', 'human@flowcrypt.com', 'human+nopgp@flowcrypt.com', 'expired.on.attester@domain.com', - 'test.ci.compose@org.flowcrypt.com']; - -export const startGoogleApiMock = async (logger: (line: string) => void) => { - class LoggedApi extends Api { - protected log = (req: http.IncomingMessage, res: http.ServerResponse, errRes?: Buffer) => { - if (req.url !== '/favicon.ico') { - logger(`${res.statusCode} ${req.method} ${req.url} | ${errRes ? errRes : ''}`); - } - } - } - const api = new LoggedApi<{ query: { [k: string]: string }, body?: unknown }, unknown>('google-mock', { - '/o/oauth2/auth': async ({ query: { client_id, response_type, access_type, state, redirect_uri, scope, login_hint, result } }, req) => { - if (isGet(req) && client_id === oauth.clientId && response_type === 'code' && access_type === 'offline' && state && redirect_uri === oauth.redirectUri && scope) { // auth screen - if (!login_hint) { - return oauth.consentChooseAccountPage(req.url!); - } else if (!result) { - return oauth.consentPage(req.url!, login_hint); - } else { - return oauth.consentResultPage(login_hint, state, result); - } - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/oauth2/v4/token': async ({ query: { grant_type, refreshToken, client_id, code, redirect_uri } }, req) => { - if (isPost(req) && grant_type === 'authorization_code' && code && client_id === oauth.clientId) { // auth code from auth screen gets exchanged for access and refresh tokens - return oauth.getRefreshTokenResponse(code); - } else if (isPost(req) && grant_type === 'refresh_token' && refreshToken && client_id === oauth.clientId) { // here also later refresh token gets exchanged for access token - return oauth.getAccessTokenResponse(refreshToken); - } - throw new Error(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/oauth2/v1/tokeninfo': async ({ query: { access_token } }, req) => { - oauth.checkAuthorizationHeader(`Bearer ${access_token}`); - if (isGet(req)) { - return { issued_to: 'issued_to', audience: 'audience', scope: 'scope', expires_in: oauth.expiresIn, access_type: 'offline' }; - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/m8/feeds/contacts/default/thin': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req) && acct === 'test.ci.compose@org.flowcrypt.com') { - return { - feed: { - entry: [ - { gd$email: [{ address: 'contact.test@flowcrypt.com', primary: "true" }] } - ] - } - }; - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/settings/sendAs': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req)) { - // tslint:disable-next-line:max-line-length - const sendAs = [{ sendAsEmail: acct, displayName: 'First Last', replyToAddress: acct, signature: '', isDefault: true, isPrimary: true, treatAsAlias: false, verificationStatus: 'accepted' }]; - if (acct === 'flowcrypt.compatibility@gmail.com') { - sendAs[0].signature = 'The best footer ever!'; - const alias = 'flowcryptcompatibility@gmail.com'; - // tslint:disable-next-line:max-line-length - sendAs.push({ sendAsEmail: alias, displayName: 'An Alias', replyToAddress: alias, signature: '', isDefault: false, isPrimary: false, treatAsAlias: false, verificationStatus: 'accepted' }); - } - return { sendAs }; - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/messages': async ({ query: { q } }, req) => { // search messages - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req) && q) { - const msgs = new Data(acct).searchMessages(q); - return { messages: msgs.map(({ id, threadId }) => ({ id, threadId })), resultSizeEstimate: msgs.length }; - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/messages/?': async ({ query: { format } }, req) => { // get msg or attachment - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req)) { - const id = parseResourceId(req.url!); - const data = new Data(acct); - if (req.url!.includes('/attachments/')) { - const att = data.getAttachment(id); - if (att) { - return att; - } - throw new HttpClientErr(`MOCK attachment not found for ${acct}: ${id}`, Status.NOT_FOUND); - } - const msg = data.getMessage(id); - if (msg) { - return Data.fmtMsg(msg, format); - } - throw new HttpClientErr(`MOCK Message not found for ${acct}: ${id}`, Status.NOT_FOUND); - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/labels': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req)) { - return { labels: new Data(acct).getLabels() }; - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/threads': async ({ query: { labelIds, includeSpamTrash } }, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req)) { - const threads = new Data(acct).getThreads(); - return { threads, resultSizeEstimate: threads.length }; - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/threads/?': async ({ query: { format } }, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req) && (format === 'metadata' || format === 'full')) { - const id = parseResourceId(req.url!); - const msgs = new Data(acct).getMessagesByThread(id); - if (!msgs.length) { - const statusCode = id === '16841ce0ce5cb74d' ? 404 : 400; // intentionally testing missing thread - throw new HttpClientErr(`MOCK thread not found for ${acct}: ${id}`, statusCode); - } - return { id, historyId: msgs[0].historyId, messages: msgs.map(m => Data.fmtMsg(m, format)) }; - } - }, - '/upload/gmail/v1/users/me/messages/send?uploadType=multipart': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isPost(req)) { - if (parsedReq.body && typeof parsedReq.body === 'string') { - const parseResult = await parseMultipartDataAsMimeMsg(parsedReq.body); - await validateMimeMsg(acct, parseResult.mimeMsg, parseResult.threadId); - try { - const testingStrategyContext = new TestBySubjectStrategyContext(parseResult.mimeMsg.subject); - await testingStrategyContext.test(parseResult.mimeMsg); - } catch (e) { - if (!(e instanceof UnsuportableStrategyError)) { // No such strategy for test - throw e; - } - } - return { id: 'fakesendid', labelIds: ['SENT'], threadId: parseResult.threadId }; - } - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/drafts': async (parsedReq, req) => { - if (isPost(req)) { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - const body = parsedReq.body as DraftSaveModel; - if (body && body.message && body.message.raw - && typeof body.message.raw === 'string') { - if (body.message.threadId && !new Data(acct).getThreads().find(t => t.id === body.message.threadId)) { - throw new HttpClientErr('The thread you are replying to not found', 404); - } - return { - id: 'mockfakedraftsave', message: { - id: 'mockfakedmessageraftsave', - labelIds: ['DRAFT'], - threadId: body.message.threadId - } - }; - } - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/gmail/v1/users/me/drafts/?': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); - if (isGet(req)) { - const id = parseResourceId(req.url!); - const data = new Data(acct); - const draft = data.getDraft(id); - if (draft) { - return draft; - } - throw new HttpClientErr(`MOCK draft not found for ${acct} (draftId: ${id})`, Status.NOT_FOUND); - } else if (isPut(req)) { - return {}; - } else if (isDelete(req)) { - return {}; - } - throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); - }, - '/favicon.ico': async () => '', - }); - await api.listen(8001); - return api; -}; - -const parseMultipartDataAsMimeMsg = async (multipartData: string): Promise => { - let parsed: ParseMsgResult; - try { - parsed = await Parse.strictParse(multipartData); - } catch (e) { - if (e instanceof Error) { - throw new HttpClientErr(e.message, 400); - } - throw new HttpClientErr('Unknown error', 500); - } - return parsed; -}; - -const validateMimeMsg = async (acct: string, mimeMsg: ParsedMail, threadId?: string) => { - const inReplyToMessageId = mimeMsg.headers.get('in-reply-to') ? mimeMsg.headers.get('in-reply-to')!.toString() : ''; - if (threadId) { - const messages = new Data(acct).getMessagesByThread(threadId); - if (!messages || !messages.length) { - throw new HttpClientErr(`Error: The thread you are replying (${threadId}) to not found`, 404); - } - if (inReplyToMessageId) { - let isMessageExists = false; - for (const message of messages) { - if (message.raw) { - const parsedMimeMsg = await Parse.convertBase64ToMimeMsg(message.raw); - if (parsedMimeMsg.messageId === inReplyToMessageId) { - isMessageExists = true; - break; - } - } - } - if (!isMessageExists) { - throw new HttpClientErr(`Error: suplied In-Reply-To header (${inReplyToMessageId}) does not match any messages present in the mock data for thread ${threadId}`, 400); - } - } else { - throw new HttpClientErr(`Error: 'In-Reply-To' must not be empty if there is 'threadId'(${threadId})`, 400); - } - } - if (!mimeMsg.subject) { - throw new HttpClientErr('Error: Subject line is required', 400); - } else { - if (['Re: ', 'Fwd: '].some(e => mimeMsg.subject.startsWith(e)) && (!threadId || !inReplyToMessageId)) { - throw new HttpClientErr(`Error: Incorrect subject. Subject can't start from 'Re:' or 'Fwd:'. Current subject is '${mimeMsg.subject}'`, 400); - } else if ((threadId || inReplyToMessageId) && !['Re: ', 'Fwd: '].some(e => mimeMsg.subject.startsWith(e))) { - throw new HttpClientErr("Error: Incorrect subject. Subject must start from 'Re:' or 'Fwd:' " + - `if the message has threaId or 'In-Reply-To' header. Current subject is '${mimeMsg.subject}'`, 400); - } - // Special check for 'compose[global:compatibility] - standalone - from alias' test - if (mimeMsg.subject.endsWith('from alias') && mimeMsg.from.value[0].address !== 'flowcryptcompatibility@gmail.com') { - throw new HttpClientErr(`Error: Incorrect Email Alias. Should be 'flowcryptcompatibility@gmail.com'. Current '${mimeMsg.from.value[0].address}'`); - } - } - if (!mimeMsg.text) { - throw new HttpClientErr('Error: Message body is required', 400); - } - if (!mimeMsg.to.value.length || mimeMsg.to.value.find(em => !allowedRecipients.includes(em.address))) { - throw new HttpClientErr('Error: You can\'t send a message to unexisting email address(es)'); - } - const aliases = [acct]; - if (acct === 'flowcrypt.compatibility@gmail.com') { - aliases.push('flowcryptcompatibility@gmail.com'); - } - if (!mimeMsg.from.value.length || mimeMsg.from.value.find(em => !aliases.includes(em.address))) { - throw new HttpClientErr('You can\'t send a message from unexisting email address(es)'); - } -}; diff --git a/test/source/mock/data.ts b/test/source/mock/google/google-data.ts similarity index 95% rename from test/source/mock/data.ts rename to test/source/mock/google/google-data.ts index aa6fd3054b4..dd80f651d5f 100644 --- a/test/source/mock/data.ts +++ b/test/source/mock/google/google-data.ts @@ -1,7 +1,7 @@ -import { Util } from './../util/index'; +import { Util } from '../../util/index'; import { ParsedMail } from 'mailparser'; import { readFileSync } from 'fs'; -import UserMessages from '../../samples/mock-data'; +import UserMessages from '../../../samples/mock-data'; type GmailMsg$header = { name: string, value: string }; type GmailMsg$payload$body = { attachmentId: string, size: number, data?: string }; @@ -21,7 +21,8 @@ type Label = { id: string, name: "CATEGORY_SOCIAL", messageListVisibility: "hide type AcctDataFile = { messages: GmailMsg[]; drafts: GmailDraft[], attachments: { [id: string]: { data: string, size: number } }, labels: Label[] }; const DATA: { [acct: string]: AcctDataFile } = {}; -export class Data { + +export class GoogleData { private exludePplSearchQuery = /(?:-from|-to):"?([a-zA-Z0-9@.\-_]+)"?/g; private includePplSearchQuery = /(?:from|to):"?([a-zA-Z0-9@.\-_]+)"?/g; @@ -94,14 +95,14 @@ export class Data { private searchMessagesBySubject = (subject: string) => { subject = subject.trim().toLowerCase(); - return DATA[this.acct].messages.filter(m => Data.msgSubject(m).toLowerCase().includes(subject)); + return DATA[this.acct].messages.filter(m => GoogleData.msgSubject(m).toLowerCase().includes(subject)); } private searchMessagesByPeople = (includePeople: string[], excludePeople: string[]) => { includePeople = includePeople.map(person => person.trim().toLowerCase()); excludePeople = excludePeople.map(person => person.trim().toLowerCase()); return DATA[this.acct].messages.filter(m => { - const msgPeople = Data.msgPeople(m).toLowerCase(); + const msgPeople = GoogleData.msgPeople(m).toLowerCase(); let shouldInclude = false; let shouldExclude = false; if (includePeople.length) { // filter who to include @@ -142,7 +143,7 @@ export class Data { public getThreads = () => { const threads: GmailThread[] = []; - for (const thread of DATA[this.acct].messages.map(m => ({ historyId: m.historyId, id: m.threadId!, snippet: `MOCK SNIPPET: ${Data.msgSubject(m)}` }))) { + for (const thread of DATA[this.acct].messages.map(m => ({ historyId: m.historyId, id: m.threadId!, snippet: `MOCK SNIPPET: ${GoogleData.msgSubject(m)}` }))) { if (!threads.map(t => t.id).includes(thread.id)) { threads.push(thread); } diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts new file mode 100644 index 00000000000..7a02f201975 --- /dev/null +++ b/test/source/mock/google/google-endpoints.ts @@ -0,0 +1,251 @@ +import { TestBySubjectStrategyContext } from './strategies/send-message-strategy'; +import { UnsuportableStrategyError } from './strategies/strategy-base'; +import { isGet, isPost, parseResourceId, isPut, isDelete } from '../lib/mock-util'; +import { HttpClientErr, Status } from '../lib/api'; +import { GoogleData } from './google-data'; +import { ParsedMail } from 'mailparser'; +import Parse, { ParseMsgResult } from '../../util/parse'; +import { HandlersDefinition } from '../all-apis-mock'; +import { oauth } from '../lib/oauth'; + +type DraftSaveModel = { message: { raw: string, threadId: string } }; + +const allowedRecipients: Array = ['flowcrypt.compatibility@gmail.com', 'human+manualcopypgp@flowcrypt.com', + 'censored@email.com', 'test@email.com', 'human@flowcrypt.com', 'human+nopgp@flowcrypt.com', 'expired.on.attester@domain.com', + 'test.ci.compose@org.flowcrypt.com']; + +export const mockGoogleEndpoints: HandlersDefinition = { + '/o/oauth2/auth': async ({ query: { client_id, response_type, access_type, state, redirect_uri, scope, login_hint, result } }, req) => { + if (isGet(req) && client_id === oauth.clientId && response_type === 'code' && access_type === 'offline' && state && redirect_uri === oauth.redirectUri && scope) { // auth screen + if (!login_hint) { + return oauth.consentChooseAccountPage(req.url!); + } else if (!result) { + return oauth.consentPage(req.url!, login_hint); + } else { + return oauth.consentResultPage(login_hint, state, result); + } + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/oauth2/v4/token': async ({ query: { grant_type, refreshToken, client_id, code, redirect_uri } }, req) => { + if (isPost(req) && grant_type === 'authorization_code' && code && client_id === oauth.clientId) { // auth code from auth screen gets exchanged for access and refresh tokens + return oauth.getRefreshTokenResponse(code); + } else if (isPost(req) && grant_type === 'refresh_token' && refreshToken && client_id === oauth.clientId) { // here also later refresh token gets exchanged for access token + return oauth.getAccessTokenResponse(refreshToken); + } + throw new Error(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/oauth2/v1/tokeninfo': async ({ query: { access_token } }, req) => { + oauth.checkAuthorizationHeader(`Bearer ${access_token}`); + if (isGet(req)) { + return { issued_to: 'issued_to', audience: 'audience', scope: 'scope', expires_in: oauth.expiresIn, access_type: 'offline' }; + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/m8/feeds/contacts/default/thin': async (parsedReq, req) => { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req) && acct === 'test.ci.compose@org.flowcrypt.com') { + return { + feed: { + entry: [ + { gd$email: [{ address: 'contact.test@flowcrypt.com', primary: "true" }] } + ] + } + }; + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/settings/sendAs': async (parsedReq, req) => { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req)) { + // tslint:disable-next-line:max-line-length + const sendAs = [{ sendAsEmail: acct, displayName: 'First Last', replyToAddress: acct, signature: '', isDefault: true, isPrimary: true, treatAsAlias: false, verificationStatus: 'accepted' }]; + if (acct === 'flowcrypt.compatibility@gmail.com') { + sendAs[0].signature = 'The best footer ever!'; + const alias = 'flowcryptcompatibility@gmail.com'; + // tslint:disable-next-line:max-line-length + sendAs.push({ sendAsEmail: alias, displayName: 'An Alias', replyToAddress: alias, signature: '', isDefault: false, isPrimary: false, treatAsAlias: false, verificationStatus: 'accepted' }); + } + return { sendAs }; + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/messages': async ({ query: { q } }, req) => { // search messages + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req) && q) { + const msgs = new GoogleData(acct).searchMessages(q); + return { messages: msgs.map(({ id, threadId }) => ({ id, threadId })), resultSizeEstimate: msgs.length }; + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/messages/?': async ({ query: { format } }, req) => { // get msg or attachment + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req)) { + const id = parseResourceId(req.url!); + const data = new GoogleData(acct); + if (req.url!.includes('/attachments/')) { + const att = data.getAttachment(id); + if (att) { + return att; + } + throw new HttpClientErr(`MOCK attachment not found for ${acct}: ${id}`, Status.NOT_FOUND); + } + const msg = data.getMessage(id); + if (msg) { + return GoogleData.fmtMsg(msg, format); + } + throw new HttpClientErr(`MOCK Message not found for ${acct}: ${id}`, Status.NOT_FOUND); + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/labels': async (parsedReq, req) => { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req)) { + return { labels: new GoogleData(acct).getLabels() }; + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/threads': async ({ query: { labelIds, includeSpamTrash } }, req) => { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req)) { + const threads = new GoogleData(acct).getThreads(); + return { threads, resultSizeEstimate: threads.length }; + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/threads/?': async ({ query: { format } }, req) => { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req) && (format === 'metadata' || format === 'full')) { + const id = parseResourceId(req.url!); + const msgs = new GoogleData(acct).getMessagesByThread(id); + if (!msgs.length) { + const statusCode = id === '16841ce0ce5cb74d' ? 404 : 400; // intentionally testing missing thread + throw new HttpClientErr(`MOCK thread not found for ${acct}: ${id}`, statusCode); + } + return { id, historyId: msgs[0].historyId, messages: msgs.map(m => GoogleData.fmtMsg(m, format)) }; + } + }, + '/upload/gmail/v1/users/me/messages/send?uploadType=multipart': async (parsedReq, req) => { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isPost(req)) { + if (parsedReq.body && typeof parsedReq.body === 'string') { + const parseResult = await parseMultipartDataAsMimeMsg(parsedReq.body); + await validateMimeMsg(acct, parseResult.mimeMsg, parseResult.threadId); + try { + const testingStrategyContext = new TestBySubjectStrategyContext(parseResult.mimeMsg.subject); + await testingStrategyContext.test(parseResult.mimeMsg); + } catch (e) { + if (!(e instanceof UnsuportableStrategyError)) { // No such strategy for test + throw e; + } + } + return { id: 'fakesendid', labelIds: ['SENT'], threadId: parseResult.threadId }; + } + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/drafts': async (parsedReq, req) => { + if (isPost(req)) { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const body = parsedReq.body as DraftSaveModel; + if (body && body.message && body.message.raw + && typeof body.message.raw === 'string') { + if (body.message.threadId && !new GoogleData(acct).getThreads().find(t => t.id === body.message.threadId)) { + throw new HttpClientErr('The thread you are replying to not found', 404); + } + return { + id: 'mockfakedraftsave', message: { + id: 'mockfakedmessageraftsave', + labelIds: ['DRAFT'], + threadId: body.message.threadId + } + }; + } + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, + '/gmail/v1/users/me/drafts/?': async (parsedReq, req) => { + const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req)) { + const id = parseResourceId(req.url!); + const data = new GoogleData(acct); + const draft = data.getDraft(id); + if (draft) { + return draft; + } + throw new HttpClientErr(`MOCK draft not found for ${acct} (draftId: ${id})`, Status.NOT_FOUND); + } else if (isPut(req)) { + return {}; + } else if (isDelete(req)) { + return {}; + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, +}; + +const parseMultipartDataAsMimeMsg = async (multipartData: string): Promise => { + let parsed: ParseMsgResult; + try { + parsed = await Parse.strictParse(multipartData); + } catch (e) { + if (e instanceof Error) { + throw new HttpClientErr(e.message, 400); + } + throw new HttpClientErr('Unknown error', 500); + } + return parsed; +}; + +const validateMimeMsg = async (acct: string, mimeMsg: ParsedMail, threadId?: string) => { + const inReplyToMessageId = mimeMsg.headers.get('in-reply-to') ? mimeMsg.headers.get('in-reply-to')!.toString() : ''; + if (threadId) { + const messages = new GoogleData(acct).getMessagesByThread(threadId); + if (!messages || !messages.length) { + throw new HttpClientErr(`Error: The thread you are replying (${threadId}) to not found`, 404); + } + if (inReplyToMessageId) { + let isMessageExists = false; + for (const message of messages) { + if (message.raw) { + const parsedMimeMsg = await Parse.convertBase64ToMimeMsg(message.raw); + if (parsedMimeMsg.messageId === inReplyToMessageId) { + isMessageExists = true; + break; + } + } + } + if (!isMessageExists) { + throw new HttpClientErr(`Error: suplied In-Reply-To header (${inReplyToMessageId}) does not match any messages present in the mock data for thread ${threadId}`, 400); + } + } else { + throw new HttpClientErr(`Error: 'In-Reply-To' must not be empty if there is 'threadId'(${threadId})`, 400); + } + } + if (!mimeMsg.subject) { + throw new HttpClientErr('Error: Subject line is required', 400); + } else { + if (['Re: ', 'Fwd: '].some(e => mimeMsg.subject.startsWith(e)) && (!threadId || !inReplyToMessageId)) { + throw new HttpClientErr(`Error: Incorrect subject. Subject can't start from 'Re:' or 'Fwd:'. Current subject is '${mimeMsg.subject}'`, 400); + } else if ((threadId || inReplyToMessageId) && !['Re: ', 'Fwd: '].some(e => mimeMsg.subject.startsWith(e))) { + throw new HttpClientErr("Error: Incorrect subject. Subject must start from 'Re:' or 'Fwd:' " + + `if the message has threaId or 'In-Reply-To' header. Current subject is '${mimeMsg.subject}'`, 400); + } + // Special check for 'compose[global:compatibility] - standalone - from alias' test + if (mimeMsg.subject.endsWith('from alias') && mimeMsg.from.value[0].address !== 'flowcryptcompatibility@gmail.com') { + throw new HttpClientErr(`Error: Incorrect Email Alias. Should be 'flowcryptcompatibility@gmail.com'. Current '${mimeMsg.from.value[0].address}'`); + } + } + if (!mimeMsg.text) { + throw new HttpClientErr('Error: Message body is required', 400); + } + if (!mimeMsg.to.value.length || mimeMsg.to.value.find(em => !allowedRecipients.includes(em.address))) { + throw new HttpClientErr('Error: You can\'t send a message to unexisting email address(es)'); + } + const aliases = [acct]; + if (acct === 'flowcrypt.compatibility@gmail.com') { + aliases.push('flowcryptcompatibility@gmail.com'); + } + if (!mimeMsg.from.value.length || mimeMsg.from.value.find(em => !aliases.includes(em.address))) { + throw new HttpClientErr('You can\'t send a message from unexisting email address(es)'); + } +}; diff --git a/test/source/mock/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts similarity index 94% rename from test/source/mock/strategies/send-message-strategy.ts rename to test/source/mock/google/strategies/send-message-strategy.ts index 923335b489d..53a92937f07 100644 --- a/test/source/mock/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -1,20 +1,20 @@ -import { Data } from './../data'; +import { GoogleData } from '../google-data'; import { UnsuportableStrategyError, ITestMsgStrategy } from './strategy-base.js'; import { ParsedMail, AddressObject } from 'mailparser'; -import { HttpClientErr } from '../api.js'; -import { PgpMsg } from "../../core/pgp.js"; -import { Buf } from '../../core/buf.js'; -import { Config } from '../../util/index.js'; +import { HttpClientErr } from '../../lib/api'; +import { Config } from '../../../util'; +import { PgpMsg } from '../../../core/pgp'; +import { Buf } from '../../../core/buf'; class PwdEncryptedMessageTestStrategy implements ITestMsgStrategy { test = async (mimeMsg: ParsedMail) => { if (!mimeMsg.text.match(/https:\/\/flowcrypt.com\/[a-z0-9A-Z]{10}/)) { - throw new HttpClientErr(`Error: cannot find pwd encrypted link`); + throw new HttpClientErr(`Error: cannot find pwd encrypted link in:\n\n${mimeMsg.text}`); } if (!mimeMsg.text.includes('Follow this link to open it')) { throw new HttpClientErr(`Error: cannot find pwd encrypted open link prompt in ${mimeMsg.text}`); } - new Data(mimeMsg.from.value[0].address).storeSentMessage(mimeMsg); + new GoogleData(mimeMsg.from.value[0].address).storeSentMessage(mimeMsg); } } diff --git a/test/source/mock/strategies/strategy-base.ts b/test/source/mock/google/strategies/strategy-base.ts similarity index 100% rename from test/source/mock/strategies/strategy-base.ts rename to test/source/mock/google/strategies/strategy-base.ts diff --git a/test/source/mock/api.ts b/test/source/mock/lib/api.ts similarity index 94% rename from test/source/mock/api.ts rename to test/source/mock/lib/api.ts index 6a1586a22dc..74711c15a6f 100644 --- a/test/source/mock/api.ts +++ b/test/source/mock/lib/api.ts @@ -22,7 +22,7 @@ export enum Status { } export type RequestHandler = (parsedReqBody: REQ, req: IncomingMessage) => Promise; -type Handlers = { [request: string]: RequestHandler }; +export type Handlers = { [request: string]: RequestHandler }; export class Api { @@ -196,7 +196,15 @@ export class Api { } protected parseReqBody = (body: Buffer, req: IncomingMessage): REQ => { - return { query: this.parseUrlQuery(req.url!), body: body.length ? (req.url!.startsWith('/upload/') ? body.toString() : JSON.parse(body.toString())) : undefined } as unknown as REQ; + let parsedBody: string | undefined; + if (body.length) { + if (req.url!.startsWith('/upload/') || req.url!.startsWith('/api/message/upload')) { + parsedBody = body.toString(); + } else { + parsedBody = JSON.parse(body.toString()); + } + } + return { query: this.parseUrlQuery(req.url!), body: parsedBody } as unknown as REQ; } } diff --git a/test/source/mock/lib/mock-util.ts b/test/source/mock/lib/mock-util.ts new file mode 100644 index 00000000000..019c438781c --- /dev/null +++ b/test/source/mock/lib/mock-util.ts @@ -0,0 +1,8 @@ + +import { IncomingMessage } from 'http'; + +export const isGet = (r: IncomingMessage) => r.method === 'GET' || r.method === 'HEAD'; +export const isPost = (r: IncomingMessage) => r.method === 'POST'; +export const isPut = (r: IncomingMessage) => r.method === 'PUT'; +export const isDelete = (r: IncomingMessage) => r.method === 'DELETE'; +export const parseResourceId = (url: string) => url.match(/\/([a-zA-Z0-9\-_]+)(\?|$)/)![1]; diff --git a/test/source/mock/oauth.ts b/test/source/mock/lib/oauth.ts similarity index 79% rename from test/source/mock/oauth.ts rename to test/source/mock/lib/oauth.ts index 34b74958f31..a8ca8a12027 100644 --- a/test/source/mock/oauth.ts +++ b/test/source/mock/lib/oauth.ts @@ -1,6 +1,7 @@ -import { HttpClientErr, Status } from './api'; -import { Config } from '../util'; -import { Buf } from '../core/buf'; +import { HttpClientErr, Status, HttpAuthErr } from './api'; +import { Config } from '../../util'; +import { Buf } from '../../core/buf'; +import { Str } from '../../core/common'; // tslint:disable:variable-name @@ -10,6 +11,7 @@ export class OauthMock { private refreshTokenByAuthCode: { [authCode: string]: string } = {}; private accessTokenByRefreshToken: { [refreshToken: string]: string } = {}; private acctByAccessToken: { [acct: string]: string } = {}; + private issuedIdTokensByAcct: { [acct: string]: string[] } = {}; public clientId = '717284730244-ostjo2fdtr3ka4q9td69tdr9acmmru2p.apps.googleusercontent.com'; public expiresIn = 2 * 60 * 60; // 2hrs in seconds @@ -47,7 +49,7 @@ export class OauthMock { const access_token = this.getAccessToken(refresh_token); const acct = this.acctByAccessToken[access_token]; this.checkKnownAcct(acct); - const id_token = this.getIdToken(acct); + const id_token = this.generateIdToken(acct); return { access_token, refresh_token, expires_in: this.expiresIn, id_token, token_type: 'refresh_token' }; // guessed the token_type } @@ -56,7 +58,7 @@ export class OauthMock { const access_token = this.getAccessToken(refreshToken); const acct = this.acctByAccessToken[access_token]; this.checkKnownAcct(acct); - const id_token = this.getIdToken(acct); + const id_token = this.generateIdToken(acct); return { access_token, expires_in: this.expiresIn, id_token, token_type: 'Bearer' }; } catch (e) { throw new HttpClientErr('invalid_grant', Status.BAD_REQUEST); @@ -76,7 +78,15 @@ export class OauthMock { return acct; } - private getAccessToken = (refreshToken: string): string => { + public isIdTokenValid = (idToken: string) => { // we verify mock idToken by checking if we ever issued it + const [header, data, sig] = idToken.split('.'); + const claims = JSON.parse(Buf.fromBase64UrlStr(data).toUtfStr()); + return (this.issuedIdTokensByAcct[claims.email] || []).includes(idToken); + } + + // -- private + + private getAccessToken(refreshToken: string): string { if (this.accessTokenByRefreshToken[refreshToken]) { return this.accessTokenByRefreshToken[refreshToken]; } @@ -93,7 +103,7 @@ export class OauthMock { } } - private getIdToken = (email: string) => { + private generateIdToken = (email: string): string => { const data = { at_hash: 'at_hash', exp: this.expiresIn, @@ -108,6 +118,14 @@ export class OauthMock { email, email_verified: true, }; - return `fakeheader.${Buf.fromUtfStr(JSON.stringify(data)).toBase64UrlStr()}.fakesignature`; + const newIdToken = `fakeheader.${Buf.fromUtfStr(JSON.stringify(data)).toBase64UrlStr()}.${Str.sloppyRandom(30)}`; + if (!this.issuedIdTokensByAcct[email]) { + this.issuedIdTokensByAcct[email] = []; + } + this.issuedIdTokensByAcct[email].push(newIdToken); + return newIdToken; } + } + +export const oauth = new OauthMock(); diff --git a/test/source/mock/types.ts b/test/source/mock/types.ts deleted file mode 100644 index bd913a7c508..00000000000 --- a/test/source/mock/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type DraftSaveModel = { - message: { - raw: string, - threadId: string - } -}; diff --git a/test/source/patterns.ts b/test/source/patterns.ts index b604ed05c23..a4cce45fa4f 100644 --- a/test/source/patterns.ts +++ b/test/source/patterns.ts @@ -45,13 +45,17 @@ const validateLine = (line: string, location: string) => { errsFound++; } if (line.match(/^ {2}(public |private |protected |static |async )*((?!constructor)[a-z][a-zA-Z0-9]+)\([^;]+[^>] \{$/)) { - console.error(`wrongly using class method, which can cause binding loss (use fat arrow method properties instead):\n${line}\n`); + console.error(`wrongly using class method, which can cause binding loss (use fat arrow method properties instead) #1:\n${line}\n`); errsFound++; } if (line.match(/^ {2}(public |private |protected |static )+?[a-z][a-zA-Z0-9]+ = (async )?\(.+\)(: .+)? => .+;$/)) { console.error(`don't use single-line "method = (arg) => result" class methods, give them a method body and a return statement "method = (arg) => { return result; }":\n${line}\n`); errsFound++; } + if (line.match(/^ {2}(public |private |protected |static |async )*((?!constructor)[a-z][a-zA-Z0-9]+)\([^)]*\) \{$/)) { + console.error(`wrongly using class method, which can cause binding loss (use fat arrow method properties instead) #2:\n${line}\n`); + errsFound++; + } }; const srcFilePaths = getAllFilesInDir('./extension', /\.ts$/); diff --git a/test/source/tests/page_recipe/setup-page-recipe.ts b/test/source/tests/page_recipe/setup-page-recipe.ts index 04881f7673d..5252feb5fce 100644 --- a/test/source/tests/page_recipe/setup-page-recipe.ts +++ b/test/source/tests/page_recipe/setup-page-recipe.ts @@ -4,7 +4,17 @@ import { ControllablePage } from '../../browser'; import { expect } from 'chai'; import { SettingsPageRecipe } from './settings-page-recipe'; -type ManualEnterOpts = { usedPgpBefore?: boolean, submitPubkey?: boolean, fixKey?: boolean, naked?: boolean, genPp?: boolean, simulateRetryOffline?: boolean }; +type ManualEnterOpts = { + usedPgpBefore?: boolean, + submitPubkey?: boolean, + fixKey?: boolean, + naked?: boolean, + genPp?: boolean, + simulateRetryOffline?: boolean, + noPrvCreateOrgRule?: boolean, + enforceAttesterSubmitOrgRule?: boolean, + fillOnly?: boolean, +}; export class SetupPageRecipe extends PageRecipe { @@ -54,17 +64,32 @@ export class SetupPageRecipe extends PageRecipe { public static async manualEnter( settingsPage: ControllablePage, keyTitle: string, - { usedPgpBefore = false, submitPubkey = false, fixKey = false, naked = false, genPp = false, simulateRetryOffline = false }: ManualEnterOpts = {} + { + usedPgpBefore = false, + submitPubkey = false, + fixKey = false, + naked = false, + genPp = false, + simulateRetryOffline = false, + noPrvCreateOrgRule = false, + enforceAttesterSubmitOrgRule = false, + fillOnly = false, + }: ManualEnterOpts = {} ) { const k = Config.key(keyTitle); - if (usedPgpBefore) { - await settingsPage.waitAndClick('@action-step0foundkey-choose-manual-enter', { retryErrs: true }); - } else { - await settingsPage.waitAndClick('@action-step1easyormanual-choose-manual-enter', { retryErrs: true }); + if (!noPrvCreateOrgRule) { + if (usedPgpBefore) { + await settingsPage.waitAndClick('@action-step0foundkey-choose-manual-enter', { retryErrs: true }); + } else { + await settingsPage.waitAndClick('@action-step1easyormanual-choose-manual-enter', { retryErrs: true }); + } } await settingsPage.waitAndClick('@input-step2bmanualenter-source-paste'); await settingsPage.waitAndType('@input-step2bmanualenter-ascii-key', k.armored || ''); await settingsPage.waitAndClick('@input-step2bmanualenter-passphrase'); // blur ascii key input + if (noPrvCreateOrgRule) { // NO_PRV_CREATE cannot use the back button, so that they cannot select another setup method + await settingsPage.notPresent('@action-setup-go-back'); + } if (!naked) { await Util.sleep(1); await settingsPage.notPresent('@action-step2bmanualenter-new-random-passphrase'); @@ -89,10 +114,18 @@ export class SetupPageRecipe extends PageRecipe { await settingsPage.waitAndType('@input-step2bmanualenter-passphrase', k.passphrase); } } - if (!submitPubkey) { - await settingsPage.waitAndClick('@input-step2bmanualenter-submit-pubkey'); // uncheck + if (enforceAttesterSubmitOrgRule) { + await settingsPage.notPresent('@input-step2bmanualenter-submit-pubkey'); + } else { + await settingsPage.waitAll('@input-step2bmanualenter-submit-pubkey'); + if (!submitPubkey) { + await settingsPage.waitAndClick('@input-step2bmanualenter-submit-pubkey'); // uncheck + } } await settingsPage.waitAll('@input-step2bmanualenter-save'); + if (fillOnly) { + return; + } try { if (simulateRetryOffline) { await settingsPage.page.setOfflineMode(true); // offline mode diff --git a/test/source/tests/tests/account.ts b/test/source/tests/tests/account.ts index 4121a7a6257..e9bbe6ca665 100644 --- a/test/source/tests/tests/account.ts +++ b/test/source/tests/tests/account.ts @@ -34,13 +34,8 @@ export const defineConsumerAcctTests = (testVariant: TestVariant, testWithNewBro let fileInput = await composePage.target.$('input[type=file]'); await fileInput!.uploadFile('test/samples/large.jpg'); await composePage.waitAndRespondToModal('confirm', 'confirm', 'The files are over 5 MB'); - // get a trial - log in first + // get a trial - already logged in const subscribePage = await GmailPageRecipe.getSubscribeDialog(t, gmailPage, browser); - const oauthPage = await PageRecipe.waitForModalGetTriggeredPageAfterResponding(acct, t, browser, subscribePage, 'confirm', { - contentToCheck: 'Please log in with FlowCrypt to continue', clickOn: 'confirm' - }); - await OauthPageRecipe.google(t, oauthPage, acct, 'login'); // should cause subscribePage to reload - // get a trial await subscribePage.waitAndClick('@action-get-trial', { delay: 1 }); await PageRecipe.waitForModalAndRespond(subscribePage, 'info', { contentToCheck: 'Successfully upgraded to FlowCrypt Advanced', clickOn: 'confirm' }); await gmailPage.waitTillGone('@dialog-subscribe', { timeout: 60 }); diff --git a/test/source/tests/tests/compose.ts b/test/source/tests/tests/compose.ts index 7801d9ad4f0..30cd3e974f6 100644 --- a/test/source/tests/tests/compose.ts +++ b/test/source/tests/tests/compose.ts @@ -12,7 +12,7 @@ import { TestVariant } from '../../util'; import { expect } from "chai"; import { AvaContext } from '..'; import { Dict } from '../../core/common'; -import { Data } from '../../mock/data'; +import { GoogleData } from '../../mock/google/google-data'; import * as request from 'fc-node-requests'; import { PgpMsg } from '../../core/pgp'; import { SettingsPageRecipe } from '../page_recipe/settings-page-recipe'; @@ -459,7 +459,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithNewBrowser: const fileInput = await composePage.target.$('input[type=file]'); await fileInput!.uploadFile('test/samples/small.txt'); await ComposePageRecipe.sendAndClose(composePage, msgPwd); - const msg = new Data('flowcrypt.compatibility@gmail.com').getMessageBySubject(subject)!; + const msg = new GoogleData('flowcrypt.compatibility@gmail.com').getMessageBySubject(subject)!; const webDecryptUrl = msg.payload.body!.data!.match(/https:\/\/flowcrypt.com\/[a-z0-9A-Z]+/g)![0]; const webDecryptPage = await browser.newPage(t, webDecryptUrl); await webDecryptPage.waitAndType('.decrypt_answer', msgPwd); diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 85b33f16dcf..71ee4d9adf8 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -1,8 +1,9 @@ import { TestWithNewBrowser, TestWithGlobalBrowser } from '../../test'; import { BrowserRecipe } from '../browser_recipe'; import * as ava from 'ava'; -import { TestVariant } from '../../util'; +import { TestVariant, Util } from '../../util'; import { SetupPageRecipe } from '../page_recipe/setup-page-recipe'; +import { expect } from 'chai'; // tslint:disable:no-blank-lines-func @@ -130,6 +131,24 @@ export const defineSetupTests = (testVariant: TestVariant, testWithNewBrowser: T await SetupPageRecipe.manualEnter(settingsPage, 'flowcrypt.test.key.used.pgp', { submitPubkey: true, usedPgpBefore: true, simulateRetryOffline: true }); })); + ava.default('[standalone] has.pub@org-rules-test - no backup, no keygen', testWithNewBrowser(async (t, browser) => { + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, 'has.pub@org-rules-test.flowcrypt.com'); + await SetupPageRecipe.manualEnter(settingsPage, 'has.pub.orgrulestest', { noPrvCreateOrgRule: true, enforceAttesterSubmitOrgRule: true }); + await settingsPage.waitAll(['@action-show-encrypted-inbox', '@action-open-security-page']); + await Util.sleep(1); + await settingsPage.notPresent(['@action-open-backup-page']); + })); + + ava.default('[standalone] no.pub@org-rules-test - no backup, no keygen, enforce attester submit with submit err', testWithNewBrowser(async (t, browser) => { + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, 'no.pub@org-rules-test.flowcrypt.com'); + await SetupPageRecipe.manualEnter(settingsPage, 'no.pub.orgrulestest', { noPrvCreateOrgRule: true, enforceAttesterSubmitOrgRule: true, fillOnly: true }); + await settingsPage.waitAndClick('@input-step2bmanualenter-save'); + await settingsPage.waitAll(['@container-overlay-prompt-text', '@action-overlay-retry']); + const renderedErr = await settingsPage.read('@container-overlay-prompt-text'); + expect(renderedErr).to.contain(`Failed to submit to Attester`); + expect(renderedErr).to.contain(`Could not find LDAP pubkey on a LDAP-only domain for email no.pub@org-rules-test.flowcrypt.com on server keys.flowcrypt.com`); + })); + } }; diff --git a/tooling/build-types.ts b/tooling/build-types.ts index b341a701491..cbb115029c1 100644 --- a/tooling/build-types.ts +++ b/tooling/build-types.ts @@ -14,7 +14,11 @@ const edit = (filepath: string, editor: (content: string) => string) => { const makeMockBuild = (buildType: string) => { const mockBuildType = `${buildType}-mock`; exec(`cp -r ${buildDir(buildType)} ${buildDir(mockBuildType)}`); - const editor = (code: string) => code.replace(/const (GOOGLE_API_HOST|GOOGLE_OAUTH_SCREEN_HOST) = [^;]+;/g, `const $1 = '${MOCK_HOST[buildType]}';`); + const editor = (code: string) => { + return code + .replace(/const (GOOGLE_API_HOST|GOOGLE_CONTACTS_API_HOST|GOOGLE_OAUTH_SCREEN_HOST) = [^;]+;/g, `const $1 = '${MOCK_HOST[buildType]}';`) + .replace(/const (BACKEND_API_HOST) = [^;]+;/g, `const $1 = 'http://localhost:8001/api/';`); + }; edit(`${buildDir(mockBuildType)}/js/common/core/const.js`, editor); edit(`${buildDir(mockBuildType)}/js/content_scripts/webmail_bundle.js`, editor); }; diff --git a/tooling/fill-values.ts b/tooling/fill-values.ts index 27492e25095..a23b96ae337 100644 --- a/tooling/fill-values.ts +++ b/tooling/fill-values.ts @@ -10,10 +10,12 @@ const { compilerOptions: { outDir: targetDirExtension } } = JSON.parse(readFileS const { compilerOptions: { outDir: targetDirContentScripts } } = JSON.parse(readFileSync('./conf/tsconfig.content_scripts.json').toString()); const { version } = JSON.parse(readFileSync(`./package.json`).toString()); +// mock values for a consumer-mock or enterprise-mock test builds are regex-replaced later in `build-types.ts` const replaceables: { needle: RegExp, val: string }[] = [ { needle: /\[BUILD_REPLACEABLE_VERSION\]/g, val: version }, { needle: /\[BUILD_REPLACEABLE_GOOGLE_API_HOST\]/g, val: 'https://www.googleapis.com' }, { needle: /\[BUILD_REPLACEABLE_GOOGLE_OAUTH_SCREEN_HOST\]/g, val: 'https://accounts.google.com' }, + { needle: /\[BUILD_REPLACEABLE_BACKEND_API_HOST\]/g, val: 'https://flowcrypt.com/api/' }, ]; const paths = [