import WebStorageCookieStore from "tough-cookie-web-storage-store" ; import { CookieJar } from "tough-cookie" ; import { Observable , ReplaySubject , from , of , throwError } from "rxjs" ; import { concatMap , delay , retryWhen } from "rxjs/operators" ; import * as RestModel from "src/electron-preload/webview/lib/rest-model" ; import { DbPatch } from "src/shared/api/common" ; import { FsDbAccount } from "src/shared/model/database" ; import { IpcMainApiEndpoints } from "src/shared/api/main-process" ; import { Logger } from "src/shared/model/common" ; import { ONE_SECOND_MS } from "src/shared/constants" ; import { ProviderApi } from "src/electron-preload/webview/primary/provider-api/model" ; import { asyncDelay , curryFunctionMembers , isDatabaseBootstrapped , resolveApiUrlByPackagedWebClientUrlSafe } from "src/shared/util" ; import { resolveCachedConfig , resolveIpcMainApi } from "src/electron-preload/lib/util" ; export const resolveDomElements = async < E extends Element | null , Q extends Readonly < Record < string , ( ) => E > > , K extends keyof Q , R extends { [ key in K ] : Exclude < ReturnType < Q [ key ] > , null > ; } > ( query , logger , opts = { } ) : Promise < R > => { const { timeouts : { domElementsResolving } } = await resolveCachedConfig ( logger ) ; return new Promise ( ( resolve , reject ) => { const OPTS = { timeoutMs : ( opts . timeoutMs ?? domElementsResolving ?? ONE_SECOND_MS * 10 ) , iterationsLimit : opts . iterationsLimit || 0 , delayMinMs : 300 , } ; const startTime = Date . now ( ) ; const delayMs = OPTS . timeoutMs / 50 ; const queryKeys = Object . keys ( query ) as K [ ] ; const resolvedElements = { } ; let it = 0 ; const scanElements = ( ) => { it ++ ; queryKeys . forEach ( ( key ) => { if ( key in resolvedElements ) { return ; } const queryFn = query [ key ] ; if ( typeof queryFn !== "function" ) { throw new Error ( "Failed to resolved query function" ) ; } const element = queryFn ( ) ; if ( element ) { resolvedElements [ key ] = element as any ; } } ) ; if ( Object . keys ( resolvedElements ) . length === queryKeys . length ) { return resolve ( resolvedElements as R ) ; } if ( OPTS . iterationsLimit && ( it >= OPTS . iterationsLimit ) ) { return reject ( new Error ( `Failed to resolve some DOM elements from the list [${ queryKeys . join ( ", " ) }] having "${ it }" iterations performed` ) ) ; } if ( Date . now ( ) - startTime > OPTS . timeoutMs ) { return reject ( new Error ( `Failed to resolve some DOM elements from the list [${ queryKeys . join ( ", " ) }] within "${ OPTS . timeoutMs }" milliseconds` ) ) ; } setTimeout ( scanElements , Math . max ( OPTS . delayMinMs , delayMs ) ) ; } ; scanElements ( ) ; } ) ; } ; export function getLocationHref ( ) { return window . location . href ; } export function fillInputValue ( input , value ) { const setValue = ( ( ) => { const valueSetter = Object . getOwnPropertyDescriptor ( input , "value" ) ?. set ; const prototypeValueSetter = Object . getOwnPropertyDescriptor ( Object . getPrototypeOf ( input ) , "value" ) ?. set ; return prototypeValueSetter && valueSetter !== prototypeValueSetter ? prototypeValueSetter : valueSetter ; } ) ( ) ; if ( ! setValue ) { throw new Error ( "Form input control value setter resolving failed" ) ; } setValue . call ( input , value ) ; input . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ; } export async function submitTotpToken ( input , button , resolveToken , _logger , { submitTimeoutMs = ONE_SECOND_MS * 8 , newTokenDelayMs = ONE_SECOND_MS * 2 , submittingDetection , } = { } ) { const logger = curryFunctionMembers ( _logger , nameof ( submitTotpToken ) ) ; logger . info ( ) ; if ( input . value ) { throw new Error ( "2FA TOTP token is not supposed to be pre-filled on this stage" ) ; } const errorMessage = `Failed to submit two factor token within ${ submitTimeoutMs }ms` ; const submit = async ( ) => { logger . verbose ( "submit - start" ) ; const submitted = ( submittingDetection || ( ( urlBeforeSubmit = getLocationHref ( ) ) => { return async ( ) => getLocationHref ( ) !== urlBeforeSubmit ; } ) ( ) ) ; fillInputValue ( input , await resolveToken ( ) ) ; logger . verbose ( "input filled" ) ; button . click ( ) ; logger . verbose ( "clicked" ) ; await asyncDelay ( submitTimeoutMs ) ; if ( ! ( await submitted ( ) ) ) { throw new Error ( errorMessage ) ; } logger . verbose ( "submit - success" ) ; } ; try { await submit ( ) ; } catch ( error ) { const { message } = ( Object ( error ) as { message ? : unknown ; } ) ; if ( message !== errorMessage ) { throw error ; } logger . verbose ( `submit 1 - fail: ${ String ( message ) }` ) ; await asyncDelay ( newTokenDelayMs , submit ) ; } } export function buildDbPatchRetryPipeline < T > ( preprocessError , metadata , logger , { retriesDelay = ONE_SECOND_MS * 5 , retriesLimit = 3 } = { } ) { const errorResult = ( error ) : ReturnType < typeof throwError > => { logger . error ( nameof ( buildDbPatchRetryPipeline ) , error ) ; return throwError ( error ) ; } ; return retryWhen < T > ( ( errors ) => errors . pipe ( concatMap ( ( rawError , retryIndex ) => { const { error , retriable , skippable } = preprocessError ( rawError ) ; if ( ! isDatabaseBootstrapped ( metadata ) ) { return errorResult ( error ) ; } if ( retryIndex >= retriesLimit ) { if ( skippable ) { const message = `Skipping "buildDbPatch" call` ; logger . warn ( nameof ( buildDbPatchRetryPipeline ) , message , error ) ; return from ( Promise . resolve ( ) ) ; } return errorResult ( error ) ; } if ( retriable ) { logger . warn ( nameof ( buildDbPatchRetryPipeline ) , `Retrying call (attempt: "${ retryIndex }")` ) ; return of ( error ) . pipe ( delay ( retriesDelay ) ) ; } return errorResult ( error ) ; } ) ) ) ; } export async function persistDatabasePatch ( data , logger , bootstrapPhase ? ) { logger . info ( `${ nameof ( persistDatabasePatch ) }() start` , JSON . stringify ( { bootstrapPhase } ) ) ; await resolveIpcMainApi ( { logger } ) ( "dbPatch" ) ( { bootstrapPhase , login : data . login , metadata : data . metadata , patch : data . patch , } ) ; logger . info ( `${ nameof ( persistDatabasePatch ) }() end` ) ; } export function buildEmptyDbPatch ( ) { return { conversationEntries : { remove : [ ] , upsert : [ ] } , mails : { remove : [ ] , upsert : [ ] } , folders : { remove : [ ] , upsert : [ ] } , contacts : { remove : [ ] , upsert : [ ] } , } ; } export function disableBrowserNotificationFeature ( parentLogger ) { delete ( window as Partial < Pick < typeof window , "Notification" > > ) . Notification ; parentLogger . info ( `browser "notification" feature disabled` ) ; } export const fetchEvents = async ( providerApi , latestEventId , _logger ) : Promise < { latestEventId : RestModel . Event [ "EventID" ] ; events : RestModel . Event [ ] ; } | "refresh" > => { const logger = curryFunctionMembers ( _logger , nameof ( fetchEvents ) ) ; const events = [ ] ; const iterationState = { latestEventId , sameNextIdCounter : 0 , } ; do { const response = await providerApi . events . getEvents ( iterationState . latestEventId ) ; const hasMoreEvents = response . More === 1 ; if ( response . Refresh ) { return "refresh" ; } events . push ( response ) ; iterationState . sameNextIdCounter += Number ( iterationState . latestEventId === response . EventID ) ; iterationState . latestEventId = response . EventID ; if ( ! hasMoreEvents ) { break ; } if ( iterationState . sameNextIdCounter > 2 ) { logger . error ( `Events API indicates that there is a next event in the queue but responded with the same "next event id".` ) ; break ; } } while ( true ) ; logger . info ( `fetched ${ events . length } missed events` ) ; return { latestEventId : iterationState . latestEventId , events , } ; } ; export const documentCookiesForCustomScheme = ( ( ) => { const setNotificationSubject$ = new ReplaySubject < { url : string ; cookieString : string ; } > ( 1 ) ; const result = { setNotification$ : setNotificationSubject$ . asObservable ( ) , enable ( logger ) { logger . verbose ( nameof ( documentCookiesForCustomScheme ) , nameof ( result . enable ) ) ; const { document , location } = window ; const getUrl = ( ) : string => resolveApiUrlByPackagedWebClientUrlSafe ( location . toString ( ) ) ; const cookieJar = new CookieJar ( new WebStorageCookieStore ( window . sessionStorage ) ) ; Object . defineProperty ( document , "cookie" , { enumerable : true , configurable : true , get ( ) { const url = getUrl ( ) ; const cookies = cookieJar . getCookiesSync ( url ) ; return cookies . map ( ( cookie ) => cookie . cookieString ( ) ) . join ( "; " ) ; } , set ( cookieString ) { const url = getUrl ( ) ; cookieJar . setCookieSync ( cookieString , url ) ; setNotificationSubject$ . next ( { url , cookieString } ) ; } , } ) ; } , } ; return result ; } ) ( ) ;