Contacts.app (AddressBook): Show Existing Info for selected contact

Simple script to check quickly the existing contact info:


tell application id "com.apple.AddressBook" to set thePeople to name of people

set theChoice to choose from list thePeople
if theChoice is false then return

tell application id "com.apple.AddressBook"
	close windows
	set selectedContact to person named (item 1 of theChoice)
	tell selectedContact
		set {|birth date|, |company|, |creation date|, |department|, |first name|} to ¬
			{birth date, company, creation date, department, first name}
		set {|home page|, |id|, |job title|, |last name|, |maiden name|} to ¬
			{home page, id, job title, last name, maiden name}
		set {|middle name|, |modification date|, |name|, |nickname|, |note|, |organization|} to ¬
			{middle name, modification date, name, nickname, note, organization}
		set {|phonetic first name|, |phonetic last name|, |phonetic middle name|} to ¬
			{phonetic first name, phonetic last name, phonetic middle name}
		set {|selected|, |suffix|, |title|, |vcard|} to {selected, suffix, title, vcard}
		set |1st Phone Number| to value of phone 1
	end tell
end tell

tell application "System Events" to open location "addressbook://" & |id|

set theText to "Creation date:                " & |creation date| & return
set theText to theText & "Modification date:          " & |modification date| & return
if not (|1st Phone Number| is missing value) then set theText to "1st Phone number:         " & |1st phone Number| & return
if not (|birth date| is missing value) then set theText to "Birth date:                      " & |birth date| & return
set theText to theText & "Company:                       " & company & return
if not (department is missing value) then set theText to theText & "Department:                   " & department & return
if not (|first name| is missing value) then set theText to theText & "First name:                      " & |first name| & return
if not (|home page| is missing value) then set theText to theText & "Home page:                    " & |home page| & return
if not (|job title| is missing value) then set theText to theText & "Job title:                          " & |job title| & return
if not (|last name| is missing value) then set theText to theText & "Last name:                      " & |last name| & return
if not (|maiden name| is missing value) then set theText to theText & "Maiden name:                 " & |maiden name| & return
if not (|middle name| is missing value) then set theText to theText & "Middle name:                  " & |middle name| & return
if not (|name| is missing value) then set theText to theText & "Name:                              " & |name| & return
if not (nickname is missing value) then set theText to theText & "Nickname:                       " & nickname & return
if not (|note| is missing value) then set theText to theText & "Note:                                " & |note| & return
if not (organization is missing value) then set theText to theText & "Organization:                  " & organization & return
if not (|phonetic first name| is missing value) then set theText to theText & "Phonetic first name:       " & |phonetic first name| & return
if not (|phonetic last name| is missing value) then set theText to theText & "Phonetic last name:        " & |phonetic last name| & return
if not (|phonetic middle name| is missing value) then set theText to theText & "Phonetic middle name:  " & |phonetic middle name| & return
set theText to theText & "ID:                                    " & |id| & return
set theText to theText & "Selected:                         " & selected & return
if not (suffix is missing value) then set theText to theText & "Suffix:                              " & suffix & return
if not (title is "") then set theText to theText & "Title:                                " & title
set theText to theText & return

tell application id "com.apple.AddressBook"
	display dialog theText buttons {" ...........................-------------------                         CLOSE INFO                         --------------------- ............................."} with title "CONTACT's INFO"
	quit it
end tell

return theText & vcard

Hi KniazidisR.

A few observations on trying your script.

  1. The value of the contact’s selected property is always false unless Contacts is already open with the chosen contact selected when the script’s run and the script doesn’t close the windows before reading the ‘selected’ property.
  2. The script errors if the contact doesn’t have a phone number. It’s best to get the phones initially and then, if the result’s not {}, get the first item’s value. Or of course set |1st Phone Number| to missing value if the result is {}.
  3. open location is a StandardAdditions command, so System Events isn’t needed.
  4. Your third and fourth lines setting theText don’t use concatenation, so they lose whatever’s been set already.
  5. (My error. This point deleted.)
  6. The line which appends the title does so if the value’s not “”. On my machine, the absence of a title is indicated by missing value, as with the other properties. On the other hand, one of my contacts has “” instead of missing value as its note value. Maybe deleted text gets rendered thus?
  7. The order in which the information’s appended to the text makes for rather strange reading, but I’m assuming you have your own reasons for this.

