How Do I Search Contacts for String in EMail Address?

asobjc
foundation

(Shane Stanley) #15

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.


(Nigel Garvey) #16

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.


(Nigel Garvey) #17

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 …?


(Shane Stanley) #18

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…


(Shane Stanley) #19

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…


(Mark Alldritt) #20

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


(Jim Underwood) #21

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


(Nigel Garvey) #22

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

(Jim Underwood) #23

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

(Shane Stanley) #24

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.)


(Shane Stanley) #25

Let me also add that you have to more careful than usual when making speed comparisons with scripts like this, because the results are for one particular search. Things like how many addresses actually match the criterion could skew times differently for different approaches.

And much as I like the Contacts.framework approach, given that it’s very difficult to use generally post-10.11, I’m not sure that it’s one to pursue in practice.


(Nigel Garvey) #26

This is true to varying degrees with all applications. The fewer commands a script sends to an application, the less time it spends overall waiting for requests and replies to pass back and forth. Also, the application itself has to process fewer individual requests, which are all one-offs with no context to it and are probably housekept as such. Sending Contacts one command to return, say, the names of 100 people is faster than sending it one command to return references to the people and then 100 more to look up each person again in turn and return its name.

Most applications implementing ‘whose’ filters seem to stuggle when there are a lot of items to filter. I don’t know why, although one guess is that it may have something to do with the filters being part of the AppleScript implementations rather than what the applications offer normally. Where performance is more important than scripting convenience, and where both the data to be analysed and the data to be returned are AppleScript values, it can make sense to have the application just dump everything relevant to the script and let the script do the thinking, even though it will receive a lot more data than otherwise and maybe require a lot more code.


(koenigyvan) #27

Interesting thread.

Here running 10.12.6 in French

Nigel’s first ASObjC script works flawlessly

Nigel’s 2nd ASObjC script forces Script Debugger to quit.

Nigel’s non ASObjC script behaves flawlessly

Mark’s script (with MarksLib) behaves flawlessly

In MarksLib, the added handler:
on containerOf(theObject)
return from of (theObject as record)
end containerOf

doesn’t compile in Script Editor --> expression prévu(s) mais « of » trouvé(s).


(koenigyvan) #28

What an ass, the from behavior was described in Mark’s message.

With Nigel’s 2nd script using ASObjC, it’s the execution of the instruction :
set allContainers to contactStore’s containersMatchingPredicate:(missing value) |error|:(missing value)
which forces Script Debugger to quit.


(Nigel Garvey) #29

Hi Yvan.

I don’t know what to say about that. The code’s correct according to the documentation and it works for JMichaelTX and me. The obvious first suspects would be your later system (the script doesn’t work for Shane either, but it doesn’t crash) or some setting in SD. :confused:


(Nigel Garvey) #30

Here’s a version using the AddressBook framework. :slight_smile: Note that the Xcode documentation recommends using the Contacts framework instead with Mac OS 10.11 or later.

use AppleScript version "2.4" -- Mac OS 10.10 (Yosemite) or later.
use framework "Foundation"
use framework "AddressBook" -- The Xcode documentation recommends using the Contacts framework instead in 10.11 or later.

set emailDomain to "gmail.com"

set |⌘| to current application
-- Get a connection to the AddressBook database.
set addressBook to |⌘|'s class "ABAddressBook"'s addressBook()
-- Insert "@" before the email domain to be sure in the following search.
set emailDomain to |⌘|'s class "NSString"'s stringWithFormat_("%@%@", "@", emailDomain)
-- Construct a search for persons having e-mails with any labels and values containing the domain name. The 'key' entry isn't relevant to emails.
set searchForContactsHavingEmailInDomain to |⌘|'s class "ABPerson"'s searchElementForProperty:(|⌘|'s kABEmailProperty) label:(missing value) |key|:(missing value) value:(emailDomain) comparison:(|⌘|'s kABContainsSubStringCaseInsensitive)
-- Execute the search.
set contactsHavingEmailInDomain to addressBook's recordsMatchingSearchElement:(searchForContactsHavingEmailInDomain)

-- Intialise an array for the final result.
set desiredResult to |⌘|'s class "NSMutableArray"'s new()
-- Preset these values.
set nameFormat to |⌘|'s class "NSString"'s stringWithString:("%@ %@")
set defaultNameOrder to addressBook's defaultNameOrdering() -- The user's name ordering preference.

repeat with thisPerson in contactsHavingEmailInDomain
	-- Get the person's given and family names and the name order specified for the record.
	set givenName to (thisPerson's valueForProperty:(|⌘|'s kABFirstNameProperty))
	set familyName to (thisPerson's valueForProperty:(|⌘|'s kABLastNameProperty))
	set nameOrder to ((thisPerson's valueForProperty:(|⌘|'s kABPersonFlags)) as integer) mod 64 div 8 * 8 -- |⌘|'s kABNameOrderingMask = 56, although bit 3 isn't used.
	-- If the order's "default", use the default order set in the database.
	if (nameOrder is (|⌘|'s kABDefaultNameOrdering as integer)) then set nameOrder to defaultNameOrder
	-- Put the name together in the specified order.
	if (nameOrder is (|⌘|'s kABFirstNameFirst as integer)) 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
	
	-- Prepare an array for this person's relevant e-mail addresses.
	set addressesInDomain to |⌘|'s class "NSMutableArray"'s new()
	-- Get the email multi-value (an AddressBook-specific class) for this person.
	set thisPersonsEmailMultivalue to (thisPerson's valueForProperty:(|⌘|'s kABEmailProperty))
	-- Extract the values in the prescribed manner and add the relevant ones to the array.
	repeat with i from 0 to (thisPersonsEmailMultivalue's |count|()) - 1
		set thisAddress to (thisPersonsEmailMultivalue's valueAtIndex:(i))
		if ((thisAddress's hasSuffix:(emailDomain)) as boolean) then tell addressesInDomain to addObject:(thisAddress)
	end repeat
	-- When done, add the name and address results to the output array.
	tell desiredResult to addObject:({displayName, addressesInDomain})
end repeat

return desiredResult as list

(koenigyvan) #31

@ Nigel

Don’t worry. Maybe it’s related to the fact that I never use iCloud.

Your late script behaves flawlessly here.


(Jim Underwood) #32

Nigel, thanks for the detailed response and discussion. I just learned a ton of very important script design criteria.

Your guidance needs to be in a well known AppleScript wiki somewhere, so that others can easily find it. Perhaps the wiki at the new applescript@apple-dev.groups.io would be appropriate.

Meanwhile, I’ve Evernoted it for my own reference.

Thanks again.


(Jim Underwood) #33

Shane, thanks for that guidance. So I take that to mean we should use the non-ASObjC solutions for access to the Contacts app.

That would suggest one of these scripts:

From my very limited testing, with only 10 Contacts, Nigel’s script is the fastest (0.03 vs 0.2 sec). If anyone has a large number of Contacts (>> 100), I’d love to know the execution times you get.

My sincere thanks to all who have participated in this thread. I have learned much from you. My initial question has been answered by both Mark and Nigel, and both are very acceptable, but I’m selecting Nigel’s script as the solution because:

  • It is the fastest
  • It avoids the issue with «class from» compile bug.

(Shane Stanley) #34

Likely lifespan is a factor to take into account. This is one of those situations where you have the luxury of multiple approaches, all with reasonable performance. So what you choose is likely to boil down to other factors, which carry different weight for different people.

But it’s entirely possible that some other task involving Contacts can be done reasonably only by one approach or another, in which case it’s good to know there are alternatives.