import { PasswordBasedPreset } from "fs-json-store-encryption-adapter" ; import type { RateLimiterMemory } from "rate-limiter-flexible" ; import { URL } from "@cliqz/url-parser" ; import { mapValues , pick } from "remeda" ; import { ACCOUNT_EXTERNAL_CONTENT_PROXY_URL_REPLACE_PATTERN , DEFAULT_API_CALL_TIMEOUT , DEFAULT_MESSAGES_STORE_PORTION_SIZE , LOCAL_WEBCLIENT_PROTOCOL_RE_PATTERN , ONE_MINUTE_MS , ONE_SECOND_MS , PROTON_API_ENTRY_URLS , WEB_CLIENTS_BLANK_HTML_FILE_NAME , ZOOM_FACTOR_DEFAULT , } from "./constants" ; import { AccountConfig } from "./model/account" ; import { BaseConfig , Config } from "./model/options" ; import { DbPatch } from "./api/common" ; import { FsDbAccount , View } from "./model/database" ; import { LoginFieldContainer } from "./model/container" ; import { PROVIDER_APP_NAMES , PROVIDER_REPO_MAP } from "./proton-apps-constants" ; import { StatusCodeError } from "./model/error" ; export function initialConfig ( ) { { const encryptionPreset = { keyDerivation : { type : "sodium.crypto_pwhash" , preset : BUILD_ENVIRONMENT === "e2e" ? "mode:interactive|algorithm:default" : "mode:moderate|algorithm:default" } , encryption : { type : "crypto" , preset : "algorithm:aes-256-cbc" } , } ; return { spellCheckLocale : false , encryptionPreset , window : { bounds : { width : 1024 , height : 768 } , ... ( BUILD_START_MAXIMIZED_BY_DEFAULT ? { maximized : true } : { } ) , } , fetching : { rateLimit : { intervalMs : ONE_MINUTE_MS , maxInInterval : 275 , } , messagesStorePortionSize : DEFAULT_MESSAGES_STORE_PORTION_SIZE , } , timeouts : { dbBootstrapping : ONE_MINUTE_MS * 60 * 12 , dbSyncing : ONE_MINUTE_MS * 30 , webViewApiPing : ONE_SECOND_MS * 15 , webViewBlankDOMLoaded : ONE_SECOND_MS * 15 , domElementsResolving : ONE_SECOND_MS * 20 , defaultApiCall : DEFAULT_API_CALL_TIMEOUT , databaseLoading : ONE_MINUTE_MS * 5 , indexingBootstrap : ONE_SECOND_MS * 30 , clearSessionStorageData : ONE_SECOND_MS * 3 , attachmentLoadAverage : ONE_MINUTE_MS * 2 , fullTextSearch : ONE_SECOND_MS * 30 , } , updateCheck : { releasesUrl : "https://api.github.com/repos/vladimiry/ElectronMail/releases" , proxyRules : "" , proxyBypassRules : "" , } , indexingBootstrapBufferSize : 1000 , jsFlags : [ "--max-old-space-size=6144" , ] , commandLineSwitches : [ ] , localDbMailsListViewMode : "plain" , userAgents : [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:93.0) Gecko/20100101 Firefox/93.0" , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36" , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36" , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36" , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15" , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15" , "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.0; rv:94.0) Gecko/20100101 Firefox/94.0" , "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36" , "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" , "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36" , "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36" , "Mozilla/5.0 (X11; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0" , "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0" , "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0" , ] , zoomFactorDisabled : false , persistentSessionSavingInterval : ONE_MINUTE_MS * 15 , dbSyncingIntervalTrigger : ONE_MINUTE_MS * 5 , dbSyncingOnlineTriggerDelay : ONE_SECOND_MS * 3 , dbSyncingFiredTriggerDebounce : ONE_SECOND_MS * 5 , shouldRequestDbMetadataReset : "initial" , calendarNotification : false , checkUpdateAndNotify : false , customTrayIconColor : "" , customUnreadBgColor : "" , customUnreadTextColor : "" , disableSpamNotifications : true , doNotRenderNotificationBadgeValue : false , enableHideControlsHotkey : false , findInPage : true , fullTextSearch : true , hideControls : false , hideOnClose : ! BUILD_DISABLE_CLOSE_TO_TRAY_FEATURE , idleTimeLogOutSec : 0 , layoutMode : "top" , logLevel : "error" , startHidden : ! BUILD_DISABLE_START_HIDDEN_FEATURE , themeSource : "system" , unreadNotifications : true , zoomFactor : ZOOM_FACTOR_DEFAULT , } ; } } export const pickBaseConfigProperties = ( config ) : NoExtraProps < Required < BaseConfig > > => pick ( config , [ "calendarNotification" , "checkUpdateAndNotify" , "customTrayIconColor" , "customUnreadBgColor" , "customUnreadTextColor" , "disableSpamNotifications" , "doNotRenderNotificationBadgeValue" , "enableHideControlsHotkey" , "findInPage" , "fullTextSearch" , "hideControls" , "hideOnClose" , "idleTimeLogOutSec" , "layoutMode" , "logLevel" , "startHidden" , "themeSource" , "unreadNotifications" , "zoomFactor" , ] ) ; export const accountPickingPredicate = ( { login : criteriaLogin } ) => { return ( { login } ) => login === criteriaLogin ; } ; export const pickAccountStrict = ( accounts , criteria ) : AccountConfig => { const account = accounts . find ( accountPickingPredicate ( criteria ) ) ; if ( ! account ) { throw new StatusCodeError ( `Account with "${ criteria . login }" login has not been found` , "NotFoundAccount" ) ; } return account ; } ; export const asyncDelay = async < T > ( pauseTimeMs , resolveAction ? ) : Promise < T | void > => { return new Promise < T | void > ( ( resolve ) => { setTimeout ( ( ) => { return typeof resolveAction === "function" ? resolve ( resolveAction ( ) ) : resolve ( ) ; } , pauseTimeMs ) ; } ) ; } ; export function curryFunctionMembers < T extends object | ( ( ... a : any [ ] ) => any ) > ( src , ... args ) { const dest = typeof src === "function" ? src . bind ( undefined ) : Object . create ( null ) ; for ( const key of Object . getOwnPropertyNames ( src ) ) { const srcMember = ( src as any ) [ key ] ; ( dest as any ) [ key ] = typeof srcMember === "function" ? srcMember . bind ( src , ... args ) : srcMember ; } return dest ; } export function isEntityUpdatesPatchNotEmpty ( { conversationEntries , folders , mails , contacts } ) { return [ conversationEntries . remove , conversationEntries . upsert , mails . remove , mails . upsert , folders . remove , folders . upsert , contacts . remove , contacts . upsert , ] . some ( ( { length } ) => Boolean ( length ) ) ; } export function walkConversationNodesTree ( rootNodes , fn ) { const state = { nodes : [ ... rootNodes ] } ; while ( state . nodes . length ) { const node = state . nodes . pop ( ) ; if ( ! node ) { continue ; } const called = fn ( { node , mail : node . mail } ) ; if ( typeof called === "string" && called === "break" ) { return ; } state . nodes . unshift ( ... [ ... node . children ] ) ; } } export function filterConversationNodesMails ( rootNodes , filter = ( ) : boolean => true ) { const result = [ ] ; walkConversationNodesTree ( rootNodes , ( { mail } ) => { if ( mail && filter ( mail ) ) { result . push ( mail ) ; } } ) ; return result ; } export function mailDateComparatorDefaultsToDesc ( o1 , o2 , order = "desc" ) { return order === "desc" ? o2 . sentDate - o1 . sentDate : o1 . sentDate - o2 . sentDate ; } export function mapBy < T , K > ( iterable , by ) { const map = new Map < K , T [ ] > ( ) ; for ( const el of iterable ) { const key = by ( el ) ; const list = map . get ( key ) || [ ] ; list . push ( el ) ; map . set ( key , list ) ; } return map ; } export function buildEnumBundle < M , K extends keyof M , V extends Extract < M [ keyof M ] , string | number > > ( nameValueMap ) { const { names , values , valueNameMap } = Object . entries ( nameValueMap ) . reduce ( ( accumulator , entry ) => { const [ key , value ] = entry as unknown as readonly [ K , V ] ; accumulator . names . push ( key ) ; accumulator . values . push ( value ) ; accumulator . valueNameMap [ value ] = key ; return accumulator ; } , { values : [ ] , names : [ ] , valueNameMap : { } as any , } ) ; const isValidValue = ( value ) : value is V => { return ( value as any ) in valueNameMap ; } ; interface ResolveNameByValue { ( value ) : K ; < S extends boolean > ( value , strict ) : S extends true ? K : K | undefined ; } const resolveNameByValue = ( value , strict = true ) => { if ( strict && ! isValidValue ( value ) ) { throw new Error ( `Failed to parse "${ String ( value ) }" value from the "${ JSON . stringify ( nameValueMap ) }" map` ) ; } return valueNameMap [ value ] ; } ; interface ParseValue { ( rawValue ) : V ; < S extends boolean > ( rawValue , strict ) : S extends true ? V : V | undefined ; } const parseValue = ( rawValue , strict = true ) => { const name = resolveNameByValue ( rawValue , strict ) ; if ( typeof name === "undefined" ) { return undefined as any ; } return nameValueMap [ name ] ; } ; return { ... nameValueMap , _ : { resolveNameByValue , parseValue , names , values , nameValueMap , isValidValue , } , } as const ; } export function isDatabaseBootstrapped ( metadata ) { if ( ! metadata ) { return false ; } return ( typeof metadata . latestEventId === "string" && Boolean ( metadata . latestEventId . trim ( ) ) ) ; } export function getRandomInt ( min , max ) { min = Math . ceil ( min ) ; max = Math . floor ( max ) ; return min + Math . floor ( Math . random ( ) * ( max - min ) ) ; } export const getWebViewPartition = ( ( ) => { const prefix = "partition/webview/" ; const result = ( login ) => `${ prefix }${ login }` ; return result ; } ) ( ) ; export const validateLoginDelaySecondsRange = ( ( ) => { const re = /^(\d+)-(\d+)$/ ; const result = ( loginDelaySecondsRange ) => { const match = re . exec ( loginDelaySecondsRange ) || [ ] ; const end = Number ( match . pop ( ) ) ; const start = Number ( match . pop ( ) ) ; if ( isNaN ( start ) || isNaN ( end ) ) { return { validationError : `Invalid data format, "number-number" format is expected.` } ; } if ( start > end ) { return { validationError : `"Start" value is bigger than "end" value.` } ; } return { start , end } ; } ; return result ; } ) ( ) ; export const parseLoginDelaySecondsRange = ( loginDelaySecondsRange ) => { const validation = validateLoginDelaySecondsRange ( loginDelaySecondsRange ) ; if ( "validationError" in validation ) { return ; } return validation ; } ; export const validateExternalContentProxyUrlPattern = ( { externalContentProxyUrlPattern : value , enableExternalContentProxy : enabled , } ) : boolean => { if ( ! enabled && ! value ) { return true ; } if ( ! value ) { return false ; } const parsedUrl = ( ( ) => { try { const url = new URL ( value ) ; return { origin : parseUrlOriginWithNullishCheck ( value ) , ... pick ( url , [ "pathname" , "search" ] ) , } ; } catch { return null ; } } ) ( ) ; return Boolean ( parsedUrl && ( parsedUrl . origin && ! parsedUrl . origin . includes ( ACCOUNT_EXTERNAL_CONTENT_PROXY_URL_REPLACE_PATTERN ) ) && [ parsedUrl . pathname , parsedUrl . search ] . filter ( ( valuePart ) => valuePart . includes ( ACCOUNT_EXTERNAL_CONTENT_PROXY_URL_REPLACE_PATTERN ) ) . length === 1 ) ; } ; export function removeArrayDuplicateItems < T extends any > ( array ) { return [ ... new Set < T > ( array ) . values ( ) ] ; } export const parsePackagedWebClientUrl = ( ( ) => { const re = new RegExp ( `^(${ LOCAL_WEBCLIENT_PROTOCOL_RE_PATTERN }:)` ) ; const result = ( urlArg ) => { if ( ! re . exec ( urlArg ) ) { return null ; } const url = new URL ( urlArg ) ; return pick ( url , [ "protocol" , "hostname" , "pathname" ] ) ; } ; return result ; } ) ( ) ; export const resolvePackagedWebClientApp = ( ( ) => { const subProjects = PROVIDER_APP_NAMES . filter ( ( projectType ) => projectType !== "proton-mail" ) ; const result = ( url ) => { const pathname = `${ url . pathname }/` ; const foundSubProject = subProjects . find ( ( subProject ) => { return pathname . startsWith ( `/${ PROVIDER_REPO_MAP [ subProject ] . basePath }/` ) ; } ) ; const project = foundSubProject || "proton-mail" ; const [ , ... projectSubPathParts ] = pathname . split ( "/" ) . filter ( Boolean ) ; const projectSubPath = projectSubPathParts . length ? projectSubPathParts . join ( "/" ) : undefined ; return { project , projectSubPath } ; } ; return result ; } ) ( ) ; const testProtonAppPage = ( targetProjectType , { url , logger } ) : { shouldInitProviderApi : boolean ; blankHtmlPage : boolean ; packagedWebClientUrl : ReturnType < typeof parsePackagedWebClientUrl > ; projectType ? : keyof typeof PROVIDER_REPO_MAP ; } => { let projectType ; const packagedWebClientUrl = parsePackagedWebClientUrl ( url ) ; const blankHtmlPage = packagedWebClientUrl ?. pathname === `/${ WEB_CLIENTS_BLANK_HTML_FILE_NAME }` ; const protonMailProject = Boolean ( ! blankHtmlPage && packagedWebClientUrl && ( projectType = resolvePackagedWebClientApp ( packagedWebClientUrl ) . project ) === targetProjectType ) ; const result = { shouldInitProviderApi : protonMailProject , blankHtmlPage , packagedWebClientUrl , projectType , } as const ; logger . verbose ( nameof ( testProtonAppPage ) , JSON . stringify ( { ... result , url , projectType } ) ) ; return result ; } ; export const testProtonMailAppPage = ( params ) : ReturnType < typeof testProtonAppPage > & { shouldDisableBrowserNotificationFeature : boolean ; } => { const baseResult = testProtonAppPage ( "proton-mail" , params ) ; return { ... baseResult , shouldDisableBrowserNotificationFeature : baseResult . shouldInitProviderApi , } ; } ; export const testProtonCalendarAppPage = ( params ) : ReturnType < typeof testProtonAppPage > => { return testProtonAppPage ( "proton-calendar" , params ) ; } ; export const consumeMemoryRateLimiter = async ( consume ) : Promise < { waitTimeMs : number ; } > => { try { await consume ( ) ; return { waitTimeMs : 0 } ; } catch ( _error ) { const error = _error as ( { msBeforeNext ? : unknown ; } | Unpacked < ReturnType < typeof RateLimiterMemory . prototype . consume > > ) ; if ( typeof error === "object" && typeof error . msBeforeNext === "number" ) { return { waitTimeMs : error . msBeforeNext } ; } throw error ; } } ; export const assertTypeOf = ( { value , expectedType } , errorMessagePrefix ) : void | never => { const actualType = typeof value ; if ( actualType !== expectedType ) { throw new Error ( `${ errorMessagePrefix } ${ JSON . stringify ( { actualType , expectedType : expectedType } ) }` ) ; } } ; export const verifyUrlOriginValue = ( origin ) : string | never => { if ( ! origin || String ( origin ) . trim ( ) === "null" ) { throw new Error ( `Unexpected "origin" value detected (value: "${ JSON . stringify ( { origin : String ( origin ) } ) }")` ) ; } return origin ; } ; export const parseUrlOriginWithNullishCheck = ( url ) : string | never => { const { origin } = new URL ( url ) ; verifyUrlOriginValue ( origin ) ; return origin ; } ; export const buildUrlOriginsFailedMsgTester = ( ( ) => { const result = ( allowedOriginsOrUrls ) => { const originsWhitelist = allowedOriginsOrUrls . map ( parseUrlOriginWithNullishCheck ) ; return ( url ) => { const urlOrigin = parseUrlOriginWithNullishCheck ( url ) ; return originsWhitelist . includes ( urlOrigin ) ? null : `No matched value found in ${ JSON . stringify ( originsWhitelist ) } URL origins list for "${ urlOrigin }" value.` ; } ; } ; return result ; } ) ( ) ; export const lowerConsoleMessageEventLogLevel = ( logLevel , message ) : typeof logLevel => { if ( String ( message ) . includes ( "OfflineError: No network connection" ) ) { return "warn" ; } return logLevel ; } ; export const depersonalizeProtonApiUrl = ( url ) : string => { try { if ( ! new URL ( url ) . pathname ) { return url ; } } catch { return url ; } const splitBy = "/" ; const splitParts = url . split ( splitBy ) ; const lastPart = splitParts . pop ( ) ; return [ ... splitParts , lastPart && lastPart . length >= 15 ? "<wiped-out>" : lastPart , ] . join ( splitBy ) ; } ; export const depersonalizeLoggedUrlsInString = ( ( ) => { const protonUrlRe = new RegExp ( PROTON_API_ENTRY_URLS . map ( ( value ) => `${ value }/\\S*` ) . join ( "|" ) , "gi" ) ; const result = ( value ) => { if ( typeof value !== "string" ) { return String ( value ) ; } return value . replace ( protonUrlRe , depersonalizeProtonApiUrl ) ; } ; return result ; } ) ( ) ; export const buildInitialVendorsAppCssLinks = ( hrefs , shouldUseDarkColors ? ) : string => { return hrefs . reduce ( ( accumulator , value ) => { const stylesheet = ( typeof shouldUseDarkColors !== "boolean" || ( value . endsWith ( "-dark.css" ) && shouldUseDarkColors ) || ( value . endsWith ( "-light.css" ) && ! shouldUseDarkColors ) ) ; return ( accumulator + `<link ${ stylesheet ? 'rel="stylesheet"' : 'rel="preload" as="style"' } href="${ value }"/>` ) ; } , "" ) ; } ; export const resolveApiUrlByPackagedWebClientUrlSafe = ( urlArg ) : string => { if ( ! parsePackagedWebClientUrl ( urlArg ) ) { throw new Error ( `The "${ urlArg }" doesn't look like the URL of the packaged Proton web client URL` ) ; } const packagedWebClientEndSlashedUrlStr = ( ( ) : string => { const url = new URL ( urlArg ) ; url . protocol = "https:" ; return url . toString ( ) ; } ) ( ) + "/" ; const result = PROTON_API_ENTRY_URLS . find ( ( apiEntryUrl ) => packagedWebClientEndSlashedUrlStr . startsWith ( `${ apiEntryUrl }/` ) ) ; if ( ! result ) { throw new Error ( `Failed to resolve Proton entry API URL by "${ urlArg }" packaged Proton web client URL` ) ; } return result ; } ; export const getPlainErrorProps = < T extends unknown > ( value ) : T | { code ? : string ; name ? : string ; message ? : string ; stack ? : string ; } => { if ( value === null ) { return { message : `stringified "null"` } ; } if ( typeof value === "undefined" ) { return { message : `stringified "undefined"` } ; } if ( typeof value !== "object" ) { return value ; } return { ... mapValues ( pick ( Object ( value ) as unknown as { code ? : unknown ; name ? : unknown ; message ? : unknown ; stack ? : unknown ; } , [ "code" , "message" , "name" , "stack" ] ) , String ) , } ; } ;