Help modifying a script for contacts issue via Script Editor

Hello, I’m looking to modify an existing script I have found online, source. The purpose of this script is to remove the “X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK” text that was added on to hundreds of contacts after importing them from iPhone contacts to iCloud contacts.

The existing script targets the native contact list on the Mac, which works fine, but when trying to get it to target the iCloud contact list, it errors out. Can anyone please help modify the existing script?

-- Define the search string
set search_string to "X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK"

-- Get the list of contacts from the Contacts app
tell application "Contacts"
	set contact_list to people
end tell

-- Iterate through each contact
repeat with contact in contact_list
	set match_found to false

	tell application "Contacts"
		-- Iterate through each phone number of the contact
		repeat with phone_number in phones of contact
			set phone_value to value of phone_number
			
			-- Check if the phone number contains the search string
			if phone_value contains search_string then
				-- Remove the search string from the phone number
				set correct_phone_value to text 1 thru ((offset of search_string in phone_value) - 1) of phone_value
				set value of phone_number to correct_phone_value
				
				set match_found to true
			end if
		end repeat
		
		-- Save the contact if a match was found
		if match_found then save contact
	end tell
end repeat

I tried modifying the “set contact_list to people” to the following below but none have worked so far.

  • “All iCloud”, All iCloud, “iCloud”, iCloud, “General”, General

Below is are images of the error I receive when I run the script.

Thank you!

Trying to target the contact list highlighted in red

Hi @pndr. Welcome to MacScripter.

Contacts.app’s scripting dictionary doesn’t offer a way to target particular accounts and I’m not sure what different accounts are included when it returns people.

I’ve put together a version of the script which targets the Contacts framework directly instead of using the Contacts application. I don’t know if it’ll be any more successful for your purposes, but it’s worth a try. It doesn’t work when run in Script Debugger, possibly because of the thread on which SD runs scripts. But it does work in Script Editor on my system and may do so in the script menu or as an application in its own right.

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

main()