All those long if not … then lines are crying out for a repeat, so here’s an attempt to implement one and fix some of the above issues. It’s basically your script revamped.

on main()
	tell application id "com.apple.AddressBook" to set {wasOpen, thePeople} to {running, name of people}
	
	set theChoice to choose from list thePeople
	if theChoice is false then return
	
	tell application id "com.apple.AddressBook"
		set selectedContact to person named (item 1 of theChoice)
		set values to selectedContact's {creation date, modification date, phones, birth date, ¬
			company, department, first name, home page, ¬
			job title, last name, maiden name, middle name, ¬
			name, nickname, note, organization, ¬
			phonetic first name, phonetic last name, phonetic middle name, id, ¬
			selected, suffix, title, vcard}
		set theirPhones to item 3 of values
		if (theirPhones is {}) then
			set item 3 of values to missing value
		else
			set item 3 of values to value of theirPhones's first item
		end if
		if not (wasOpen) then quit
	end tell
	set labels to {"Creation date:                ", "Modification date:          ", "1st Phone number:         ", "Birth date:                       ", ¬
		"Company:                       ", "Department:                   ", "First name:                      ", "Home page:                    ", ¬
		"Job title:                          ", "Last name:                      ", "Maiden name:                 ", "Middle name:                  ", ¬
		"Name:                              ", "Nickname:                       ", "Note:                                ", "Organization:                  ", ¬
		"Phonetic first name:       ", "Phonetic last name:        ", "Phonetic middle name:  ", "ID:                                    ", ¬
		"Selected:                         ", "Suffix:                              ", "Title:                                "}
	
	set output to {}
	repeat with i from 1 to (count labels) -- 1 fewer labels than values. 
		set value to item i of values
		set vClass to value's class
		if (((vClass is text) and (value is not "")) or (vClass is date)) then
			set end of output to item i of labels & value
		else if (vClass is boolean) then
			set end of output to item i of labels & item ((value as integer) + 1) of {"No", "Yes"}
		end if
	end repeat
	set theText to join(output, linefeed)
	set vcard to end of values
	
	display dialog theText buttons {" ...........................-------------------                         CLOSE INFO                         --------------------- ............................."} with title "CONTACT's INFO"
	
	return theText & (linefeed & linefeed & vcard)
end main

on join(lst, delim)
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to delim
	set txt to lst as text
	set AppleScript's text item delimiters to astid
	return txt
end join

main()

Thank you for taking the time to improve my simple script. I will write your version in my script library, because it is really more correct than mine. :slight_smile:

My apologies, KniazidisR. The point I made originally about the company property in your script was nonsense. This property’'s meant to be a boolean and indicates whether or not the contact’s a company as opposed to an individual. A company name, if any, is covered by the organization property. I’ve corrected my post and script above.

And here for your delectation is a version which uses the Contacts framework instead of the Contacts app. It doesn’t include the selected state, which is a property of the app, or the creation and modification dates, for which I can’t find framework equivalents.

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

