AppleScript to export individual vCards to backup to Dropbox

I have a script that I know will allow me to:

  • export vCards individually
  • Place them in a Dropbox folder

Here is the code: https://talk.macpowerusers.com/t/contacts-how-often-do-you-clear-them-out/13469/6?u=bocciaman

Someone, please teach me how to apply it.

For me, as script or as application is better. No need osascript:


tell application "Finder"
	if not (exists folder "Backups" of folder "Dropbox" of home) then
		make new folder in (folder "Dropbox" of home) with properties {name:"Backups"}
	end if
	if not (exists folder "vCards" of folder "Backups" of folder "Dropbox" of home) then
		make new folder in (folder "Backups" of folder "Dropbox" of home) with properties {name:"vCards"}
	end if
	set vPath to (folder "vCards" of folder "Backups" of folder "Dropbox" of home) as string
end tell

tell application "Contacts"
	repeat with cardPerson in people
		set nameOfvCard to name of cardPerson & ".vcf"
		set outFile to (open for access file (vPath & nameOfvCard) with write permission)
		write (vcard of cardPerson as text) to outFile
		close access outFile
	end repeat
	quit
end tell

More efficient and safer like this:

set vPath to (path to home folder as text) & "Dropbox:Backups:vCards:"
do shell script "mkdir -p " & quoted form of POSIX path of vPath

tell application "Contacts"
	set {contactNames, vCards} to {name, vcard} of people
	quit
end tell

repeat with i from 1 to (count contactNames)
	set nameOfvCard to (item i of contactNames) & ".vcf"
	set outFile to (open for access file (vPath & nameOfvCard) with write permission)
	try
		set eof outFile to 0
		write (item i of vCards) to outFile as «class utf8»
	end try
	close access outFile
end repeat

What part of the script identifies the path to “my” Dropbox? My path is “/Users/bocciaman/Dropbox/Backups/vCards"

The first part in each script, specifically the lines beginning “set vPath to …”. KniaizidisR uses the Finder’s ‘home’ keyword, which represents the home folder of the user in which the script’s run. This is the “bocciaman” folder in your case. Mine uses the Standard Additions function ‘path to home folder as text’, which does pretty much the same thing. The paths produced in each case are HFS paths: colon-separated and with the name of the disk on the front. The second line of my script uses the POSIX version of the path (slash-separated) in a shell function which creates the “Backups” and “vCards” folders if they don’t already exist. KniazidisR uses the Finder and its specifiers to do the same thing.

Both scripts worked but it only created 55 vcf cards in the Dropbox folder, not the 682 files that I was expecting. Not sure what to edit to fix it.

Model: 2009 iMac
AppleScript: 2.10
Browser: Safari 537.36
Operating System: macOS 10.14

Both scripts above exports to Dropbox contact’s info. Sure you have 682 registered contacts?
Maybe, those is not contacts (that is, persons), but 682 messages from those contacts in your mail application?

The only reasons I can think of for this are either:

• Contacts is only returning 55 of your 682 contacts’ names to the script, or:
• 627 of the names are duplicates, or:
• There’s something about writing to a Dropbox folder which is causing 627 of the vcfs to be lost.

I don’t know of any reason why the first should be true, I although I don’t rule it out.

The second is easy to understand technically. Since the vcf files are named after the contacts, each duplicated name simply causes an existing file to be overwritten. (This does in fact happen with two of my own contacts, which are for different divisions of the same company.) But it’s hard to believe you only have 55 different names among 682 contacts.

I haven’t tested the scripts on my Dropbox folder as I’m loathe to upload my contacts’ details to an unknown destination. But there’s no problem writing them to folders on my Desktop.

Are you able to provide any clues?

You may try again with this edited version

set targetFile to (path to desktop as text) & "vCards_sdraCv.txt"
my writeto(targetFile, "", «class utf8», false) # create an empty log file

set vPath to (path to home folder as text) & "Dropbox:Backups:vCards:"
do shell script "mkdir -p " & quoted form of POSIX path of vPath

tell application "Contacts"
	set {contactNames, vCards} to {name, vcard} of people
	quit
