// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import #import #import "PhoneNumberNormalizer.h" #import "ContactStore.h" #import "NSString+Hex.h" #import "Contact.h" #import "ServerAPIConnector.h" #import "MyIdentityStore.h" #import "Utils.h" #import "UserSettings.h" #import "ProtocolDefines.h" #import "EntityManager.h" #import "ThreemaError.h" #import "AppGroup.h" #import "WorkDataFetcher.h" #import "ValidationLogger.h" #import "IdentityInfoFetcher.h" #import "CryptoUtils.h" #import "TrustedContacts.h" #define MIN_CHECK_INTERVAL 5*60 #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelInfo; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif static const uint8_t emailHashKey[] = {0x30,0xa5,0x50,0x0f,0xed,0x97,0x01,0xfa,0x6d,0xef,0xdb,0x61,0x08,0x41,0x90,0x0f,0xeb,0xb8,0xe4,0x30,0x88,0x1f,0x7a,0xd8,0x16,0x82,0x62,0x64,0xec,0x09,0xba,0xd7}; static const uint8_t mobileNoHashKey[] = {0x85,0xad,0xf8,0x22,0x69,0x53,0xf3,0xd9,0x6c,0xfd,0x5d,0x09,0xbf,0x29,0x55,0x5e,0xb9,0x55,0xfc,0xd8,0xaa,0x5e,0xc4,0xf9,0xfc,0xd8,0x69,0xe2,0x58,0x37,0x07,0x23}; static const NSTimeInterval minimumSyncInterval = 30; /* avoid multiple concurrent syncs, e.g. triggered by interval timer + incoming message from unknown user */ @implementation ContactStore { NSDate *lastMaxModificationDate; NSDate *lastFullSyncDate; NSTimer *checkStatusTimer; dispatch_queue_t syncQueue; EntityManager *entityManager; } + (ContactStore*)sharedContactStore { static ContactStore *instance; @synchronized (self) { if (!instance) instance = [[ContactStore alloc] init]; } return instance; } - (id)init { self = [super init]; if (self) { syncQueue = dispatch_queue_create("ch.threema.contactsync", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(syncQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)); /* register a callback to get information about address book changes */ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookChangeDetected:) name:CNContactStoreDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orderChanged:) name:@"ThreemaContactsOrderChanged" object:nil]; entityManager = [[EntityManager alloc] init]; /* update display/sort order prefs to match system */ if (@available(iOS 11.0, *)) { BOOL sortOrder = [[CNContactsUserDefaults sharedDefaults] sortOrder] == CNContactSortOrderGivenName; [[UserSettings sharedUserSettings] setSortOrderFirstName:sortOrder]; } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [[UserSettings sharedUserSettings] setSortOrderFirstName:(ABPersonGetSortOrdering() == kABPersonSortByFirstName)]; #pragma clang diagnostic pop } } return self; } - (void)dealloc { [checkStatusTimer invalidate]; } - (void)addressBookChangeDetected:(NSNotification *)notification { DDLogInfo(@"Address book change detected"); [[ValidationLogger sharedValidationLogger] logString:@"Address book change detected"]; [self synchronizeAddressBookForceFullSync:NO onCompletion:^(BOOL addressBookAccessGranted) { [self updateAllContacts]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil]; } onError:^(NSError *error) { [self updateAllContacts]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil]; }]; } - (Contact*)contactForIdentity:(NSString *)identity { /* check in local DB first */ EntityManager *entityManager = [[EntityManager alloc] init]; Contact *contact = [entityManager.entityFetcher contactForId: identity]; return contact; } - (void)addContactWithIdentity:(NSString *)identity verificationLevel:(int32_t)verificationLevel onCompletion:(void(^)(Contact *contact, BOOL alreadyExists))onCompletion onError:(void(^)(NSError *error))onError { /* check in local DB first */ EntityManager *entityManager = [[EntityManager alloc] init]; NSError *error; Contact *contact = [entityManager.entityFetcher contactForId:identity error:&error]; if (contact) { onCompletion(contact, YES); return; } if (error != nil) { onError(error); } /* not found - request from server */ ServerAPIConnector *apiConnector = [[ServerAPIConnector alloc] init]; [apiConnector fetchIdentityInfo:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) { /* save new contact */ dispatch_async(dispatch_get_main_queue(), ^{ /* save new contact */ Contact *contact = [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:verificationLevel state:state type:type featureMask:featureMask alerts:YES]; /* force synchronisation */ [self synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil]; [WorkDataFetcher checkUpdateWorkDataForce:YES onCompletion:nil onError:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kSafeBackupTrigger object:nil]; onCompletion(contact, NO); }); } onError:^(NSError *error) { onError(error); }]; } - (Contact*)addContactWithIdentity:(NSString*)identity publicKey:(NSData*)publicKey cnContactId:(NSString *)cnContactId verificationLevel:(int32_t)verificationLevel featureMask:(NSNumber *)featureMask alerts:(BOOL)alerts { return [self addContactWithIdentity:identity publicKey:publicKey cnContactId:cnContactId verificationLevel:verificationLevel state:nil type:nil featureMask:featureMask alerts:alerts]; } - (Contact*)addContactWithIdentity:(NSString*)identity publicKey:(NSData*)publicKey cnContactId:(NSString *)cnContactId verificationLevel:(int32_t)verificationLevel state:(NSNumber *)state type:(NSNumber *)type featureMask:(NSNumber *)featureMask alerts:(BOOL)alerts { /* Make sure this is not our own identity */ if ([MyIdentityStore sharedMyIdentityStore].isProvisioned && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) { DDLogInfo(@"Ignoring attempt to add own identity"); return nil; } /* Check if we already have a contact with this identity */ __block BOOL added = NO; __block Contact *contact; [entityManager performSyncBlockAndSafe:^{ contact = [entityManager.entityFetcher contactForId: identity]; if (contact) { DDLogInfo(@"Found existing contact with identity %@", identity); if (![publicKey isEqualToData:contact.publicKey]) { DDLogError(@"Public key doesn't match for existing identity %@!", identity); if (alerts) { [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationErrorPublicKeyMismatch object:nil userInfo:nil]; } return; } } else { added = YES; contact = [entityManager.entityCreator contact]; contact.identity = identity; contact.publicKey = publicKey; contact.featureMask = featureMask; if (state != nil) { contact.state = state; } if (type != nil) { if ([type isEqualToNumber:@1]) { NSMutableOrderedSet *workIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].workIdentities]; if (![workIdentities containsObject:contact.identity]) [workIdentities addObject:contact.identity]; [UserSettings sharedUserSettings].workIdentities = workIdentities; } } } if (contact.verificationLevel == nil || (contact.verificationLevel.intValue < verificationLevel && contact.verificationLevel.intValue != kVerificationLevelFullyVerified) || verificationLevel == kVerificationLevelFullyVerified) contact.verificationLevel = [NSNumber numberWithInt:verificationLevel]; if (contact.workContact == nil) { if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) { contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified]; contact.workContact = [NSNumber numberWithBool:YES]; } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) { contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified]; contact.workContact = [NSNumber numberWithBool:YES]; } else { contact.workContact = [NSNumber numberWithBool:NO]; } } if ([contact.workContact isEqualToNumber:[NSNumber numberWithBool:YES]] && (contact.verificationLevel.intValue == kVerificationLevelWorkVerified || contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified)) { if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) { contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified]; } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) { contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified]; } } // check if this is a trusted contact (like *THREEMA) if ([TrustedContacts isTrustedContactWithIdentity:identity publicKey:publicKey]) { contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified]; } if (cnContactId) { if (contact.cnContactId != nil) { if (![contact.cnContactId isEqualToString:cnContactId]) { /* contact is already linked to a different CNContactID - check if the name matches; if so, the CNContactID may have changed and we need to re-link */ CNContactStore *cnAddressBook = [CNContactStore new]; [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[cnContactId]]; NSError *error; NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); } else { if (cnContacts.count == 1) { CNContact *foundContact = cnContacts.firstObject; NSString *firstName = foundContact.givenName; NSString *lastName = foundContact.familyName; if (contact.firstName != nil && contact.firstName.length > 0 && contact.lastName != nil && contact.lastName.length > 0) { if ([firstName isEqualToString:contact.firstName] && [lastName isEqualToString:contact.lastName]) { DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId); [self linkContact:contact toCnContactId:cnContactId]; } } else if (contact.firstName != nil && contact.firstName.length > 0) { if ([firstName isEqualToString:contact.firstName]) { DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId); [self linkContact:contact toCnContactId:cnContactId]; } } else if (contact.lastName != nil && contact.lastName.length > 0) { if ([lastName isEqualToString:contact.lastName]) { DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId); [self linkContact:contact toCnContactId:cnContactId]; } } else { // No name for the contact to compare, replace the cncontactid DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId); [self linkContact:contact toCnContactId:cnContactId]; } } } } }]; } } else { [self linkContact:contact toCnContactId:cnContactId]; } } }]; if (added) [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddedContact object:contact]; return contact; } - (Contact *)addWorkContactWithIdentity:(NSString *)identity publicKey:(NSData*)publicKey firstname:(NSString *)firstname lastname:(NSString *)lastname { /* Make sure this is not our own identity */ if ([MyIdentityStore sharedMyIdentityStore].isProvisioned && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) { DDLogInfo(@"Ignoring attempt to add own identity"); return nil; } /* Check if we already have a contact with this identity */ __block BOOL added = NO; __block Contact *contact; [entityManager performSyncBlockAndSafe:^{ contact = [entityManager.entityFetcher contactForId: identity]; if (contact) { DDLogInfo(@"Found existing contact with identity %@", identity); if (![publicKey isEqualToData:contact.publicKey]) { DDLogError(@"Public key doesn't match for existing identity %@!", identity); return; } } else { added = YES; contact = [entityManager.entityCreator contact]; contact.identity = identity; contact.publicKey = publicKey; contact.firstName = firstname; contact.lastName = lastname; NSMutableOrderedSet *workIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].workIdentities]; if (![workIdentities containsObject:contact.identity]) [workIdentities addObject:contact.identity]; [UserSettings sharedUserSettings].workIdentities = workIdentities; } contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified]; contact.workContact = [NSNumber numberWithBool:YES]; }]; if (added) [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddedContact object:contact]; [self updateFeatureMasksForContacts:@[contact] onCompletion:^{ } onError:^(NSError *error) { }]; return contact; } - (void)updateContact:(Contact*)contact { if (contact.cnContactId == nil) { return; } CNContactStore *cnAddressBook = [CNContactStore new]; [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]]; NSError *error; NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); } else { CNContact *foundContact = cnContacts.firstObject; [self _updateContact:contact withCnContact:foundContact]; } } }]; } - (void)_updateContact:(Contact *)contact withCnContact:(CNContact *)cnContact { NSString *newFirstName = cnContact.givenName; NSString *newLastName = cnContact.familyName; /* no name? try company name and e-mail address (Outlook auto-generated contacts etc.) */ if (newFirstName.length == 0 && newLastName.length == 0) { NSString *companyName = cnContact.organizationName; if (companyName.length > 0) { newLastName = companyName; } else { /* no name? try e-mail address (Outlook auto-generated contacts etc.) */ if (cnContact.emailAddresses.count > 0) { newLastName = ((CNLabeledValue *)cnContact.emailAddresses.firstObject).value; } } } if (newFirstName != contact.firstName && ![newFirstName isEqual:contact.firstName]) contact.firstName = newFirstName; if (newLastName != contact.lastName && ![newLastName isEqual:contact.lastName]) contact.lastName = newLastName; /* get image, if any */ NSData *newImageData = nil; if (cnContact.imageDataAvailable) { newImageData = cnContact.thumbnailImageData; } if (newImageData != contact.imageData && ![newImageData isEqualToData:contact.imageData]) contact.imageData = newImageData; DDLogVerbose(@"Updated contact %@ %@", contact.firstName, contact.lastName); } - (void)updateAllContacts { NSArray *allContacts = [entityManager.entityFetcher allContacts]; if (allContacts == nil || allContacts.count == 0) { return; } // Migration of verfication level kVerificationLevelWorkVerified and kVerificationLevelWorkFullyVerified to flag workContact [entityManager performAsyncBlockAndSafe:^{ for (Contact *contact in allContacts) { if (contact.workContact == nil || contact.verificationLevel.intValue == kVerificationLevelWorkVerified || contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) { if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) { contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified]; contact.workContact = [NSNumber numberWithBool:YES]; } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) { contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified]; contact.workContact = [NSNumber numberWithBool:YES]; } else { contact.workContact = [NSNumber numberWithBool:NO]; } } } }]; NSArray *linkedContacts = [allContacts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { Contact *contact = (Contact *)evaluatedObject; return contact.cnContactId != nil; }]]; if (linkedContacts == nil || linkedContacts.count == 0) { return; } CNContactStore *cnAddressBook = [CNContactStore new]; [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { [entityManager performAsyncBlockAndSafe:^{ /* go through all contacts and resync with address book; only create address book ref when encountering the first contact that is linked */ int nupdated = 0; for (Contact *contact in linkedContacts) { NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]]; NSError *error; NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); } else { if (cnContacts != nil && cnContacts.count > 0) { CNContact *foundContact = cnContacts.firstObject; [self _updateContact:contact withCnContact:foundContact]; nupdated++; } } } DDLogInfo(@"Updated %d contacts", nupdated); }]; [self updateStatusForAllContacts]; } }]; } - (void)linkContact:(Contact*)contact toCnContactId:(NSString *)cnContactId { /* obtain first/last name from address book */ [entityManager performSyncBlockAndSafe:^{ contact.cnContactId = cnContactId; [self updateContact:contact]; }]; } - (void)unlinkContact:(Contact*)contact { [entityManager performSyncBlockAndSafe:^{ contact.abRecordId = [NSNumber numberWithInt:0]; contact.cnContactId = nil; contact.firstName = nil; contact.lastName = nil; contact.imageData = nil; }]; } - (void)fetchPublicKeyForIdentity:(NSString*)identity onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError { [entityManager performBlock:^{ // check in local DB first Contact *contact = [entityManager.entityFetcher contactForId:identity]; if (contact.publicKey) { onCompletion(contact.publicKey); } else { // not found - request from server if ([UserSettings sharedUserSettings].blockUnknown) { DDLogVerbose(@"Block unknown contacts is on - discarding message"); onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:kBlockUnknownContactErrorCode]); return; } [[IdentityInfoFetcher sharedIdentityInfoFetcher] fetchIdentityInfoFor:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) { dispatch_async(dispatch_get_main_queue(), ^{ // First, check in local DB again, as it may have already been saved in the meantime (in case of parallel requests) Contact *contact = [entityManager.entityFetcher contactForId:identity]; if (contact.publicKey) { onCompletion(contact.publicKey); return; } // Save new contact. Do it on main queue to ensure that it's done by the time we signal completion. [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:kVerificationLevelUnverified state:state type:type featureMask:featureMask alerts:NO]; [self synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil]; [WorkDataFetcher checkUpdateWorkDataForce:YES onCompletion:nil onError:nil]; onCompletion(publicKey); }); } onError:^(NSError * _Nonnull error) { onError(error); }]; } }]; } - (void)prefetchIdentityInfo:(NSSet*)identities onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { NSMutableSet *identitiesToFetch = [NSMutableSet set]; // Skip identities that we already have a contact for for (NSString *identity in identities) { Contact *contact = [entityManager.entityFetcher contactForId:identity]; if (!contact) { [identitiesToFetch addObject:identity]; } } if ([identitiesToFetch count] == 0) { onCompletion(); return; } [[IdentityInfoFetcher sharedIdentityInfoFetcher] prefetchIdentityInfo:identitiesToFetch onCompletion:onCompletion onError:onError]; } - (void)upgradeContact:(Contact*)contact toVerificationLevel:(int)verificationLevel { if ((contact.verificationLevel.intValue < verificationLevel && contact.verificationLevel.intValue != kVerificationLevelFullyVerified) || verificationLevel == kVerificationLevelFullyVerified) { [entityManager performSyncBlockAndSafe:^{ contact.verificationLevel = [NSNumber numberWithInt:verificationLevel]; }]; } } - (void)setWorkContact:(Contact *)contact workContact:(BOOL)workContact { [entityManager performSyncBlockAndSafe:^{ contact.workContact = [NSNumber numberWithBool:workContact]; }]; } - (void)updateProfilePicture:(Contact *)contact imageData:(NSData *)imageData didFailWithError:(NSError **)error { UIImage *image = [UIImage imageWithData:imageData]; if (image == nil) { *error = [ThreemaError threemaError:@"Image decoding failed"]; return; } [entityManager performSyncBlockAndSafe:^{ ImageData *dbImage = [entityManager.entityCreator imageData]; dbImage.data = imageData; dbImage.width = [NSNumber numberWithInt:image.size.width]; dbImage.height = [NSNumber numberWithInt:image.size.height]; contact.contactImage = dbImage; }]; [self removeProfilePictureRequest:contact.identity]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationIdentityAvatarChanged object:contact.identity]; } - (void)deleteProfilePicture:(Contact *)contact { [entityManager performSyncBlockAndSafe:^{ contact.contactImage = nil; }]; [self removeProfilePictureRequest:contact.identity]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationIdentityAvatarChanged object:contact.identity]; } - (void)removeProfilePictureFlagForAllContacts { [entityManager performAsyncBlockAndSafe:^{ NSArray *allContacts = [entityManager.entityFetcher allContacts]; if (allContacts != nil) { for (Contact *contact in allContacts) { contact.profilePictureSended = NO; } } }]; } - (void)removeProfilePictureFlagForContact:(NSString *)identity { [entityManager performAsyncBlockAndSafe:^{ Contact *contact = [entityManager.entityFetcher contactForId:identity]; if (contact != nil) { contact.profilePictureSended = NO; } }]; } - (BOOL)existsProfilePictureRequest:(NSString *)identity { @synchronized (self) { return [[[UserSettings sharedUserSettings] profilePictureRequestList] containsObject:identity]; } } - (void)removeProfilePictureRequest:(NSString *)identity { @synchronized (self) { if ([self existsProfilePictureRequest:identity]) { NSMutableSet *profilePictureRequestList = [NSMutableSet setWithArray:[UserSettings sharedUserSettings].profilePictureRequestList]; [profilePictureRequestList removeObject:identity]; [UserSettings sharedUserSettings].profilePictureRequestList = profilePictureRequestList.allObjects; } } } - (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError { [self synchronizeAddressBookForceFullSync:forceFullSync ignoreMinimumInterval:NO onCompletion:onCompletion onError:onError]; } - (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) { return; } /* Get all entries from the user's address book, hash the e-mail addresses and phone numbers and send to the server. */ if (![UserSettings sharedUserSettings].syncContacts) { // trigger updating of status for identities dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{ [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval]; if (onCompletion != nil) { dispatch_async(dispatch_get_main_queue(), ^{ onCompletion(NO); }); } }); return; } CNContactStore *cnAddressBook = [CNContactStore new]; if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] != CNAuthorizationStatusAuthorized) { [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { [self synchronizeAddressBookForceFullSync:forceFullSync onCompletion:onCompletion onError:onError]; } else { DDLogInfo(@"Address book access has NOT been granted: %@", error); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{ [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval]; }); if (onCompletion != nil) dispatch_async(dispatch_get_main_queue(), ^{ onCompletion(NO); }); } }]; return; } if (cnAddressBook == nil) { DDLogInfo(@"Address book is nil"); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{ [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval]; }); return; } dispatch_async(syncQueue, ^{ NSUserDefaults *defaults = [AppGroup userDefaults]; NSDate *lastServerCheck = [defaults objectForKey:@"ContactsSyncLastCheck"]; NSInteger lastServerCheckInterval = [defaults integerForKey:@"ContactsSyncLastCheckInterval"]; BOOL fullServerSync = YES; /* calculate earliest date for next server check */ if (lastServerCheck != nil) { if (-[lastServerCheck timeIntervalSinceNow] < lastServerCheckInterval) { DDLogInfo(@"Last server contacts sync less than %ld seconds ago", (long)lastServerCheckInterval); if (forceFullSync) { DDLogInfo(@"Forcing full sync"); } else { fullServerSync = NO; } } } /* check if we are within the minimum interval */ if (fullServerSync) { if (!ignoreMinimumInterval && lastFullSyncDate != nil && -[lastFullSyncDate timeIntervalSinceNow] < minimumSyncInterval) { DDLogInfo(@"Still within minimum interval - not syncing"); if (onCompletion != nil) dispatch_async(dispatch_get_main_queue(), ^{ onCompletion(YES); }); return; } lastFullSyncDate = [NSDate date]; } [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: build all e-mail and phone number hashes"]; /* extract all e-mail and phone number hashes from the user's address book */ [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { NSError *error; NSMutableArray *allCNContacts = [NSMutableArray new]; NSArray *containers = [cnAddressBook containersMatchingPredicate:nil error:&error]; for (CNContainer *container in containers) { NSPredicate *predicate = [CNContact predicateForContactsInContainerWithIdentifier:container.identifier]; NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; [allCNContacts addObjectsFromArray:cnContacts]; } [self processAddressBookContacts:allCNContacts fullServerSync:fullServerSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError]; } }]; }); } - (void)processAddressBookContacts:(NSArray*)contacts fullServerSync:(BOOL)fullServerSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError { NSUserDefaults *defaults = [AppGroup userDefaults]; NSSet *emailLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]]; NSSet *mobileNoLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]]; PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance]; NSString *countryCode = [PhoneNumberNormalizer userRegion]; DDLogInfo(@"Current country code: %@", countryCode); NSMutableSet *emailHashesBase64 = [NSMutableSet set]; NSMutableSet *mobileNoHashesBase64 = [NSMutableSet set]; NSMutableDictionary *emailHashToCnContactId = [NSMutableDictionary dictionary]; NSMutableDictionary *mobileNoHashToCnContactId = [NSMutableDictionary dictionary]; for (CNContact *person in contacts) { NSString *cnContactId = person.identifier; NSString *name = [CNContactFormatter stringFromContact:person style:CNContactFormatterStyleFullName]; for (CNLabeledValue *label in person.emailAddresses) { NSString *email = label.value; if (email.length > 0) { NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; NSString *emailHashBase64 = [self hashEmailBase64:emailNormalized]; [emailHashToCnContactId setObject:cnContactId forKey:emailHashBase64]; [emailHashesBase64 addObject:emailHashBase64]; /* Gmail address? If so, hash with the other domain as well */ NSString *emailNormalizedAlt = nil; if ([emailNormalized hasSuffix:@"@gmail.com"]) emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@gmail.com" withString:@"@googlemail.com"]; else if ([emailNormalized hasSuffix:@"@googlemail.com"]) emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@googlemail.com" withString:@"@gmail.com"]; if (emailNormalizedAlt != nil) { NSString *emailHashAltBase64 = [self hashEmailBase64:emailNormalizedAlt]; [emailHashToCnContactId setObject:cnContactId forKey:emailHashAltBase64]; [emailHashesBase64 addObject:emailHashAltBase64]; } DDLogVerbose(@"%@ (%@): %@", name, cnContactId, emailNormalized); } } for (CNLabeledValue *label in person.phoneNumbers) { NSString *phone = [label.value stringValue]; if (phone.length > 0) { /* normalize phone number first */ NSString *mobileNoNormalized = [normalizer phoneNumberToE164:phone withDefaultRegion:countryCode prettyFormat:nil]; if (mobileNoNormalized == nil) continue; NSString *mobileNoHashBase64 = [self hashMobileNoBase64:mobileNoNormalized]; [mobileNoHashToCnContactId setObject:cnContactId forKey:mobileNoHashBase64]; [mobileNoHashesBase64 addObject:mobileNoHashBase64]; DDLogVerbose(@"%@ (%@): %@", name, cnContactId, mobileNoNormalized); } } } if (!fullServerSync) { /* a full server sync is not scheduled right now, so remove any hashes that we checked last time from the list */ for (NSString *emailHash in emailLastCheck) { [emailHashesBase64 removeObject:emailHash]; } for (NSString *mobileNoHash in mobileNoLastCheck) { [mobileNoHashesBase64 removeObject:mobileNoHash]; } } if (emailHashesBase64.count == 0 && mobileNoHashesBase64.count == 0) { DDLogInfo(@"No new contacts to synchronize"); if (onCompletion != nil) dispatch_async(dispatch_get_main_queue(), ^{ onCompletion(YES); }); return; } [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"ContactSync: Start request %lu emails, %lu phonenumbers", (unsigned long)emailHashesBase64.count, (unsigned long)mobileNoHashesBase64.count]]; ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn matchIdentitiesWithEmailHashes:[emailHashesBase64 allObjects] mobileNoHashes:[mobileNoHashesBase64 allObjects] includeInactive:NO onCompletion:^(NSArray *identities, int checkInterval) { if (fullServerSync) { [defaults setObject:[emailHashesBase64 allObjects] forKey:@"ContactsSyncLastEmailHashes"]; [defaults setObject:[mobileNoHashesBase64 allObjects] forKey:@"ContactsSyncLastMobileNoHashes"]; [defaults setObject:[NSDate date] forKey:@"ContactsSyncLastCheck"]; } else { /* add the hashes we just checked to the full list */ NSMutableArray *prevEmailHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]]; NSMutableArray *prevMobileNoHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]]; [prevEmailHashes addObjectsFromArray:[emailHashesBase64 allObjects]]; [prevMobileNoHashes addObjectsFromArray:[mobileNoHashesBase64 allObjects]]; [defaults setObject:prevEmailHashes forKey:@"ContactsSyncLastEmailHashes"]; [defaults setObject:prevMobileNoHashes forKey:@"ContactsSyncLastMobileNoHashes"]; } [defaults setInteger:checkInterval forKey:@"ContactsSyncLastCheckInterval"]; [defaults synchronize]; [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: Start core data stuff"]; /* Core data stuff on main thread */ dispatch_async(dispatch_get_main_queue(), ^{ NSSet *excludedIds = [NSSet setWithArray:[UserSettings sharedUserSettings].syncExclusionList]; NSMutableArray *allIdentities = [NSMutableArray new]; for (NSDictionary *identityData in identities) { NSString *identity = [identityData objectForKey:@"identity"]; /* ignore this ID? */ if ([excludedIds containsObject:identity]) continue; NSString *cnContactId = [emailHashToCnContactId objectForKey:[identityData objectForKey:@"emailHash"]]; if (cnContactId == nil) { cnContactId = [mobileNoHashToCnContactId objectForKey:[identityData objectForKey:@"mobileNoHash"]]; } if (cnContactId == nil) { continue; } DDLogVerbose(@"Adding identity %@ to contacts", identity); [allIdentities addObject:identity]; [self addContactWithIdentity:identity publicKey:[[NSData alloc] initWithBase64EncodedString:[identityData objectForKey:@"publicKey"] options:0] cnContactId:cnContactId verificationLevel:kVerificationLevelServerVerified featureMask:nil alerts:NO]; } DDLogNotice(@"ContactSync: Found %lu contacts", (unsigned long)allIdentities.count); // trigger updating of status for identities dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{ [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status and featuremask for all contacts"]; [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval]; }); if (onCompletion != nil) { dispatch_async(dispatch_get_main_queue(), ^{ [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: onCompletion"]; onCompletion(YES); }); } else { [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: finished"]; } }); } onError:^(NSError *error) { DDLogError(@"Synchronize address book failed: %@", error); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Synchronize address book failed: %@", error]]; if (onError != nil) { dispatch_async(dispatch_get_main_queue(), ^{ onError(error); }); } }]; } - (void)linkedIdentities:(NSString*)email mobileNo:(NSString*)mobileNo onCompletion:(void(^)(NSArray *identities))onCompletion { NSArray *emailHashesBase64 = [NSArray array]; NSArray *mobileNoHashesBase64 = [NSArray array]; if (email.length > 0) { NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; NSString *emailHashBase64 = [self hashEmailBase64:emailNormalized]; emailHashesBase64 = @[emailHashBase64]; } if (mobileNo.length > 0) { /* normalize phone number first */ PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance]; NSString *countryCode = [PhoneNumberNormalizer userRegion]; NSString *mobileNoNormalized = [normalizer phoneNumberToE164:mobileNo withDefaultRegion:countryCode prettyFormat:nil]; if (mobileNoNormalized != nil) { NSString *mobileNoHashBase64 = [self hashMobileNoBase64:mobileNoNormalized]; mobileNoHashesBase64 = @[mobileNoHashBase64]; } } if (emailHashesBase64.count > 0 || mobileNoHashesBase64.count > 0) { ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn matchIdentitiesWithEmailHashes:emailHashesBase64 mobileNoHashes:mobileNoHashesBase64 includeInactive:YES onCompletion:^(NSArray *identities, int checkInterval) { onCompletion(identities); } onError:^(NSError *error) { DDLogError(@"linked identities failed: %@", error); NSArray *emptyArray = [NSArray array]; onCompletion(emptyArray); }]; } else { NSArray *emptyArray = [NSArray array]; onCompletion(emptyArray); } } - (NSArray *)allIdentities { NSFetchRequest *fetchRequest = [entityManager.entityFetcher fetchRequestForEntity:@"Contact"]; fetchRequest.propertiesToFetch = @[@"identity"]; NSArray *result = [entityManager.entityFetcher executeFetchRequest:fetchRequest]; if (result != nil) { return [self identitiesForContacts:result]; } else { DDLogError(@"Cannot get identities"); return nil; } } - (NSArray *)contactsWithVerificationLevel:(NSInteger)verificationLevel { return [entityManager.entityFetcher contactsWithVerificationLevel:verificationLevel]; } - (NSArray *)contactsWithFeatureMaskNil { return [entityManager.entityFetcher contactsWithFeatureMaskNil]; } - (NSArray *)allContacts { return [entityManager.entityFetcher allContacts]; } - (void)orderChanged:(NSNotification*)notification { [entityManager performAsyncBlockAndSafe:^{ /* update display name and sort index of all contacts */ NSArray *allContacts = [entityManager.entityFetcher allContacts]; if (allContacts != nil) { for (Contact *contact in allContacts) { /* set last name again to trigger update of display name and sort index */ contact.lastName = contact.lastName; } } }]; } - (NSArray *)identitiesForContacts:(NSArray *)contacts { NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count]; for (Contact *contact in contacts) { [identities addObject:contact.identity]; } return identities; } - (NSArray *)validIdentitiesForContacts:(NSArray *)contacts { NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count]; for (Contact *contact in contacts) { if (contact.state.intValue != kStateInvalid) { [identities addObject:contact.identity]; } } return identities; } - (void)updateFeatureMasksForContacts:(NSArray *)contacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { NSArray *identities = [self identitiesForContacts: contacts]; ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn getFeatureMasksForIdentities:identities onCompletion:^(NSArray *featureMasks) { [entityManager performSyncBlockAndSafe:^{ for (NSInteger i=0; i<[identities count]; i++) { NSNumber *featureMask = [featureMasks objectAtIndex: i]; if (featureMask.integerValue >= 0) { NSString *identityString = [identities objectAtIndex:i]; Contact *contact = [entityManager.entityFetcher contactForId: identityString]; contact.featureMask = featureMask; } } }]; onCompletion(); } onError:^(NSError *error) { onError(error); }]; } - (void)updateFeatureMasksForIdentities:(NSArray *)identities onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn getFeatureMasksForIdentities:identities onCompletion:^(NSArray *featureMasks) { [entityManager performSyncBlockAndSafe:^{ for (NSInteger i=0; i<[identities count]; i++) { NSNumber *featureMask = [featureMasks objectAtIndex: i]; if (featureMask.integerValue >= 0) { NSString *identityString = [identities objectAtIndex:i]; Contact *contact = [entityManager.entityFetcher contactForId: identityString]; contact.featureMask = featureMask; } } }]; onCompletion(); } onError:^(NSError *error) { onError(error); }]; } - (BOOL)needCheckStatus:(BOOL)ignoreInterval { if (ignoreInterval) { return YES; } NSUserDefaults *defaults = [AppGroup userDefaults]; NSDate *dateLastCheck = [defaults objectForKey:@"DateLastCheckStatus"]; if (dateLastCheck == nil) { return true; } NSInteger checkInterval = [self getCheckStatusInterval]; NSDate *dateOfNextCheck = [dateLastCheck dateByAddingTimeInterval:checkInterval]; NSDate *now = [NSDate date]; return [now timeIntervalSinceDate:dateOfNextCheck] > 0; } - (void)setupCheckStatusTimer { NSUserDefaults *defaults = [AppGroup userDefaults]; NSDate *now = [NSDate date]; [defaults setObject:now forKey:@"DateLastCheckStatus"]; [defaults synchronize]; NSInteger checkInterval = [self getCheckStatusInterval]; checkStatusTimer = [NSTimer scheduledTimerWithTimeInterval:checkInterval target:self selector:@selector(updateStatusForAllContacts) userInfo:nil repeats:NO]; } - (NSInteger) getCheckStatusInterval { NSUserDefaults *defaults = [AppGroup userDefaults]; NSInteger checkInterval = [defaults integerForKey:@"CheckStatusInterval"]; return MAX(checkInterval, MIN_CHECK_INTERVAL); } - (void)updateStatusForAllContacts { [self updateStatusForAllContactsIgnoreInterval:NO]; } - (void)updateStatusForAllContactsIgnoreInterval:(BOOL)ignoreInterval { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) { NSArray *contacts = [entityManager.entityFetcher allContacts]; [self updateStatusForContacts:contacts onCompletion:^() { [self setupCheckStatusTimer]; } onError:^(){ [self setupCheckStatusTimer]; }]; } else { if ([self needCheckStatus:ignoreInterval] == NO) { [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: do not update status and featuremasks"]; return; } NSArray *contacts = [entityManager.entityFetcher allContacts]; [self updateStatusForContacts:contacts onCompletion:^() { [self setupCheckStatusTimer]; [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status and featuremasks finished"]; } onError:^(){ [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status featuremasks finished with error"]; [self setupCheckStatusTimer]; }]; } } - (void)updateStatusForContacts:(NSArray *)contacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(void))onError { NSArray *identities = [self validIdentitiesForContacts: contacts]; ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn checkStatusOfIdentities:identities onCompletion:^(NSArray *states, NSArray *types, NSArray *featureMasks, int checkInterval) { [entityManager performSyncBlockAndSafe:^{ NSMutableOrderedSet *workIdentities = [NSMutableOrderedSet new]; for (NSInteger i=0; i<[identities count]; i++) { NSNumber *state = [states objectAtIndex: i]; NSNumber *type = [types objectAtIndex:i]; NSNumber *featureMask = [featureMasks objectAtIndex:i]; NSString *identityString = [identities objectAtIndex:i]; Contact *contact = [entityManager.entityFetcher contactForId: identityString]; if (![contact.state isEqualToNumber:state]) contact.state = state; if ([type isEqualToNumber:@1]) { [workIdentities addObject:contact.identity]; } if (![featureMask isEqual:[NSNull null]]) { if (![contact.featureMask isEqualToNumber:featureMask]) { contact.featureMask = featureMask; } } } if (![[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) { [UserSettings sharedUserSettings].workIdentities = workIdentities; } }]; NSUserDefaults *defaults = [AppGroup userDefaults]; [defaults setInteger:checkInterval forKey:@"CheckStatusInterval"]; [defaults synchronize]; onCompletion(); } onError:^(NSError *error) { DDLogError(@"Status update failed: %@", error); onError(); }]; } - (void)updateAllContactsToCNContact { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" NSUserDefaults *defaults = [AppGroup userDefaults]; if ([defaults boolForKey:@"AlreadyUpdatedToCNContacts"]) { return; } NSArray *linkedContacts = [[entityManager.entityFetcher allContacts] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { Contact *contact = (Contact *)evaluatedObject; return contact.abRecordId != nil && contact.abRecordId.intValue != 0; }]]; if (linkedContacts == nil || linkedContacts.count == 0) { NSUserDefaults *defaults = [AppGroup userDefaults]; [defaults setBool:YES forKey:@"AlreadyUpdatedToCNContacts"]; [defaults synchronize]; return; } CNContactStore *cnAddressBook = [CNContactStore new]; [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { ABAddressBookRef addressBook = nil; int nupdated = 0; for (Contact *contact in linkedContacts) { if (addressBook == nil) { addressBook = ABAddressBookCreate(); if (addressBook == nil) return; } ABRecordRef abPerson = ABAddressBookGetPersonWithRecordID(addressBook, contact.abRecordId.intValue); if (abPerson != nil) { NSString *firstName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonFirstNameProperty)); NSString *lastName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonLastNameProperty)); NSString *middleName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonMiddleNameProperty)); NSString *company = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonOrganizationProperty)); NSString *fullName = [NSString stringWithFormat:@"%@ %@ %@", firstName, middleName, lastName]; ABMutableMultiValueRef multiPhone = ABRecordCopyValue(abPerson, kABPersonPhoneProperty); NSMutableArray *personPhones = [NSMutableArray new]; if (ABMultiValueGetCount(multiPhone) > 0) { for (CFIndex i = 0; i < ABMultiValueGetCount(multiPhone); i++) { CFStringRef phoneRef = ABMultiValueCopyValueAtIndex(multiPhone, i); [personPhones addObject:(__bridge NSString *)phoneRef]; CFRelease(phoneRef); } } CFRelease(multiPhone); ABMutableMultiValueRef multiEmail = ABRecordCopyValue(abPerson, kABPersonEmailProperty); NSMutableArray *personEmails = [NSMutableArray new]; if (ABMultiValueGetCount(multiEmail) > 0) { for (CFIndex i = 0; i < ABMultiValueGetCount(multiEmail); i++) { CFStringRef emailRef = ABMultiValueCopyValueAtIndex(multiEmail, i); [personEmails addObject:(__bridge NSString *)emailRef]; CFRelease(emailRef); } } CFRelease(multiEmail); // Check is there a CNContact for the ABPerson NSPredicate *predicate = [CNContact predicateForContactsMatchingName:fullName]; NSError *error; NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); } else { if (cnContacts.count == 1) { NSLog(@"Found the CNContact for ABPerson; Identifier: %@", [((CNContact *)cnContacts.firstObject) identifier]); [entityManager performSyncBlockAndSafe:^{ contact.cnContactId = [((CNContact *)cnContacts.firstObject) identifier]; }]; } else if (cnContacts.count > 1) { // Find correct contact in array NSMutableArray *phoneEmailMatch = [NSMutableArray new]; NSMutableArray *phoneMatch = [NSMutableArray new]; NSMutableArray *emailMatch = [NSMutableArray new]; for (CNContact *contact in cnContacts) { if ([company isEqualToString:contact.organizationName]) { // compare ABPerson numbers with CNContact numbers BOOL foundPhone = NO; for (NSString *abPhone in personPhones) { for (CNLabeledValue *label in contact.phoneNumbers) { NSString *phoneNumber = [label.value stringValue]; if (phoneNumber.length > 0) { if ([phoneNumber isEqualToString:abPhone]) { foundPhone = YES; } else { foundPhone = NO; } } } } // compare ABPerson emails with CNContact emails BOOL foundEmail = NO; for (NSString *abEmail in personEmails) { for (CNLabeledValue *label in contact.emailAddresses) { NSString *email = label.value; if (email.length > 0) { if ([email isEqualToString:abEmail]) { foundEmail = YES; } else { foundEmail = NO; } } } } if (foundEmail && foundPhone) { [phoneEmailMatch addObject:contact]; } else { if (foundEmail) { [emailMatch addObject:contact]; } if (foundPhone) { [phoneMatch addObject:contact]; } } } } // compare is only one contact with mail and phone match if (phoneEmailMatch.count == 1) { [entityManager performSyncBlockAndSafe:^{ NSLog(@"Found phone and email of the CNContact for ABPerson; Identifier: %@", [((CNContact *)phoneEmailMatch.firstObject) identifier]); contact.cnContactId = [((CNContact *)phoneEmailMatch.firstObject) identifier]; }]; } else if (phoneMatch.count == 1 && emailMatch.count == 0) { [entityManager performSyncBlockAndSafe:^{ NSLog(@"Found phone of the CNContact for ABPerson; Identifier: %@", [((CNContact *)phoneMatch.firstObject) identifier]); contact.cnContactId = [((CNContact *)phoneMatch.firstObject) identifier]; }]; } else if (emailMatch.count == 1 && phoneMatch.count == 0) { [entityManager performSyncBlockAndSafe:^{ NSLog(@"Found email of the CNContact for ABPerson; Identifier: %@", [((CNContact *)emailMatch.firstObject) identifier]); contact.cnContactId = [((CNContact *)emailMatch.firstObject) identifier]; }]; } else { NSLog(@"Found %lu contacts that could match", phoneEmailMatch.count + phoneMatch.count + emailMatch.count); } } else { NSLog(@"Found no CNContact for ABPerson"); // skip } } nupdated++; } } if (addressBook != nil) CFRelease(addressBook); DDLogInfo(@"Updated %d contacts to CNContact", nupdated); NSUserDefaults *defaults = [AppGroup userDefaults]; [defaults setBool:YES forKey:@"AlreadyUpdatedToCNContacts"]; [defaults synchronize]; } }]; #pragma clang diagnostic pop } - (void)cnContactAskAccessEmailsForContact:(Contact *)contact completionHandler:(void (^)(BOOL granted, NSArray *array))completionHandler { if (contact.cnContactId == nil) completionHandler(YES, nil); __block NSArray *cnContacts; CNContactStore *cnAddressBook = [CNContactStore new]; [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { NSError *error; NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]]; NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); completionHandler(YES, nil); } else { cnContacts = tmpCnContacts; NSMutableArray *emails = [NSMutableArray new]; if (cnContacts.count == 1) { for (CNContact *person in cnContacts) { for (CNLabeledValue *label in person.emailAddresses) { NSMutableDictionary *dict = [NSMutableDictionary new]; NSString *emailLabel = label.label; NSString *email = label.value; if (email.length > 0) { [dict setValue:[CNLabeledValue localizedStringForLabel:emailLabel] forKey:@"label"]; [dict setValue:email forKey:@"address"]; [emails addObject:dict]; } } } } completionHandler(YES, emails); } } else { completionHandler(NO, nil); } }]; } - (void)cnContactAskAccessPhoneNumbersForContact:(Contact *)contact completionHandler:(void (^)(BOOL granted, NSArray *array))completionHandler { if (contact.cnContactId == nil) completionHandler(YES, nil); __block NSArray *cnContacts; CNContactStore *cnAddressBook = [CNContactStore new]; [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted == YES) { NSError *error; NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]]; NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); completionHandler(YES, nil); } else { cnContacts = tmpCnContacts; NSMutableArray *phoneNumbers = [NSMutableArray new]; if (cnContacts.count == 1) { for (CNContact *person in cnContacts) { for (CNLabeledValue *label in person.phoneNumbers) { NSMutableDictionary *dict = [NSMutableDictionary new]; NSString *phoneLabel = label.label; NSString *phone = [label.value stringValue]; if (phone.length > 0) { [dict setValue:[CNLabeledValue localizedStringForLabel:phoneLabel] forKey:@"label"]; [dict setValue:phone forKey:@"number"]; [phoneNumbers addObject:dict]; } } } } completionHandler(YES, phoneNumbers); } } else { completionHandler(NO, nil); } }]; } - (NSArray *)cnContactEmailsForContact:(Contact *)contact { if (contact.cnContactId == nil) return nil; __block NSArray *cnContacts; CNContactStore *cnAddressBook = [CNContactStore new]; NSError *error; NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]]; NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); return nil; } else { cnContacts = tmpCnContacts; NSMutableArray *emails = [NSMutableArray new]; if (cnContacts.count == 1) { for (CNContact *person in cnContacts) { for (CNLabeledValue *label in person.emailAddresses) { NSMutableDictionary *dict = [NSMutableDictionary new]; NSString *emailLabel = label.label; NSString *email = label.value; if (email.length > 0) { [dict setValue:[CNLabeledValue localizedStringForLabel:emailLabel] forKey:@"label"]; [dict setValue:email forKey:@"address"]; [emails addObject:dict]; } } } } return emails; } } - (NSArray *)cnContactPhoneNumbersForContact:(Contact *)contact { if (contact.cnContactId == nil) return nil; __block NSArray *cnContacts; CNContactStore *cnAddressBook = [CNContactStore new]; NSError *error; NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]]; NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error]; if (error) { NSLog(@"error fetching contacts %@", error); return nil; } else { cnContacts = tmpCnContacts; NSMutableArray *phoneNumbers = [NSMutableArray new]; if (cnContacts.count == 1) { for (CNContact *person in cnContacts) { for (CNLabeledValue *label in person.phoneNumbers) { NSMutableDictionary *dict = [NSMutableDictionary new]; NSString *phoneLabel = label.label; NSString *phone = [label.value stringValue]; if (phone.length > 0) { [dict setValue:[CNLabeledValue localizedStringForLabel:phoneLabel] forKey:@"label"]; [dict setValue:phone forKey:@"number"]; [phoneNumbers addObject:dict]; } } } } return phoneNumbers; } } - (NSString*)hashEmailBase64:(NSString*)email { NSData *emailHashKeyData = [NSData dataWithBytes:emailHashKey length:sizeof(emailHashKey)]; return [[CryptoUtils hmacSha256ForData:[email dataUsingEncoding:NSASCIIStringEncoding] key:emailHashKeyData] base64EncodedStringWithOptions:0]; } - (NSString*)hashMobileNoBase64:(NSString*)mobileNo { NSData *mobileNoHashKeyData = [NSData dataWithBytes:mobileNoHashKey length:sizeof(mobileNoHashKey)]; return [[CryptoUtils hmacSha256ForData:[mobileNo dataUsingEncoding:NSASCIIStringEncoding] key:mobileNoHashKeyData] base64EncodedStringWithOptions:0]; } @end