How Do I Search Contacts for String in EMail Address?

How Do I Search For String in Contact’s EMail Address?

Running Contacts 9.0 (1679.10) on macOS 10.11.6

For example, I want to find all Contacts that use a specific domain in their email address.
I need a list of Contact Full Name, with associated emails that match.

I’ve done a lot of searching on the 'net, and have not found anything useful.
Here is the script I have so far. I know I can continue it and brute-force a solution, but surely there must be a better method.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

use BPLIB : script "BridgePlus" version "1.3.2"

--- SEARCH EMAIL ADDRESS FOR STRING ---
set emailFilter to "gmail"

--- I WANT RESULTS LIKE THIS ---
-- {"Full Name", {"email address1", "email address2"}
set desiredResult to {¬
{"John Doe", {"jdoe_home@...", "jdoe_work@..."}}, ¬
{"Jack Smith", "jsmith@..."}}


tell application "Contacts"

--- RETURNS EMPTY ITEM FOR PERSONS, THAT DO NOT HAVE MATCHING EMAIL ---
set emailList to value of every email of every person whose (value contains emailFilter)

### FAILS:  DOES NOT WORK ###
#  set peoList to name of every person whose email's value contains emailFilter

### SO I HAVE TO USE THIS ###
# which returns ALL Contacts
# when I just want those with matching emails

set peoList to name of every person

end tell

--- THIS WORKS NICELY, BUT ... ---
-- how to I associate Contact Name with each email?

set emailClean to BPLIB's listByDeletingBlanksIn:emailList

Example Results

I got it down to this:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

tell application "Contacts"
	set theEmails to (every email where its value ends with "@latenightsw.com") of every person
	set theResult to {}
	
	repeat with anEmail in theEmails
		repeat with anItem in items of contents of anEmail
			set end of theResult to value of contents of anItem
		end repeat
	end repeat
end tell

Oops: didn’t notice that you wanted the name as well…

1 Like

Okay, try this. There is some black magic in this one:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions


tell application "Contacts"
	set theEmails to (every email where its value ends with "@latenightsw.com") of every person
	set theEmailAddresses to {}
	set theNames to {}
	
	repeat with anEmail in theEmails
		repeat with anItem in items of contents of anEmail
			set thePerson to «class from» of (contents of anItem as record)
			set end of theEmailAddresses to value of contents of anItem
			set end of theNames to (thePerson's first name) & " " & (thePerson's last name)
		end repeat
	end repeat
end tell

NOTE: This will NOT work when run with SD’s debugging enabled - sigh. Also, AppleScript converts the «class from» into from when compiling which won’t compile after future edits.

1 Like

Thanks, Mark. That’s very clever. I would never have discovered/figured that out.

This is a clever trick to eliminate the empty emails:

repeat with anItem in items of contents of anEmail

This is such a common task (or so it seems to me) that it is amazing that Apple make it so hard. Makes you wonder if they really use their own product (Apple Contacts).

Wow! Is this a bug? Other than copy/paste from a text version, any suggested workarounds?

I guess one should put this in a handler that is very rarely edited.

I read somewhere that the Apple Contacts are actually stored in a MySQL (or SQLite) database. If that is true, I wonder if it would be best to query the DB directly? (especially now that Shane has given us such a great SQLite Lib)

My workaround: created a text expansion for «class from»
When AppleScript throws the compile error, just replace the selected “from”.

I expect its implemented using CoreData which uses SQLite to actually store data. I recommend using the published AddressBook APIs to access this information. The location and structure of this database are not documented and so can change on you from one Mac OS release to the next. Also, there are security issues when directly accessing the underlying database.

You have choices:

  • the Contacts app’s scripting interface
  • the AddressBook Objective-C APIs. The Objective-C APIs are complicated but very flexible.

I assume they still work, but I think the Contacts framework introduced in 10.11 is meant as a replacement. And it’s not particularly ASObjC friendly from what I’ve seen, with very limited predicate support.

It does take some poking around to find out how to do what you want! The AddressBook framework still exists (and works) in El Capitan and requires an equal amount of research. The two have quite different approaches to filtering and handling data but work on the same basic structure as the Contacts application itself. Here’s an effort using the Contacts framework:

use AppleScript version "2.5" -- Mac OS 10.11 (El Capitan) or later.
use framework "Foundation"
use framework "Contacts"

set emailDomain to "gmail.com"

set |⌘| to current application
-- Get a connection to the Contacts database.
set contactStore to |⌘|'s class "CNContactStore"'s new()
-- There's no CNContact predicate for filtering by e-mail address, so fetch every contact. Include e-mail addresses, given names, and family names.
set allContacts to contactStore's unifiedContactsMatchingPredicate:(missing value) keysToFetch:({|⌘|'s CNContactEmailAddressesKey, |⌘|'s CNContactGivenNameKey, |⌘|'s CNContactFamilyNameKey}) |error|:(missing value)
-- Use an NSPredicate to filter the result for contacts with at least one e-mail address ending with the required domain name.
set predicateForHavingAddressInDomain to |⌘|'s class "NSPredicate"'s predicateWithFormat:("ANY emailAddresses.value ENDSWITH %@") argumentArray:({emailDomain})
set contactsWithAddressInDomain to allContacts's filteredArrayUsingPredicate:(predicateForHavingAddressInDomain)