end tell
set treatedNames to {}
repeat with i from 1 to (count contactNames)
	set bareName to (item i of contactNames)
	if bareName is in treatedNames then
		set nameOfvCard to bareName & "_" & i & ".vcf"
	else
		set end of treatedNames to bareName
		set nameOfvCard to bareName & ".vcf"
	end if
	my writeto(targetFile, nameOfvCard & linefeed, «class utf8», true) # report name of every files
	set outFile to (open for access file (vPath & nameOfvCard) with write permission)
	try
		set eof outFile to 0
		write (item i of vCards) to outFile as «class utf8»
	on error errmsg number nbr
		my writeto(targetFile, errmsg & tab & nameOfvCard & linefeed, «class utf8», true) # report error message and name of the offending file
	end try
	close access outFile
end repeat

#=====
(*
Handler borrowed to Regulus6633 - http://macscripter.net/viewtopic.php?id=36861
*)
on writeto(targetFile, theData, dataType, apendData)
	-- targetFile is the path to the file you want to write
	-- theData is the data you want in the file.
	-- dataType is the data type of theData and it can be text, list, record etc.
	-- apendData is true to append theData to the end of the current contents of the file or false to overwrite it
	try
		set targetFile to targetFile as «class furl»
		set openFile to open for access targetFile with write permission
		if not apendData then set eof of openFile to 0
		write theData to openFile starting at eof as dataType
		close access openFile
		return true
	on error
		try
			close access file targetFile
		end try
		return false
	end try
end writeto

#=====

This way vcard with identical name will generate files with different names.

So you will have a track to explain what is at work.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) samedi 2 novembre 2019 14:06:36

Added instructions creating a log report so you will be able to see what was done and what possibly failed.

The AppleScript above this post worked! I was still getting an error about 2 H&R block vcf cards not being found. Therefore, I manually backed up those 2 records and deleted them as contacts. Then the script ran with no errors and I got 1038 individual .vcf cards in my Dropbox folder!

Thank you!

set vPath to (path to home folder as text) & "Dropbox:Backups:vCards:"
do shell script "mkdir -p " & quoted form of POSIX path of vPath

tell application "Contacts"
	set {contactNames, vCards} to {name, vcard} of people
	quit
end tell
set treatedNames to {}
repeat with i from 1 to (count contactNames)
	set bareName to (item i of contactNames)
	if bareName is in treatedNames then
		set nameOfvCard to bareName & "_" & i & ".vcf"
	else
		set end of treatedNames to bareName
		set nameOfvCard to bareName & ".vcf"
	end if
	set outFile to (open for access file (vPath & nameOfvCard) with write permission)
	try
		set eof outFile to 0
		write (item i of vCards) to outFile as «class utf8»
	end try
	close access outFile
end repeat

You were expecting 682 files, only got 55 because of repeated names, used Yvan’s script instead, got 1038 files, and that’s right? :confused:

I think that just means that AppleScript was being stopped once it got to the contacts with the “&” in the name. See the screenshot for the number of contacts.

The availability of an ampersand in the barename is not a problem.

set i to 12 # why not
set barename to "ase&gt" # why not
set nameOfvCard to barename & "_" & i & ".vcf"
--> "ase&gt_12.vcf"

Look at the content of Dropbox.
I guess that you will see some files whose name is .vcf and many others whose name is _.vcf

In message #9, I added instructions creating a log report so you will be able to see what was done and what possibly failed.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) dimanche 3 novembre 2019 11:08:32

Here for fun is an ASObjC version using the Contacts framework. It’s pretty fast. :slight_smile: It assumes a title-forename-surname-honours name order, but I think there’s a setting which can be queried to determine this. I’ll look into it later. (Edit: Now duly looked into and incorporated into the script.)

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

property vPath : "~/Dropbox/Backups/vCards/"
property logPath : "~/Desktop/vCards_sdraCv.txt"

main()

