import { addContacts , labelContactEmails , labelContacts } from '@proton/shared/lib/api/contacts' ; import { processApiRequestsSafe } from '@proton/shared/lib/api/helpers/safeApiRequests' ; import { createContactGroup } from '@proton/shared/lib/api/labels' ; import { API_CODES , HOUR , LABEL_COLORS } from '@proton/shared/lib/constants' ; import { CATEGORIES , OVERWRITE } from '@proton/shared/lib/contacts/constants' ; import { prepareContact } from '@proton/shared/lib/contacts/encrypt' ; import { getContactCategories , getContactEmails } from '@proton/shared/lib/contacts/properties' ; import { chunk , uniqueBy } from '@proton/shared/lib/helpers/array' ; import { noop , randomIntFromInterval } from '@proton/shared/lib/helpers/function' ; import { wait } from '@proton/shared/lib/helpers/promise' ; import { Api , KeyPair , Label , SimpleMap } from '@proton/shared/lib/interfaces' ; import { ContactProperties , IMPORT_GROUPS_ACTION , ImportCategories , ImportedContact , } from '@proton/shared/lib/interfaces/contacts' ; import { getContactId , splitErrors , extractContactImportCategories } from '@proton/shared/lib/contacts/helpers/import' ; import { AddContactsApiResponse , AddContactsApiResponses , EncryptedContact , ImportContactsModel , } from '@proton/shared/lib/interfaces/contacts/Import' ; import { IMPORT_CONTACT_ERROR_TYPE , ImportContactError } from '@proton/shared/lib/contacts/errors/ImportContactError' ; const { SINGLE_SUCCESS } = API_CODES ; const BATCH_SIZE = 10 ; const encryptContact = async ( contact , { privateKey , publicKey } ) => { try { const contactEncrypted = await prepareContact ( contact , { privateKey , publicKey } ) ; return { contact : contactEncrypted , contactId : getContactId ( contact ) , contactEmails : getContactEmails ( contact ) , categories : getContactCategories ( contact ) , } ; } catch ( error ) { const contactId = getContactId ( contact ) ; return new ImportContactError ( IMPORT_CONTACT_ERROR_TYPE . ENCRYPTION_ERROR , contactId ) ; } } ; const submitContacts = async ( { contacts , labels , overwrite , api , isImport , } ) => { let responses = [ ] ; try { const { Responses } = await api < AddContactsApiResponses > ( { ... addContacts ( { Contacts : contacts . map ( ( { contact } ) => contact ) , Overwrite : overwrite , Labels : labels , Import : isImport ? 1 : 0 , } ) , timeout : HOUR , silence : true , } ) ; responses = Responses ; } catch ( error ) { const { Code = 0 , Error = error . message } = error . data || { } ; responses = contacts . map ( ( contact , index ) => ( { Index : index , Response : { Code , Error } , } ) ) ; } return responses . map ( ( response ) => { const { Index , Response : { Error : errorMessage , Code , Contact } , } = response ; if ( Code !== SINGLE_SUCCESS || ! Contact ) { const error = new Error ( errorMessage ) ; const { contactId } = contacts [ Index ] ; return new ImportContactError ( IMPORT_CONTACT_ERROR_TYPE . EXTERNAL_ERROR , contactId , error ) ; } const contact = contacts [ Index ] ; return { contactID : Contact . ID , contactEmailIDs : Contact . ContactEmails . map ( ( contactEmail ) => contactEmail . ID ) , categories : extractContactImportCategories ( Contact , contact ) , } ; } ) ; } ; interface ProcessData { contacts : ContactProperties [ ] ; labels : CATEGORIES ; overwrite : OVERWRITE ; keyPair : KeyPair ; api : Api ; signal : AbortSignal ; isImport ? : boolean ; onProgress : ( encrypted : EncryptedContact [ ] , imported : ImportedContact [ ] , errors : ImportContactError [ ] ) => void ; } export const processContactsInBatches = async ( { contacts , labels , overwrite , keyPair , api , signal , isImport , onProgress , } ) => { const batches = chunk ( contacts , BATCH_SIZE ) ; const promises = [ ] ; const imported = [ ] ; for ( let i = 0 ; i < batches . length ; i ++ ) { if ( signal . aborted ) { return [ ] ; } const batchedContacts = batches [ i ] ; const [ result ] = await Promise . all ( [ Promise . all ( batchedContacts . map ( ( contacts ) => encryptContact ( contacts , keyPair ) ) ) , wait ( 100 ) , ] ) ; const { errors , rest : encrypted } = splitErrors ( result ) ; if ( signal . aborted ) { return [ ] ; } onProgress ( encrypted , [ ] , errors ) ; if ( encrypted . length ) { const promise = submitContacts ( { contacts : encrypted , labels , overwrite , isImport , api } ) . then ( ( result ) => { const { errors , rest : importedSuccess } = splitErrors ( result ) ; imported . push ( importedSuccess ) ; onProgress ( [ ] , importedSuccess , errors ) ; } ) ; promises . push ( promise ) ; } } await Promise . all ( promises ) ; return imported . flat ( ) ; } ; export const submitCategories = async ( categories , api ) => { const newCategories = uniqueBy ( categories . filter ( ( { action } ) => action === IMPORT_GROUPS_ACTION . CREATE ) , ( { targetName } ) => targetName ) ; const newLabelIDsMap = { } ; const createRequests = newCategories . map ( ( { targetName } ) => { return async ( ) => { try { const { Label : { ID } , } = await api < { Label : Label ; } > ( createContactGroup ( { Name : targetName , Color : LABEL_COLORS [ randomIntFromInterval ( 0 , LABEL_COLORS . length - 1 ) ] , } ) ) ; newLabelIDsMap [ targetName ] = ID ; } catch ( e ) { noop ( ) ; } } ; } ) ; await processApiRequestsSafe ( createRequests , 1000 , 100 * 1000 ) ; const labelRequests = [ ] ; categories . forEach ( ( { action , targetGroup , targetName , contactEmailIDs , contactIDs } ) => { if ( action === IMPORT_GROUPS_ACTION . IGNORE ) { return ; } if ( action === IMPORT_GROUPS_ACTION . MERGE ) { const labelID = targetGroup . ID ; if ( contactEmailIDs . length ) { labelRequests . push ( ( ) => api ( labelContactEmails ( { LabelID : labelID , ContactEmailIDs : contactEmailIDs } ) ) . catch ( noop ) ) ; } if ( contactIDs . length ) { labelRequests . push ( ( ) => api ( labelContacts ( { LabelID : labelID , ContactIDs : contactIDs } ) ) . catch ( noop ) ) ; } return ; } if ( action === IMPORT_GROUPS_ACTION . CREATE ) { const labelID = newLabelIDsMap [ targetName ] ; if ( labelID && contactEmailIDs . length ) { labelRequests . push ( ( ) => api ( labelContactEmails ( { LabelID : labelID , ContactEmailIDs : contactEmailIDs } ) ) . catch ( noop ) ) ; } if ( labelID && contactIDs . length ) { labelRequests . push ( ( ) => api ( labelContacts ( { LabelID : labelID , ContactIDs : contactIDs } ) ) . catch ( noop ) ) ; } } } ) ; return processApiRequestsSafe ( labelRequests , 1000 , 100 * 1000 ) ; } ; export const extractTotals = ( model ) => { const { parsedVcardContacts , totalEncrypted , totalImported , errors } = model ; const totalToImport = parsedVcardContacts . length ; const totalToProcess = 2 * totalToImport ; const totalErrors = errors . length ; const totalProcessed = totalEncrypted + totalImported + totalErrors ; return { totalToImport , totalToProcess , totalImported , totalProcessed } ; } ;