on main()
	set search_string to "X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK"
	
	tell application "Contacts" to if (running) then quit
	
	set |⌘| to current application
	set contactStore to |⌘|'s class "CNContactStore"'s new()
	
	-- Fetch all the contacts in the user's database (regardless of account?)
	-- We're only interested in their phone numbers.
	set contactArray to contactStore's unifiedContactsMatchingPredicate:(missing value) ¬
		keysToFetch:({|⌘|'s CNContactPhoneNumbersKey}) |error|:(missing value)
	
	repeat with thisContact in contactArray
		set match_found to false
		set phones to thisContact's phoneNumbers()'s mutableCopy() -- Editable array of labeled values.
		repeat with labeledValue in phones
			set numberString to labeledValue's value()'s stringValue()
			-- Does the search string occur in the phone number string?
			set bogeyRange to (numberString's rangeOfString:(search_string))
			-- If so, replace the labeled value in the phones list with an edited version.
			if (bogeyRange's |length|() > 0) then
				set match_found to true
				set correctedString to (numberString's substringWithRange:({0, bogeyRange's location()}))
				set newPhoneValue to (|⌘|'s class "CNPhoneNumber"'s phoneNumberWithStringValue:(correctedString))
				set labeledValue's contents to (labeledValue's labeledValueBySettingValue:(newPhoneValue))
			end if
		end repeat
		-- If any of the labeled values were changed, save an edited copy of the contact.
		if (match_found) then
			set mutableContact to thisContact's mutableCopy()
			(mutableContact's setPhoneNumbers:(phones))
			set saveRequest to |⌘|'s class "CNSaveRequest"'s new()
			(saveRequest's updateContact:(mutableContact))
			(contactStore's executeSaveRequest:(saveRequest) |error|:(missing value))
		end if
	end repeat
end main
1 Like

Hello @Nigel_Garvey!

Thank you for the script. It worked flawlessly. It even removed the ones in the General folder I created as a test.

I didn’t realize earlier but email attributes are also affected, as shown below.

Could you please help again by updating the script to target emails instead of phone numbers?

I tried modifying it myself using the article below but was unsuccessful.

Thank you!

Hi @pndr.

Here you go. E-mail addresses, like phone numbers, are labeled values, but their values are the strings themselves instead of structures containing the strings.

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

main()

on main()
	set search_string to "X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK"
	
	tell application "Contacts" to if (running) then quit
	
	set |⌘| to current application
	set contactStore to |⌘|'s class "CNContactStore"'s new()
	
	-- Fetch all the contacts in the user's database (regardless of account?)
	-- In this script, we're only interested in their e-mail addresses.
	set contactArray to contactStore's unifiedContactsMatchingPredicate:(missing value) ¬
		keysToFetch:({|⌘|'s CNContactEmailAddressesKey}) |error|:(missing value)
	
	repeat with thisContact in contactArray
		set match_found to false
		set emailAddresses to thisContact's emailAddresses()'s mutableCopy() -- Editable array of labeled values.
		repeat with labeledValue in emailAddresses
			set addressString to labeledValue's value()
			-- Does the search string occur in the e-mail address string?
			set bogeyRange to (addressString's rangeOfString:(search_string))
			-- If so, replace the labeled value in the emailAddresses list with an edited version.
			if (bogeyRange's |length|() > 0) then
				set match_found to true
				set correctedString to (addressString's substringWithRange:({0, bogeyRange's location()}))
				set labeledValue's contents to (labeledValue's labeledValueBySettingValue:(correctedString))
			end if
		end repeat
		-- If any of the labeled values were changed, save an edited copy of the contact.
		if (match_found) then
			set mutableContact to thisContact's mutableCopy()
			(mutableContact's setEmailAddresses:(emailAddresses))
			set saveRequest to |⌘|'s class "CNSaveRequest"'s new()
			(saveRequest's updateContact:(mutableContact))
			(contactStore's executeSaveRequest:(saveRequest) |error|:(missing value))
		end if
	end repeat
end main

It worked perfectly again. Thank you for all your help!

Here for completeness is a combination of the above two scripts which checks the phone numbers and e-mail addresses in the same run:

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

main() 

on main()
	set search_string to "X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK"
	
	tell application "Contacts" to if (running) then quit
	
	set |⌘| to current application
	-- Fetch all the contacts from the user's Contacts database, including their phone numbers and e-mail addresses.
	set contactStore to |⌘|'s class "CNContactStore"'s new()
	set contactArray to contactStore's unifiedContactsMatchingPredicate:(missing value) ¬
		keysToFetch:({|⌘|'s CNContactPhoneNumbersKey, |⌘|'s CNContactEmailAddressesKey}) |error|:(missing value)
	-- Work through each in turn.
	repeat with thisContact in contactArray
		set {phonesCorrected, phoneNumbers} to checkPhoneNumbers(thisContact, search_string)
		set {emailsCorrected, emailAddresses} to checkEmailAddresses(thisContact, search_string)
		if ((phonesCorrected) or (emailsCorrected)) then
			set mutableContact to thisContact's mutableCopy()
			if (phonesCorrected) then (mutableContact's setPhoneNumbers:(phoneNumbers))
			if (emailsCorrected) then (mutableContact's setEmailAddresses:(emailAddresses))
			set saveRequest to |⌘|'s class "CNSaveRequest"'s new()
			(saveRequest's updateContact:(mutableContact))
			(contactStore's executeSaveRequest:(saveRequest) |error|:(missing value))
		end if
	end repeat
end main

on checkPhoneNumbers(thisContact, search_string)
	set match_found to false
	set phoneNumbers to thisContact's phoneNumbers()'s mutableCopy()
	repeat with labeledValue in phoneNumbers
		set numberString to labeledValue's value()'s stringValue()
		set bogeyRange to (numberString's rangeOfString:(search_string))
		if (bogeyRange's |length|() > 0) then
			set match_found to true
			set correctedString to (numberString's substringWithRange:({0, bogeyRange's location()}))
			set newPhoneNumber to (current application's class "CNPhoneNumber"'s phoneNumberWithStringValue:(correctedString))
			set labeledValue's contents to (labeledValue's labeledValueBySettingValue:(newPhoneNumber))
		end if
	end repeat
	return {match_found, phoneNumbers}
end checkPhoneNumbers

on checkEmailAddresses(thisContact, search_string)
	set match_found to false
	set emailAddresses to thisContact's emailAddresses()'s mutableCopy()
	repeat with labeledValue in emailAddresses
		set addressString to labeledValue's value()
		set bogeyRange to (addressString's rangeOfString:(search_string))
		if (bogeyRange's |length|() > 0) then
			set match_found to true
			set correctedString to (addressString's substringWithRange:({0, bogeyRange's location()}))
			set labeledValue's contents to (labeledValue's labeledValueBySettingValue:(correctedString))
		end if
	end repeat
	return {match_found, emailAddresses}
end checkEmailAddresses

Nigel, just wanted to say thanks, I had the same issue and your script fixed it in about a minute. Much appreciated.

Hi Nigel, I just noticed I have these “X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK” also on what the Contacts app calls iCloudNotes, on home pages and on Addresses (sometimes, there is multiple address fields). I tried now for a few days to amend your script but I only made a mess of my 3800 contacts. Luckily I had a backup. Would it be possible to have the script edited to cover addresses, iCloud Notes and home pages please? I am unable to resolve this myself.

Hi @Blue993.

I’ll have to look at this tomorrow (UK time). I can’t find any reference to “iCloud Notes” in Contacts, but such things apparently exist in the Notes application. A URL address has the same structure in Contacts as an e-mail address, so that shouldn’t be difficult. A postal address has several components — street, city, state, etc. Are any particular components affected, or might it be any or all of them?

Hi Nigel, that’s incredibly kind of you. In terms of the physical address, it seems it’s the country that is affected. I think the “iCloud note” is coming from when I have a contact both on exchange and on iCloud, The Contacts app then distinguishes the note on iCloud from the note on Exchange by calling it “iCloud note”. The ones affected are,the iCloud ones only, which probably is just the notes field from in an iCloud contacts entry.

Here you go then, @Blue993. This version also zaps any instances of “X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK” at the ends of URLs, notes, and any fields in postal addresses. Like the previous versions, it only works in Script Editor (not in Script Debugger or Script Menu) — now for the additional reason that only applications which have a special permission from Apple are allowed to access contacts’ note properties. I don’t know the reason for this, but Script Editor appears to be one such application as at macOS 13.7.3 and macOS 15.3.

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

main()

on main()
	set search_string to "X-SHARED-PHOTO-DISPLAY-PREF:ALWAYS_ASK"
	
	tell application "Contacts" to if (running) then quit
	
	set |⌘| to current application
	-- Fetch all the contacts from the user's Contacts database, including their phone numbers, e-mail addresses, URL addresses, postal addresses, and notes.
	set contactStore to |⌘|'s class "CNContactStore"'s new()
	set contactArray to contactStore's unifiedContactsMatchingPredicate:(missing value) ¬
		keysToFetch:({|⌘|'s CNContactPhoneNumbersKey, |⌘|'s CNContactEmailAddressesKey, |⌘|'s CNContactUrlAddressesKey, |⌘|'s CNContactPostalAddressesKey, |⌘|'s CNContactNoteKey}) |error|:(missing value)
	set postalAddressFormatter to |⌘|'s class "CNPostalAddressFormatter"'s new()
	set postalAddressKeys to {|⌘|'s CNPostalAddressCountryKey, |⌘|'s CNPostalAddressISOCountryCodeKey, |⌘|'s CNPostalAddressPostalCodeKey, |⌘|'s CNPostalAddressStateKey, |⌘|'s CNPostalAddressCityKey, |⌘|'s CNPostalAddressStreetKey}
	-- Work through each contact in turn.
	repeat with thisContact in contactArray
		set {phonesCorrected, phoneNumbers} to checkPhoneNumbers(thisContact, search_string)
		set {emailsCorrected, emailAddresses} to checkURLs(thisContact, |⌘|'s CNContactEmailAddressesKey, search_string)
		set {urlsCorrected, urlAddresses} to checkURLs(thisContact, |⌘|'s CNContactUrlAddressesKey, search_string)
		set {addressesCorrected, postalAddresses} to checkPostalAddresses(thisContact, postalAddressFormatter, postalAddressKeys, search_string)
		set {noteCorrected, note} to checkNote(thisContact, search_string)
		if ((phonesCorrected) or (emailsCorrected) or (urlsCorrected) or (addressesCorrected) or (noteCorrected)) then
			set mutableContact to thisContact's mutableCopy()
			if (phonesCorrected) then (mutableContact's setPhoneNumbers:(phoneNumbers))
			if (emailsCorrected) then (mutableContact's setEmailAddresses:(emailAddresses))
			if (urlsCorrected) then (mutableContact's setUrlAddresses:(emailAddresses))
			if (addressesCorrected) then (mutableContact's setPostalAddresses:(postalAddresses))
			if (noteCorrected) then (mutableContact's setNote:(note))
			set saveRequest to |⌘|'s class "CNSaveRequest"'s new()
			(saveRequest's updateContact:(mutableContact))
			(contactStore's executeSaveRequest:(saveRequest) |error|:(missing value))
		end if
	end repeat
end main

on checkPhoneNumbers(thisContact, search_string)
	set bogey_found to false
	set phoneNumbers to thisContact's phoneNumbers()'s mutableCopy()
	repeat with labeledValue in phoneNumbers
		set {corrected, correctedString} to checkString(labeledValue's value()'s stringValue(), search_string)
		if (corrected) then
			set bogey_found to true
			set newPhoneNumber to (current application's class "CNPhoneNumber"'s phoneNumberWithStringValue:(correctedString))
			set labeledValue's contents to (labeledValue's labeledValueBySettingValue:(newPhoneNumber))
		end if
	end repeat
	return {bogey_found, phoneNumbers}
end checkPhoneNumbers

on checkURLs(thisContact, theKey, search_string)
	set bogey_found to false
	set emailAddresses to (thisContact's valueForKey:(theKey))'s mutableCopy()
	repeat with labeledValue in emailAddresses
		set {corrected, correctedString} to checkString(labeledValue's value(), search_string)
		if (corrected) then
			set bogey_found to true
			set labeledValue's contents to (labeledValue's labeledValueBySettingValue:(correctedString))
		end if
	end repeat
	return {bogey_found, emailAddresses}
end checkURLs

on checkPostalAddresses(thisContact, formatter, postalAddressKeys, search_string)
	set |⌘| to current application
	set bogey_found to false
	set postalAddresses to thisContact's postalAddresses()'s mutableCopy()
	repeat with labeledValue in postalAddresses
		set postalAddress to labeledValue's value()
		if ((formatter's stringFromPostalAddress:(postalAddress))'s containsString:(search_string)) then
			set bogey_found to true
			set postalAddress to postalAddress's mutableCopy()
			repeat with thisKey in postalAddressKeys
				set {corrected, correctedString} to checkString(postalAddress's valueForKey:(thisKey), search_string)
				if (corrected) then
					if (thisKey's isEqualToString:(|⌘|'s CNPostalAddressCountryKey)) then
						(postalAddress's setCountry:(correctedString))
					else if (thisKey's isEqualToString:(|⌘|'s CNPostalAddressISOCountryCodeKey)) then
						(postalAddress's setISOCountryCode:(correctedString))
					else if (thisKey's isEqualToString:(|⌘|'s CNPostalAddressPostalCodeKey)) then
						(postalAddress's setPostalCode:(correctedString))
					else if (thisKey's isEqualToString:(|⌘|'s CNPostalAddressStateKey)) then
						(postalAddress's setState:(correctedString))
					else if (thisKey's isEqualToString:(|⌘|'s CNPostalAddressCityKey)) then
						(postalAddress's setCity:(correctedString))
					else if (thisKey's isEqualToString:(|⌘|'s CNPostalAddressStreetKey)) then
						(postalAddress's setStreet:(correctedString))
					end if
				end if
			end repeat
			set labeledValue's contents to (labeledValue's labeledValueBySettingValue:(postalAddress))
		end if
	end repeat
	
	return {bogey_found, postalAddresses}
end checkPostalAddresses

on checkNote(thisContact, search_string)
	return checkString(thisContact's note(), search_string)
end checkNote

on checkString(theString, search_string)
	set bogeyRange to theString's rangeOfString:(search_string)
	set bogey_found to (bogeyRange's |length|() > 0)
	if (bogey_found) then set theString to theString's substringWithRange:({0, bogeyRange's location()})
	return {bogey_found, theString}
end checkString

Edit: Renamed the handler which deals with both e-mail addresses and Web URLs to reduce confusion.

Hi David, the script worked beautifully, thank you so much. I am also analyzing it to learn from you about scripting. I still don’t really understand what happens that causes these “X-SHARED…” strings to be added, it should really not happen in the first place. But your script deals with it beautifully!

Hi @Blue993. Glad the script worked OK for you.

If you’re just starting to learn AppleScript, it would probably be better to look at some of the simpler scripts on this site first, even though the current task’s the one that currently interests you. My scripts above use what’s known as AppleScriptObjC (or ASObjC) and are a thorough mixture of AppleScript and a way of addressing Objective-C routines in the operating system — namely those in the “Contacts” framework here. While AppleScript’s meant to be able to use terminology from the things it’s controlling, in most cases it’ll be controlling AppleScriptable applications whose terminology’s used with AppleScript syntax. In the script at the top of this thread, for instance, the terms people, phones, value, and save are from the Contacts application’s scripting dictionary, but the syntax is AppleScript throughout. (The save command’s incorrectly used in this script, however. In Contacts, it’s just save (with no parameter), which saves all the unsaved changes in the application.)

Who’s David? :wink:

Apologies Nigel, I got your name wrong