import { getItem , setItem , removeItem } from '@proton/shared/lib/helpers/storage' ; import { MAILBOX_LABEL_IDS } from '@proton/shared/lib/constants' ; import { LabelCount } from '@proton/shared/lib/interfaces' ; import { destroyOpenPGP , loadOpenPGP } from '@proton/shared/lib/openpgp' ; import isTruthy from '@proton/shared/lib/helpers/isTruthy' ; import { wait } from '@proton/shared/lib/helpers/promise' ; import { captureMessage } from '@proton/shared/lib/helpers/sentry' ; import { IDBPDatabase , openDB , deleteDB } from 'idb' ; import { Location , History } from 'history' ; import { EncryptedSearchDB , ESProgressBlob , GetUserKeys } from '../../models/encryptedSearch' ; import { ES_MAX_PARALLEL_MESSAGES } from '../../constants' ; import { decryptIndexKey } from './esBuild' ; import { extractSearchParameters , filterFromUrl , pageFromUrl , setSortInUrl , sortFromUrl } from '../mailboxUrl' ; import { isSearch } from '../elements' ; import { roundMilliseconds } from './esSearch' ; /**
 * Helpers to work with ES blobs in localStorage
 */ const getESItem = ( userID , blobName ) => getItem ( `ES:${ userID }:${ blobName }` ) ; const setESItem = ( userID , blobName , blobValue ) => setItem ( `ES:${ userID }:${ blobName }` , blobValue ) ; const removeESItem = ( userID , blobName ) => removeItem ( `ES:${ userID }:${ blobName }` ) ; export const removeESFlags = ( userID ) => { Object . keys ( window . localStorage ) . forEach ( ( key ) => { const chunks = key . split ( ':' ) ; if ( chunks [ 0 ] === 'ES' && chunks [ 1 ] === userID ) { removeItem ( key ) ; } } ) ; } ; export const getES = { Key : ( userID ) => getESItem ( userID , 'Key' ) , Event : ( userID ) => getESItem ( userID , 'Event' ) , Progress : ( userID ) : ESProgressBlob | null => JSON . parse ( getESItem ( userID , 'BuildProgress' ) || 'null' ) , Size : ( userID ) => parseInt ( getESItem ( userID , 'SizeIDB' ) || '0' , 10 ) || 0 , Pause : ( userID ) => getESItem ( userID , 'Pause' ) === 'true' , Enabled : ( userID ) => getESItem ( userID , 'ESEnabled' ) === 'true' , } ; export const setES = { Key : ( userID , armoredKey ) => setESItem ( userID , 'Key' , armoredKey ) , Event : ( userID , eventID ) => setESItem ( userID , 'Event' , eventID ) , Progress : ( userID , esProgressBlob ) => setESItem ( userID , 'BuildProgress' , JSON . stringify ( esProgressBlob ) ) , Size : ( userID , size ) => setESItem ( userID , 'SizeIDB' , `${ size }` ) , Pause : ( userID ) => setESItem ( userID , 'Pause' , 'true' ) , Enabled : ( userID ) => setESItem ( userID , 'ESEnabled' , 'true' ) , } ; export const removeES = { Key : ( userID ) => removeESItem ( userID , 'Key' ) , Event : ( userID ) => removeESItem ( userID , 'Event' ) , Progress : ( userID ) => removeESItem ( userID , 'BuildProgress' ) , Size : ( userID ) => removeESItem ( userID , 'SizeIDB' ) , Pause : ( userID ) => removeESItem ( userID , 'Pause' ) , Enabled : ( userID ) => removeESItem ( userID , 'ESEnabled' ) , } ; /**
 * Helpers to work with ES IndexedDB
 */ export const openESDB = async ( userID ) => openDB < EncryptedSearchDB > ( `ES:${ userID }:DB` ) ; export const deleteESDB = async ( userID ) => deleteDB ( `ES:${ userID }:DB` ) ; export const createESDB = async ( userID ) => { return openDB < EncryptedSearchDB > ( `ES:${ userID }:DB` , 1 , { upgrade ( esDB ) { esDB . createObjectStore ( 'messages' , { keyPath : 'ID' } ) . createIndex ( 'byTime' , [ 'Time' , 'Order' ] , { unique : true , } ) ; } , } ) ; } ; /**
 * Helper to send ES-related sentry reports
 */ export const esSentryReport = ( errorMessage , extra ? ) => { captureMessage ( `[EncryptedSearch] ${ errorMessage }` , { extra } ) ; } ; /**
 * Check whether the index key exists
 */ export const indexKeyExists = ( userID ) => ! ! getES . Key ( userID ) ; /**
 * Check whether a previously started indexing process has terminated successfully
 */ export const isDBReadyAfterBuilding = ( userID ) => ! getES . Progress ( userID ) ; /**
 * Check whether a key exists and the corresponding indexing process has terminated successfully
 */ export const wasIndexingDone = ( userID ) => indexKeyExists ( userID ) && isDBReadyAfterBuilding ( userID ) ; /**
 * Fetch the oldest message from IDB
 */ export const getOldestMessage = async ( esDB ) => { return esDB . getFromIndex ( 'messages' , 'byTime' , IDBKeyRange . lowerBound ( [ 0 , 0 ] ) ) ; } ; /**
 * Fetch Time and Order of the oldest message from IDB
 */ export const getOldestTimePoint = async ( userID ) => { const esDB = await openESDB ( userID ) ; const oldestMessage = await getOldestMessage ( esDB ) ; esDB . close ( ) ; return [ oldestMessage ?. Time || 0 , oldestMessage ?. Order || 0 ] as [ number , number ] ; } ; /**
 * Fetch Time of the oldest message from IDB, eventually corrected by a given factor
 */ export const getOldestTime = async ( userID , correctionFactor ? ) => { const timePoint = await getOldestTimePoint ( userID ) ; return timePoint [ 0 ] * ( correctionFactor || 1 ) ; } ; /**
 * Fetch the number of messages in IDB
 */ export const getNumMessagesDB = async ( userID ) => { const esDB = await openESDB ( userID ) ; const count = await esDB . count ( 'messages' ) ; esDB . close ( ) ; return count ; } ; /**
 * Fetch the number of messages in the mailbox when indexing had started, i.e.
 * excluding those that have changed since then
 */ export const getESTotal = ( userID ) => { return getES . Progress ( userID ) ?. totalMessages || 0 ; } ; /**
 * Fetch the indexing progress from BuildProgress
 */ export const getESCurrentProgress = ( userID ) => { const progressBlob = getES . Progress ( userID ) ; if ( ! progressBlob ) { return 0 ; } const { currentMessages , totalMessages } = progressBlob ; return Math . ceil ( ( ( currentMessages || 0 ) / totalMessages ) * 100 ) ; } ; /**
 * Overwrites the BuildProgress blob in localStorage
 */ export const setESProgress = ( userID , newProperties ) => { const progressBlob = getES . Progress ( userID ) ; if ( ! progressBlob ) { return ; } setES . Progress ( userID , { ... progressBlob , ... newProperties , } ) ; } ; /**
 * Set the number of messages already indexed in BuildProgress and save it to localStorage
 */ export const setESCurrent = ( userID , currentMessages ) => { setESProgress ( userID , { currentMessages : Math . floor ( currentMessages / ES_MAX_PARALLEL_MESSAGES ) * ES_MAX_PARALLEL_MESSAGES , } ) ; } ; /**
 * Increase by 1 the locally cached number of times the user has paused indexing
 */ export const increaseNumPauses = ( userID ) => { setESProgress ( userID , { numPauses : ( getES . Progress ( userID ) ?. numPauses || 0 ) + 1 , } ) ; } ; /**
 * Add a timestamp to the locally cached set of indexing timestamps
 */ export const addESTimestamp = ( userID , type ) => { const progressBlob = getES . Progress ( userID ) ; if ( ! progressBlob ) { return ; } const { timestamps } = progressBlob ; timestamps . push ( { type , time : roundMilliseconds ( Date . now ( ) ) } ) ; setESProgress ( userID , { timestamps , } ) ; } ; /**
 * Stores the initial estimate in seconds, but only if it's the first of such predictions
 */ export const setOriginalEstimate = ( userID , inputEstimate ) => { const progressBlob = getES . Progress ( userID ) ; if ( ! progressBlob ) { return ; } if ( progressBlob . originalEstimate === 0 ) { setESProgress ( userID , { originalEstimate : inputEstimate , } ) ; } } ; /**
 * Update the rolling estimated size of IDB
 */ export const updateSizeIDB = ( userID , addend ) => { const sizeIDB = getES . Size ( userID ) ; const newSize = sizeIDB + addend ; setES . Size ( userID , newSize ) ; } ; /**
 * Read the current total amount of messages
 */ export const getTotalMessages = async ( messageCounts ) => { return messageCounts . find ( ( labelCount ) => labelCount ?. LabelID === MAILBOX_LABEL_IDS . ALL_MAIL ) ?. Total || 0 ; } ; /**
 * Check whether ES can be used not just because the index key exists in localStorage
 * but also because IDB is not corrupt, i.e. the object store exists
 */ export const canUseES = async ( userID ) => { if ( ! indexKeyExists ( userID ) ) { return false ; } const esDB = await openESDB ( userID ) ; const isIntact = esDB . objectStoreNames . contains ( 'messages' ) ; esDB . close ( ) ; return isIntact ; } ; /**
 * Destroy and load openpgp workers back again
 */ export const refreshOpenpgp = async ( ) => { const { openpgp } = window as any ; const openpgpWorkers = openpgp . getWorker ( ) ; if ( ! openpgpWorkers ) { return ; } while ( openpgpWorkers . workers . some ( ( worker ) => worker . requests ) ) { await wait ( 200 ) ; } await destroyOpenPGP ( ) ; await loadOpenPGP ( ) ; } ; /**
 * Return index keys from legacy blobs and associated user IDs
 */ export const checkNewUserID = async ( getUserKeys ) => { return ( await Promise . all ( Object . keys ( window . localStorage ) . map ( async ( key ) => { const chunks = key . split ( ':' ) ; if ( chunks [ 0 ] === 'ES' && chunks [ 2 ] === 'Key' ) { const userID = chunks [ 1 ] ; try { const indexKey = await decryptIndexKey ( getUserKeys , getItem ( key ) ) ; if ( indexKey ) { return { userID , indexKey } ; } } catch ( error ) { } } } ) ) ) . filter ( isTruthy ) ; } ; /**
 * Parse search parameters from URL
 */ export const parseSearchParams = ( location ) => { const searchParameters = extractSearchParameters ( location ) ; return { filterParameter : filterFromUrl ( location ) , sortParameter : sortFromUrl ( location ) , isSearch : isSearch ( searchParameters ) , page : pageFromUrl ( location ) , searchParameters , } ; } ; /**
 * Reset sort in URL, e.g because ES doesn't support SIZE sort
 */ export const resetSort = ( history ) => { history . push ( setSortInUrl ( history . location , { sort : 'Time' , desc : true } ) ) ; } ;