How Do I Search Contacts for String in EMail Address?

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.

Thanks Mark. Works great!

I’ve updated your script to use this new handler, as well as output a results list that I wanted.
BTW, this time it ran in 0.08 sec! Very fast!

(*
  Purpose:  Get List of Contact Name and Associated EMails
                for EMail Addresses that contain a string
                
  Version:  2.0      2017-07-20
  
  Author:    Mark Alldritt (original version)
              JMichaelTX (rev in this version)
                All changes marked with "# JMTX"
*)

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

use MkLib : script "MarksLib" # JMTX Add


tell application "Contacts"
  set theEmails to (every email where its value contains "gmail") of every person # JMTX Chg
  set theEmailAddresses to {}
  set theNames to {}
  set nameEmailList to {} # JMTX Add
  
  repeat with anEmail in theEmails
    set perName to "" # JMTX Add
    set emailList to {} # JMTX Add
    
    repeat with anItem in items of contents of anEmail
      
      --- Make Use of Mark's New contanterOf() Handler ---
      set thePerson to MkLib's containerOf(contents of anItem as record) # JMTX Chg
      
      if (perName = "") then set perName to name of thePerson # JMTX Add
      set end of emailList to value of contents of anItem # JMTX Add
      
      set end of theEmailAddresses to value of contents of anItem
      --set end of theNames to (thePerson's first name) & " " & (thePerson's last name)
      set end of theNames to (thePerson's name) # JMTX Chg
    end repeat
    
    --- UPDATE nameEMailList ---      # JMTX Add this entire block
    if (perName ≠ "") then
      set end of nameEmailList to {perName, emailList}
      set perName to ""
      set emailList to {}
    end if
    
  end repeat
end tell

return nameEmailList # JMTX Add

Example Results

This uses the Contacts application, but minimises the number of commands sent to it:

set emailDomain to "gmail.com"

-- Get a list of all contact names and a matching list containing lists of their e-mail addresses.
tell application "Contacts" to set {contactNames, emailAddresses} to {name, value of emails} of people

set nameEmailList to {}

-- Work through the list of e-mail address lists.
repeat with i from 1 to (count emailAddresses)
	-- Get the list for the next person.
	set theseAddresses to item i of emailAddresses
	-- If it's not empty …
	if (theseAddresses is not {}) then
		-- … make a list of all its addresses which end with the required domain name …
		set addressesInDomain to {}
		repeat with thisAddress in theseAddresses
			if (thisAddress ends with emailDomain) then set end of addressesInDomain to thisAddress's contents
		end repeat
		-- … and if this isn't empty, add a list containing the corresponding person name and the found addresses to the output list.
		if (addressesInDomain is not {}) then set end of nameEmailList to {item i of contactNames, addressesInDomain}
	end if
end repeat

return nameEmailList

Thanks for sharing Nigel. This script seems very fast.
Here is an interesting comparison in execution time as reported by the Script Geek app:

Any ideas why your last script, the “non-ASObjC” ver, is so much faster than your ASObjC version for the first run time (0.030 vs 0.321) ?

@ShaneStanley:
In a real world use, say via FastScripts, does this mean the non-ASObjC would be faster for a single execution?

Also interesting is a comparison with Mark’s (@alldritt) version (with my mods):

This would seem to suggest that minimizing commands to the Contacts app greatly reduces execution time, outweighing any benefit of using the whose clause.


--- Nigel's Script ---
tell application "Contacts" to set {contactNames, emailAddresses} to {name, value of emails} of people

--- Mark's Script ---
tell application "Contacts"
  set theEmails to (every email where its value contains "gmail") of every person # JMTX Chg
  
  --- And then later in a loop of theEmails ---
  set thePerson to MkLib's containerOf(contents of anItem as record) # JMTX Chg
end tell

Any thoughts/comments from anyone on this?

For Reference:

  1. Nigel’s non-ASObjC Script

It would be faster for the first execution. Because FastScripts runs all scripts in the same AppleScript component instance, Contacts.framework would be loaded by the first script to use it. ScriptGeek behaves a bit more like an applet or editor, creating a new component instance for each script. (That’s why it breaks out the first run time.)