-- Intialise an array for the final result.
set desiredResult to |⌘|'s class "NSMutableArray"'s new()
-- Preset these ObjC values.
set nameFormat to |⌘|'s class "NSString"'s stringWithString:("%@ %@")
set predicateForEndingWithDomain to |⌘|'s class "NSPredicate"'s predicateWithFormat:("self ENDSWITH %@") argumentArray:({emailDomain})
-- Extract the required information from each contact.
repeat with thisContact in contactsWithAddressInDomain
	-- Their name in the order set for the contact.
	set givenName to thisContact's givenName()
	set familyName to thisContact's familyName()
	if ((|⌘|'s class "CNContactFormatter"'s nameOrderForContact:(thisContact)) is |⌘|'s CNContactDisplayNameOrderGivenNameFirst) then
		set displayName to |⌘|'s class "NSString"'s stringWithFormat_(nameFormat, givenName, familyName)
	else
		set displayName to |⌘|'s class "NSString"'s stringWithFormat_(nameFormat, familyName, givenName)
	end if
	-- Their e-mail addresses which end with the required domain name.
	set thisContactsEmailAddresses to (thisContact's valueForKeyPath:("emailAddresses.value"))
	set addressesInDomain to (thisContactsEmailAddresses's filteredArrayUsingPredicate:(predicateForEndingWithDomain))
	-- Add the results to the output array.
	tell desiredResult to addObject:{displayName, addressesInDomain}
end repeat

return desiredResult as list

It certainly looks the goods – but it’s returning an empty array for allContacts here. There’s no error, and adding a CNContact predicate instead of missing value makes no difference. I’m seeing lots of this:

CSSMERR_DL_DATASTORE_DOESNOT_EXIST

in Console. But when I add:

set canI to current application's CNContactStore's authorizationStatusForEntityType:(current application's CNEntityTypeContacts)

It’s telling me I have authorization.

I’ll have another try in the morning after a restart.

Hmmm… I wonder if this is because I have my contacts in iCloud.

Sounds possible. Besides “contacts” and “groups”, the Contacts framework has “containers”, which seem to be equivalent to accounts. Maybe these have to be quizzed individually if you want more than just “On My Mac” results. The version below does that, although I can’t test it against iCloud. There’s no attempt to weed out any duplicate results.

use AppleScript version "2.5" -- Mac OS 10.11 (El Capitan) or later.
use framework "Foundation"
use framework "Contacts"

set emailDomain to "gmail.com"

set |⌘| to current application
-- Get a connection to the Contacts database.
set contactStore to |⌘|'s class "CNContactStore"'s new()
-- Get every container (equivalent to accounts?).
set allContainers to contactStore's containersMatchingPredicate:(missing value) |error|:(missing value)
-- Get all the contacts from each container.
set allContacts to |⌘|'s class "NSMutableArray"'s new()
repeat with thisContainer in allContainers
	set predicateForContactsInContainer to (|⌘|'s class "CNContact"'s predicateForContactsInContainerWithIdentifier:(thisContainer's identifier()))
	set contactsInContainer to (contactStore's unifiedContactsMatchingPredicate:(predicateForContactsInContainer) keysToFetch:({|⌘|'s CNContactEmailAddressesKey, |⌘|'s CNContactGivenNameKey, |⌘|'s CNContactFamilyNameKey}) |error|:(missing value))
	tell allContacts to addObjectsFromArray:(contactsInContainer)
end repeat
-- Use an NSPredicate to filter the result for contacts with at least one e-mail address ending with the required domain name.
set predicateForHavingAddressInDomain to |⌘|'s class "NSPredicate"'s predicateWithFormat:("ANY emailAddresses.value ENDSWITH %@") argumentArray:({emailDomain})
tell allContacts to filterUsingPredicate:(predicateForHavingAddressInDomain)
set contactsWithAddressInDomain to allContacts -- Change of variable name to reflect filtered contents.

-- Intialise an array for the final result.
set desiredResult to |⌘|'s class "NSMutableArray"'s new()
-- Preset these ObjC values.
set nameFormat to |⌘|'s class "NSString"'s stringWithString:("%@ %@")
set predicateForEndingWithDomain to |⌘|'s class "NSPredicate"'s predicateWithFormat:("self ENDSWITH %@") argumentArray:({emailDomain})
-- Extract the required information from each contact.
repeat with thisContact in contactsWithAddressInDomain
	-- Their name in the order set for the contact.
	set givenName to thisContact's givenName()
	set familyName to thisContact's familyName()
	if ((|⌘|'s class "CNContactFormatter"'s nameOrderForContact:(thisContact)) is |⌘|'s CNContactDisplayNameOrderGivenNameFirst) then
		set displayName to |⌘|'s class "NSString"'s stringWithFormat_(nameFormat, givenName, familyName)
	else
		set displayName to |⌘|'s class "NSString"'s stringWithFormat_(nameFormat, familyName, givenName)
	end if
	-- Their e-mail addresses which end with the required domain name.
	set thisContactsEmailAddresses to (thisContact's valueForKeyPath:("emailAddresses.value"))
	set addressesInDomain to (thisContactsEmailAddresses's filteredArrayUsingPredicate:(predicateForEndingWithDomain))
	-- Add the results to the output array.
	tell desiredResult to addObject:{displayName, addressesInDomain}
end repeat

return desiredResult as list

Still no luck here. When I try unifiedMeContactWithKeysToFetch:error: I get an error, but it doesn’t make much sense: Error Domain=CNErrorDomain Code=200 "Updated Record Does Not Exist" UserInfo={NSLocalizedDescription=Updated Record Does Not Exist, NSLocalizedFailureReason=The save request failed because it updates a record that does not exist or has already been deleted.}

If I try containersMatchingPredicate:error:, It only returns one container: <CNContainer: 0x60000126e500: identifier=_local, accountIdentifier=_local, name=On My Mac, type=1 ( Local ), enabled=1, permissions=<CNContainerPermissions: 0x60800020ecb0: canCreateContacts=㈠, canDeleteContacts=%, canCreateGroups=%>>​​​.

It smells a bit like something odd on my Mac. On the one hand I’m just a bit hesitant to fiddle and risk mangling my contacts, and on the other I’m just nervous :unamused:

Thanks, Nigel! :+1:

Works perfectly for me in 0.36 sec in Script Debugger 6.0.5 (6A205) on macOS 10.11.6.

It is not a big difference, but Mark’s version took a whopping 0.56 sec! LOL.
Still, it may be an indicator of performance for large number of Contacts.

I have to admit it seems verbose and complicated to me, but I haven’t studied it yet – just ran a quick test.

Of course, if it can be refactored into a useful handler, then the verboseness doesn’t really matter.

Jim, are your contacts stored in iCloud?

Shane, I think so. But then, I don’t really use Apple Contacts much.
IAC, here’s my list:

OK, I’ve managed to get On My Mac appearing again, and added some contacts to it. Here, Nigel’s script is only finding matches in On My Mac, and not in All iCloud. But it looks like it might be getting values from both on Jim’s – which is what I would have expected.

That’s a relief! :slight_smile: Thanks for reporting back.

[quote]It is not a big difference, but Mark’s version took a whopping 0.56 sec! LOL.
Still, it may be an indicator of performance for large number of Contacts.[/quote]

I’d expect using the framework directly (assuming it was well done!) to be faster than sending commands to the application.

The comments, long variable names, and presetting of ObjC values (I missed a couple!) make it look slightly worse than it is. :wink: But otherwise that’s the nature of ASObjC and the peculiarities of the Contacts framework.

If the script in post 8 is more successful with with your iCloud contacts than it was with Shane’s, you won’t need the post 10 version with its individual container fetches. If you don’t know anyone whose family name goes before their given name, and don’t intend to, you could perhaps hard-code the name order.

That Me contact error’s certainly very strange. Is your Me record in iCloud too? Or there instead of on your computer? Can you access your contacts from the application itself? Have you tried the usual stuff like restarting, running in Script Editor …?

Yes.

Yes – that’s the really strange part, to me.

All of that. It’s telling me I only have one container, On My Mac.

Weird…

So it looks like what I’m seeing is a Sierra issue. There’s no longer access to contacts unless the app is codesigned and with a com.apple.security.personal-information.addressbook entitlement. I’m not sure how to do the latter for an applet, and it may need adding to Script Debugger.

It’s a bit more complicated because the only way I can see to add the entitlement to a project in Xcode is to turn on sandboxing, turn on the entitlement, then modify the entitlement file to turn off sandboxing – which all seems a bit icky.

But at least I know it’s not my system…

I’ve added a new containerOf() function to my MarksLib library.