on main()
	set |⌘| to current application
	
	-- Get every contact in the store, including the properties required for sorting and name formatting.
	set contactStore to |⌘|'s class "CNContactStore"'s new()
	set keyDescriptor to |⌘|'s class "CNContactVCardSerialization"'s descriptorForRequiredKeys()
	-- A nil predicate matches all contacts.
	set allContacts to (contactStore's unifiedContactsMatchingPredicate:(missing value) ¬
		keysToFetch:({keyDescriptor}) |error|:(missing value))
	-- Sort the contacts in the default name order set on the machine.
	set comparator to |⌘|'s class "CNContact"'s comparatorForNameSortOrder:(|⌘|'s CNContactSortOrderUserDefault)
	set allContacts to (allContacts's sortedArrayUsingComparator:(comparator)) as list
	-- Replace them in the list with their formatted names.
	set nameFormatter to |⌘|'s class "CNContactFormatter"'s new()
	repeat with thisContact in allContacts
		set thisContact's contents to (nameFormatter's stringFromContact:(thisContact)) as text
	end repeat
	
	-- Ask the user to choose one of the names.
	set theChoice to (choose from list allContacts with prompt "Which contact?")
	if (theChoice is false) then error number -128
	set chosenName to theChoice's beginning
	
	-- Get the contact associated with the chosen name.
	set namePred to |⌘|'s class "CNContact"'s predicateForContactsMatchingName:(chosenName)
	set chosenContact to (contactStore's unifiedContactsMatchingPredicate:(namePred) ¬
		keysToFetch:({keyDescriptor}) |error|:(missing value))'s firstObject()
	
	-- Get the particular details we want and append them with labels to a list.
	-- Concatenating the details to the AS text labels coerces them to text themselves.
	set output to {"ID:                                    " & chosenContact's identifier(), "Name:                              " & chosenName}
	set labels to {"Title:                                ", "First name:                      ", "Middle name:                  ", ¬
		"Last name:                      ", "Maiden name:                 ", "Suffix:                              ", ¬
		"Nickname:                       ", "Phonetic first name:       ", "Phonetic middle name:  ", ¬
		"Phonetic last name:        ", "Birth date:                       ", "Organization:                  ", ¬
		"Department:                   ", "Job title:                          ", "Company:                        ", ¬
		"1st Phone number:         ", "Home page:                    ", "Note:                                "}
	set values to chosenContact's {namePrefix(), givenName(), middleName(), ¬
		familyName(), previousFamilyName(), nameSuffix(), ¬
		nickname(), phoneticGivenName(), phoneticMiddleName(), ¬
		phoneticFamilyName(), birthday(), organizationName(), ¬
		departmentName(), jobTitle(), contactType(), ¬
		phoneNumbers(), urlAddresses(), |note|()}
	repeat with i from 1 to (count values)
		set value to item i of values
		if (value is missing value) then -- No birth date given.
		else if ((value's class is integer) or (value's isKindOfClass:(|⌘|'s class "NSNumber"))) then -- Company (1) or not (0)? 
			set end of output to (item i of labels) & (item ((value as integer) + 1) of {"No", "Yes"})
		else if (value's isKindOfClass:(|⌘|'s class "NSString")) then -- String value.
			if (value's |length|() > 0) then set end of output to (item i of labels) & value
		else if (value's isKindOfClass:(|⌘|'s class "NSArray")) then -- Phone(s) or Web URL(s).
			if (value's |count|() > 0) then
				set value to value's firstObject()'s value()
				if (value's isKindOfClass:(|⌘|'s class "CNPhoneNumber")) then set value to value's stringValue()
				if (value's |length|() > 0) then set end of output to (item i of labels) & value
			end if
		else -- NSDateComponents object for birth date.
			set end of output to (item i of labels) & (value's |date|() as date)
		end if
	end repeat
	
	set theText to join(output, linefeed)
	display dialog theText buttons {" ...........................-------------------                         CLOSE INFO                         --------------------- ............................."} with title "CONTACT's INFO"
	
	set vcardData to |⌘|'s class "CNContactVCardSerialization"'s dataWithContacts:({chosenContact}) |error|:(missing value)
	set vcard to |⌘|'s class "NSString"'s alloc()'s initWithData:(vcardData) encoding:(|⌘|'s NSUTF8StringEncoding)
	
	return theText & (linefeed & linefeed & vcard)
end main

on join(lst, delim)
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to delim
	set txt to lst as text
	set AppleScript's text item delimiters to astid
	return txt
end join

main()

Thanks for both brilliant versions of the script you provided.

Here is a version of the script that will return the contacts as an AppleScript record…

use AppleScript version "2.5" -- OS X 10.11 (El Capitan) or later.
use framework "Foundation"
use framework "Contacts"
use scripting additions
-- https://developer.apple.com/documentation/contacts/cncontact?language=objc

property ca : current application

on run
	local tid, contactStore, keyDescriptor, comparator, allContacts, thisContact, theChoice, chosenName, chosenContact, namePred, output, labels, values, rec, val, label, aKey, itemLists, aList, anItem
	-- Get every contact in the store, including the properties required for sorting and name formatting.
	set contactStore to ca's class "CNContactStore"'s new()
	set keyDescriptor to ca's class "CNContactVCardSerialization"'s descriptorForRequiredKeys()
	-- A nil predicate matches all contacts.
	set allContacts to (contactStore's unifiedContactsMatchingPredicate:(missing value) ¬
		keysToFetch:({keyDescriptor}) |error|:(missing value))
	-- Sort the contacts in the default name order set on the machine.
	set comparator to ca's class "CNContact"'s comparatorForNameSortOrder:(ca's CNContactSortOrderUserDefault)
	set allContacts to (allContacts's sortedArrayUsingComparator:(comparator)) as list
	-- Replace them in the list with their formatted names.
	set nameFormatter to ca's class "CNContactFormatter"'s new()
	repeat with thisContact in allContacts
		set thisContact's contents to (nameFormatter's stringFromContact:(thisContact)) as text
	end repeat
	
	-- Ask the user to choose one of the names.
	set theChoice to (choose from list allContacts with prompt "Which contact?")
	if (theChoice is false) then error number -128
	set chosenName to theChoice's beginning
	
	-- Get the contact associated with the chosen name.
	set namePred to ca's class "CNContact"'s predicateForContactsMatchingName:(chosenName)
	set chosenContact to (contactStore's unifiedContactsMatchingPredicate:(namePred) ¬
		keysToFetch:({keyDescriptor}) |error|:(missing value)) as list --'s firstObject()
	if chosenContact = missing value then
		set contactStore to missing value
		return false
	end if
	repeat with i from 1 to count chosenContact
		set tmp to (nameFormatter's stringFromContact:(item i of chosenContact))
		if tmp = chosenName then exit repeat
		--(item i of chosenContact)'s givenName()
	end repeat
	set chosenContact to item i of chosenContact
	set rec to {}
	try
		set val to chosenContact's |note|()
	on error errMsg
		beep
		display alert errMsg
	end try
	--"Phonetic-first-name:", "Phonetic-middle-name:", "Phonetic-last-name:", "Note:                                "
	set values to chosenContact's {identifier(), namePrefix(), givenName(), middleName(), familyName(), previousFamilyName(), nameSuffix(), ¬
		nickname(), birthday(), organizationName(), departmentName(), jobTitle(), contactType()}
	--, |note|() ,phoneNumbers(), urlAddresses(), phoneticGivenName(), phoneticMiddleName(), phoneticFamilyName()
	set labels to {"ID", "Prefix", "FirstName", "MiddleName", "LastName", "MaidenName", "Suffix", "Nickname", "Birthday", "Company", "Department", "JobTitle", "ContactType"}
	set output to {}
	repeat with i from 1 to count labels
		set aKey to item i of labels
		set val to (item i of values)
		if (val ≠ "") and (val ≠ missing value) then
			if aKey is "Birthday" then
				set {m, d, y} to val's {|month|(), |day|(), |year|()}
				set val to "{Month: " & m & ", Day:" & d & ", Year:" & y & "}"
			else
				set val to val as text
			end if
			if (val ≠ "") then
				if aKey is in {"Birthday", "ContactType"} then
					set recString to "{" & aKey & ": " & val & "}" --run script ("{" & label & ": " & val & "}")
				else
					set recString to "{" & aKey & ": \"" & val & "\"}" --run script ("{" & label & ": \"" & val & "\"}")
				end if
				set output to output & (run script recString)
			end if
		end if
	end repeat
	set rec to rec & output
	set labels to {"phoneNumber", "address", "email", "url", "date"}
	set itemLists to chosenContact's {phoneNumbers(), postalAddresses(), emailAddresses(), urlAddresses(), |dates|()} --{phoneNumbers, addresses, emails, urls}
	set output to {}
	set tid to text item delimiters
	set text item delimiters to ", "
	repeat with i from 1 to count labels
		set aKey to item i of labels
		set aList to item i of itemLists as list
		set recString to {}
		if (count aList) > 0 then
			repeat with i from 1 to count aList
				set anItem to item i of aList
				set {label, val} to anItem's {label(), value()}
				set label to label as text
				if label = "" then set label to aKey & "_" & i
				if aKey = "phoneNumber" then
					set val to "\"" & val's stringValue() & "\""
				else if aKey = "address" then
					set {street, city, state, zip, country} to val's {street(), city(), state(), postalCode(), country()}
					set val to "{street: \"" & street & "\", city:\"" & city & "\", state:\"" & state & "\", zipCode:\"" & zip & "\", country:\"" & country & "\"}"
				else if aKey is in {"email", "url"} then
					set val to ("\"" & val as text) & "\""
				else if aKey = "date" then
					set {m, d, y} to val's {|month|(), |day|(), |year|()}
					set val to "{Month: " & m & ", Day:" & d & ", Year:" & y & "}"
				end if
				set val to "{label:\"" & label & "\", value:" & val & "}"
				set end of recString to val
			end repeat
			set aKey to aKey & (item (((aKey ends with "s") as integer) + 1) of {"s", "es"})
			set recString to ("{" & aKey & ": {" & recString as text) & "}}"
			set output to run script recString
			set rec to rec & output
		end if
	end repeat
	set text item delimiters to tid
	return rec
end run

Can’t seem to get the “note()” call to work
When I try

chosenContact’s |note|()

I get

-[CNContact notes]: unrecognized selector sent to instance 0x7fa6b7725ef0

** EDIT ** - Due too Nigel’s catch, I edited the line to now use |note|() instead of notes().
I still get the same error

Hi Robert.

The error seems to be coming from this code:

try
	set val to chosenContact's notes()
on error errMsg
	beep
	display alert errMsg
end try

… where you have notes() instead of |note|().

However, there’s an interesting anomaly when I run my version of the script on my Ventura machine. It runs OK in Script Editor, but when fetching the |note|() value in Script Debugger, there’s now an error saying the property wasn’t requested when the contact was fetched. The various properties to fetch with the contacts are defined in the script by the descriptor returned from class "CNContactVCardSerialization"'s descriptorForRequiredKeys(). I don’t know why it includes the note in Script Editor but not in Script Debugger. And to deepen the mystery still further, the Xcode 15.2 and 16.1 documentation says of the note property: “To fetch the note property in iOS 13 or later or macOS 16 or later, add the com.apple.developer.contacts.notes entitlement to your app. The entitlement requires permission from Apple to use, and you can’t publicly distribute your app until you have permission to use it.” macOS is of course only up to version 15 at the moment. :face_with_diagonal_mouth:

I had already caught that and I tried
chosenContact’s |note|()
Same result

Hi Robert. I’ve done some more tests this morning, including on my Sequoia machine, where the difference between running the scripts in Script Editor and in Script Debugger is the same as I described a couple of posts up. The contacts’ notes are successfully acquired in Script Editor; but in Script Debugger, the scripts error with: “A property was not requested when contact was fetched.”

I’ve also tried running my script saved as applications from both SE and SD. On both machines, both applications get the “A property was not requested when contact was fetched.” error. (The application saved from Script Editor on the Sequoia machine (an M3) seizes up after getting permission to access my contacts on its first run and has to be force-quit. If it’s reopened in Script Editor and resaved, it then works perfectly up to “A property was not requested ….” I’ve no idea what’s going on here.)

Explicitly adding a CNContactNoteKey to the keysToFetch: list makes no difference.

I don’t know why you’re still getting an “unrecognised selector” error when I’m getting “A property was not requested”. As far as the latter error’s concerned, the clues suggest that “macOS 16” in the Xcode documentation is a typo and that the requirement for an app to have com.apple.developer.contacts.notes entitlement to access contacts’ notes has been in force since at least macOS 13 Ventura. Script Editor apparently has this entitlement, whereas Script Debugger and scripts saved as applications don’t.

Well that sucks. Any way to give them the entitlements or do LateNightSW have to do that with Script Debugger?

It is a bit disappointing. I can’t think of any security reasons why notes in particular should require special entitlement, although I can imagine it protecting the user from possible embarrassment! :rofl:

I’m not an application developer myself, but I gather it’s a matter of getting a digital certificate from Apple itself to put in the app bundle. Mark or Shane could probably tell you more.

The fact that CNContacts’ notes can still be fetched by scripts running in Script Editor appears to be incidental. They’re also fetched when the scripts are run in Automator (although the ‘value’ variable in my script has to be relabelled to avoid a clash with an Automator keyword), but they’re not fetched when the scripts are run in Script Menu.

The Contacts application (formerly called “Address Book”) can of course still access the notes in the user’s contacts and the scripts for it above still work in any AppleScript-running environment.

Does anyone know how to add an Entitlement to a stay open script app?