on main()
	set |⌘| to current application
	
	-- Get all the "containers" in the user's Contacts database. (Possibly only one.)
	set contactStore to |⌘|'s class "CNContactStore"'s new()
	set allContainers to contactStore's containersMatchingPredicate:(missing value) |error|:(missing value)
	-- Get all the contacts from all the containers, including the data required for their vCards.
	set allContacts to |⌘|'s class "NSMutableArray"'s new()
	set requiredKeyDescriptor to |⌘|'s class "CNContactVCardSerialization"'s descriptorForRequiredKeys()
	repeat with thisContainer in allContainers
		set containerID to thisContainer's identifier()
		set contactPredicate to (|⌘|'s class "CNContact"'s predicateForContactsInContainerWithIdentifier:(containerID))
		set theseContacts to (contactStore's unifiedContactsMatchingPredicate:(contactPredicate) keysToFetch:({requiredKeyDescriptor}) |error|:(missing value))
		tell allContacts to addObjectsFromArray:(theseContacts)
	end repeat
	
	-- Create the destination folder if it doesn't already exist.
	set expandedVPath to (|⌘|'s class "NSString"'s stringWithString:(vPath))'s stringByExpandingTildeInPath()
	tell |⌘|'s class "NSFileManager"'s defaultManager() to createDirectoryAtPath:(expandedVPath) withIntermediateDirectories:(true) attributes:(missing value) |error|:(missing value)
	
	-- Work through the contacts individually, writing their vCards to the destination folder.
	set person to |⌘|'s CNContactTypePerson
	set regexSearch to |⌘|'s NSRegularExpressionSearch
	set usedNames to |⌘|'s class "NSMutableArray"'s new()
	set duplicatedNamesLog to |⌘|'s class "NSMutableString"'s new()
	set nameFormatter to |⌘|'s class "CNContactFormatter"'s new()
	repeat with i from 1 to (count allContacts)
		set thisContact to item i of allContacts
		-- Create this contact's vCard.
		set vCardData to (|⌘|'s class "CNContactVCardSerialization"'s dataWithContacts:{thisContact} |error|:(missing value))
		-- Get the contact's personal or organisation name, formatted as set for that contact.
		set contactName to (nameFormatter's stringFromContact:(thisContact))
		-- If the name's a duplicate of one already handled, modify it for the file name and add a note to the log text.
		if (usedNames's containsObject:(contactName)) then
			tell duplicatedNamesLog to appendFormat_("%@ -> ", contactName)
			set contactName to (contactName's stringByAppendingString:("_" & i))
			tell duplicatedNamesLog to appendFormat_("%@.vcf%@", contactName, linefeed)
		else
			tell usedNames to addObject:(contactName)
		end if
		-- Construct the file name and the full path and write the vCard data to it.
		set fileName to (contactName's stringByAppendingPathExtension:("vcf"))
		set savePath to (expandedVPath's stringByAppendingPathComponent:(fileName))
		tell vCardData to writeToFile:(savePath) atomically:(true)
	end repeat
	
	-- If there's anything in the log text, write it to file and inform the user.
	if (duplicatedNamesLog's |length|() > 0) then
		set expandedLogPath to (|⌘|'s class "NSString"'s stringWithString:(logPath))'s stringByExpandingTildeInPath()
		tell duplicatedNamesLog to writeToFile:(expandedLogPath) atomically:(true) encoding:(|⌘|'s NSUTF8StringEncoding) |error|:(missing value)
		set {button returned:buttonReturned} to (display alert "PLEASE NOTE" message "One or more of your contacts' names were duplicates and their vCards were saved under modified names. A log of the modified names has been saved to the file \"" & expandedLogPath's lastPathComponent() & "\" on your desktop." as critical buttons {"Open the log file", "OK"} default button "OK")
		if (buttonReturned is "Open the log file") then
			set logFile to expandedLogPath as text as POSIX file
			tell application "Finder" to open logFile
		end if
	end if
end main

Wow. That ASObjC script is ripping fast and works like a charm. Many, many thanks for a great effort and a fully functional, reliable script.

Bravo!

Model: iMac 2017 21.5"
Browser: Safari 605.1.15
Operating System: macOS 10.14

Hello everyone!

This is my first post here.
The ASObjC script is great and I wanted to use it for my purposes. As I found out, “CNContactVCardSerialisation” does not write the → notes and → images to the vCards. There are also articles on this topic (e.g. here), but they don’t help me because of my lack of knowledge. However, you can add the notes and images to the end of the vCards.

I have tried that. It works with the notes, but I can’t get any further with the images. How can I read the data and get a base64-encoded string?

I have also downloaded the ScriptDebugger. There is an error when executing the script. But it works in the Scripteditor.

set thisContactsNote to thisContact's {|note|()} as string
--> Error: A property was not requested when contact was fetched.

Here is my script with the marked changes (and a workaround to save vCards with slashes in the name):

use AppleScript version "2.5"
use framework "Foundation"
use framework "Contacts"
use scripting additions


set vPath to do shell script "currDate=$(date \"+%Y-%m-%d %H.%M.%S\"); echo \"$HOME/Desktop/vCards/vCards Backup ($currDate)\""
set logPath to vPath & "/_log.txt"


set |⌘| to current application

-- Get all the "containers" in the user's Contacts database. (Possibly only one.)
set contactStore to |⌘|'s class "CNContactStore"'s new()
set allContainers to contactStore's containersMatchingPredicate:(missing value) |error|:(missing value)
-- Get all the contacts from all the containers, including the data required for their vCards.
set allContacts to |⌘|'s class "NSMutableArray"'s new()
set requiredKeyDescriptor to |⌘|'s class "CNContactVCardSerialization"'s descriptorForRequiredKeys()
repeat with thisContainer in allContainers
	set containerID to thisContainer's identifier()
	set contactPredicate to (|⌘|'s class "CNContact"'s predicateForContactsInContainerWithIdentifier:(containerID))
	set theseContacts to (contactStore's unifiedContactsMatchingPredicate:(contactPredicate) keysToFetch:({requiredKeyDescriptor}) |error|:(missing value))
	tell allContacts to addObjectsFromArray:(theseContacts)
end repeat

-- Create the destination folder if it doesn't already exist.
set expandedVPath to (|⌘|'s class "NSString"'s stringWithString:(vPath))'s stringByExpandingTildeInPath()
tell |⌘|'s class "NSFileManager"'s defaultManager() to createDirectoryAtPath:(expandedVPath) withIntermediateDirectories:(true) attributes:(missing value) |error|:(missing value)

-- Work through the contacts individually, writing their vCards to the destination folder.
set person to |⌘|'s CNContactTypePerson
set regexSearch to |⌘|'s NSRegularExpressionSearch
set usedNames to |⌘|'s class "NSMutableArray"'s new()
set errorLog to |⌘|'s class "NSMutableString"'s new()
set nameFormatter to |⌘|'s class "CNContactFormatter"'s new()
--set organizationnameFormatter to |⌘|'s class "CNContact.organizationName"'s new()


set c to 2
repeat with i from 1 to (count allContacts)
	set thisContact to item i of allContacts
	-- Create this contact's vCard.
	set vCardData to (|⌘|'s class "CNContactVCardSerialization"'s dataWithContacts:{thisContact} |error|:(missing value))
	
	-- Get the contact's personal or organisation name, formatted as set for that contact.
	set contactName to (nameFormatter's stringFromContact:(thisContact))
	
	##-- If the name's a duplicate of one already handled, modify it for the file name 
	##----- with sequential numbering
	if (usedNames's containsObject:(contactName)) then
		set newcontactName to (contactName's stringByAppendingString:("_" & c))
		set contactName to newcontactName
		set c to c + 1
	else
		tell usedNames to addObject:(contactName)
		set c to 2
	end if
	
	-- Construct the file name and the full path and write the vCard data to it.
	set fileName to (contactName's stringByAppendingPathExtension:("vcf"))
	
	-----------------------------------
	##-----replace "/" in path
	
	if (fileName as string) contains "/" then
		tell me
			try
				set fileName to (do shell script "echo '" & fileName & "' |sed 's/\\//_⊕⊖⊗⊘_/g'")
			on error err
				tell errorLog to appendFormat_(err, linefeed)
			end try
		end tell
	end if
	
	set savePath to (expandedVPath's stringByAppendingPathComponent:(fileName))
	tell vCardData to writeToFile:(savePath) atomically:(true)
	
	
	-----------------------------------
	##--- add image to vCard
	
	set thisContactsImage to thisContact's imageData()
	
	--? 
	
	
	##------ add note to vCard 
	###### working in Scripteditor. Error in Script Debugger: A property was not requested when contact was fetched
	
	set thisContactsNote to thisContact's |note|() as string
	
	if thisContactsNote is not "" then
		try
			set NoteList to {}
			if (count of paragraphs of thisContactsNote) > 1 then
				repeat with aPara in paragraphs of thisContactsNote
					set NoteList to NoteList & aPara & "\\\\n"
				end repeat
			else
				set NoteList to thisContactsNote
			end if
			
			tell me to do shell script "sed -i '' -e '$ d' " & quoted form of (savePath as string) & ";echo 'NOTE:" & (NoteList as string) & "
END:VCARD' >> " & quoted form of (savePath as string)
			
		on error err
			try
				set exist to savePath as string
				exist as POSIX file as alias
				set err to "Notes could not be added to vCard"
			on error
				set err to ""
			end try
			set err to err & "

"
			tell errorLog to appendFormat_(err, linefeed)
		end try
	end if
	
end repeat

-----------------------------------------------------
##---- use Finder to insert "/" back into file name

set slash to do shell script "find " & quoted form of vPath & " -mindepth 1 -maxdepth 1  -name '*_⊕⊖⊗⊘_*' -type f"

set slashes to paragraphs of slash
repeat with i from 1 to count of items of slashes
	set a_slash to (item i of slashes as POSIX file)
	tell application "Finder"
		set n to name of (a_slash as alias)
		tell me
			set nn to replace_chars(n, "_⊕⊖⊗⊘_", "/")
		end tell
		set name of (a_slash as alias) to nn
	end tell
end repeat


-- If there's anything in the log text, write it to file and inform the user.
if (errorLog's |length|() > 0) then
	set expandedLogPath to (|⌘|'s class "NSString"'s stringWithString:(logPath))'s stringByExpandingTildeInPath()
	tell errorLog to writeToFile:(expandedLogPath) atomically:(true) encoding:(|⌘|'s NSUTF8StringEncoding) |error|:(missing value)
	
	set {button returned:buttonReturned} to (display alert "PLEASE NOTE" message "One or more of your contacts or contact's notes maybe not saved! A log has been saved to the file \"_log.txt\" in vCards_Backup folder" as critical buttons {"Open the log file", "OK"} default button "OK")
	if (buttonReturned is "Open the log file") then
		tell application "Finder" to open logPath as POSIX file
	end if
end if

on replace_chars(this_text, search_string, replacement_string)
	set AppleScript's text item delimiters to the search_string
	set the item_list to every text item of this_text
	set AppleScript's text item delimiters to the replacement_string
	set this_text to the item_list as string
	set AppleScript's text item delimiters to ""
	return this_text
end replace_chars

I would appreciate any help.

The script works perfectly, I even duplicated one of my contacts just to see how the log file works. And that’s where I ran into an issue. When the log window pops up to let you know that there were errors, and you click “Open the log file”, I get another window saying I don’t have permission to view the log file. Yet, I can double-click the log file on my desktop and it opens in TextEdit. Any ideas?

Log Window

Error Window

Issue resolved. I was playing with different ideas and changed the name of the log file from “vCards_sdraCv.txt” to “vCards Export Errors” and now when I click “Open the log file” it magically opens. I haven’t a clue as to why (anyone care to offer up an explanation?), but it works now for some reason.

Hi @Homer712.

Sounds like a permissions issue on your machine. Possibly the application running the script needed permission to tell the Finder to open the file. But I’ve no idea how changing the file name has fixed it! :face_with_raised_eyebrow:

Hi @Keita. Welcome to MacScripter! Apologies for this delayed response.

After looking over my script to remind myself how it works, and trying a few things before looking at your modifications or the link you’ve provided, here are my observations about notes not being written to the vCards. I’m afraid I don’t keep images with my own contacts, so I haven’t been able to test with images too.

The input for the unifiedContactsMatchingPredicate:keysToFetch:error: method, which fetches the contacts from the database, has to include a list of keys indicating which properties are wanted with the returned “CNContact” objects. Alternatively, the list can contain a “descriptor” indicating a range of properties. My script uses the “CNContactVCardSerialization” descriptor descriptorForRequiredKeys, which represents the properties that are absolutely required in a contact’s vCard. Apparently, these don’t include any note there may be. So the returned “NSContact” objects already don’t don’t have note properties before the vCard data are extracted from them.

According to the documentation, the fetching method’s keysToFetch: parameter can be a mixture of keys and descriptors. But when I add |⌘|'s CNContactNoteKey to the list, the returned objects for my contacts with notes only gain a property called [b]iOSLegacyIdentifier[b], which has a numeric value and isn’t written to the vCards either. It turns out that in macOS 13 (or iOS 13) and later, the app running the code needs special permission in a plist file to be able to access contacts’ notes.

So to sum up briefly for now: it seems easy enough to add the keys for notes, images, and/or thumbnails to the keysToFetch: list, but actually obtaining the notes may require the script to be saved in a particular way. I’ll look into this this afternoon (GMT) and report back.