AppleScript to export individual vCards to backup to Dropbox

Just to be clear to any one else who may experience this issue, I did not change the name of the log file after it was created by the script. I changed both the destination (I don’t use Dropbox) and the log file name in the script itself.

property vPath : "/Users/homer/Documents/Contacts Export/MacBook Pro Export/vCard Exports/vCards/"
property logPath : "~/Desktop/vCards Export Errors.txt"

Further to my observations in post #20, it doesn’t look good for getting notes from contacts when using the Contacts framework. It needs some sort of authorisation from Apple and, as far as I can see, this authorisation can only be applied to applications built with Xcode.

Better news is that vCard text obtained through the Contacts application’s AppleScript interface does still contain note information as at macOS 14.3.1 (Sonoma). So here’s another version of the script which reverts to that method:

use AppleScript version "2.4" -- OS X 10.10 (Yosemite) or later
use framework "Foundation"
use scripting additions

property vPath : "~/Dropbox/Backups/vCards/"
property logPath : "~/Desktop/vCards_sdraCv.txt"
-- Replacement for slashes and/or colons in contact names when deriving file names.
property slashSubstitute : "-" -- or "_⊕⊖⊗⊘_" for Keita.

main()

on main()
	script o
		property allNames : missing value
		property allVCards : missing value
		property usedNames : {}
		property duplicatedNamesLog : {}
	end script
	
	tell application "Contacts"
		activate
		set {o's allNames, o's allVCards} to people's {name, vcard}
		quit
	end tell
	
	set expandedVPath to (current application's class "NSString"'s stringWithString:(vPath))'s stringByExpandingTildeInPath()
	tell current application's class "NSFileManager"'s defaultManager() to createDirectoryAtPath:(expandedVPath) withIntermediateDirectories:(true) attributes:(missing value) |error|:(missing value)
	set vHFSPath to expandedVPath as text as POSIX file as text
	
	repeat with i from 1 to (count o's allNames)
		set contactName to o's allNames's item i
		if ((contactName contains "/") or (contactName contains ":")) then set contactName to replaceText(contactName, {"/", ":"}, slashSubstitute)
		if (o's usedNames contains contactName) then
			set duplicatedName to contactName
			set contactName to contactName & ("_" & i)
			set o's duplicatedNamesLog's end to duplicatedName & (" -> " & contactName & ".vcf")
		else
			set o's usedNames's end to contactName
		end if
		set vCardData to o's allVCards's item i
		writeToFile(vCardData, vHFSPath & (contactName & ".vcf"), 1, «class utf8», true)
	end repeat
	if (o's duplicatedNamesLog is not {}) then
		set duplicatedNamesLog to join(o's duplicatedNamesLog, linefeed)
		set expandedLogPath to (current application's class "NSString"'s stringWithString:(logPath))'s stringByExpandingTildeInPath()
		writeToFile(duplicatedNamesLog, expandedLogPath as text as POSIX file as text, 1, «class utf8», true)
		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

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

on replaceText(mainText, searchText, replacementText)
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to searchText
	set textItems to mainText's text items
	set AppleScript's text item delimiters to replacementText
	set mainText to textItems as text
	set AppleScript's text item delimiters to astid
	return mainText
end replaceText

on writeToFile(what, HFSPath, pos, encoding, replacingAllContent)
	set fRef to (open for access (HFSPath as «class furl») with write permission)
	if (replacingAllContent) then
		set eof fRef to 0
		set pos to 1
	end if
	try
		if ((what's class is text) and (encoding is not missing value)) then
			write what to fRef starting at pos as encoding
		else
			write what to fRef starting at pos
		end if
		set fileLength to (get eof fRef)
		close access fRef
	on error errMsg
		set fileLength to (get eof fRef)
		close access fRef
		display alert "Error writing to file" message errMsg buttons {"OK"} default button 1
	end try
	return fileLength
end writeToFile

Hello and thank you for your feedback.

And yes, after I saved my version of the script in the Scripteditor as an application, the error message that a property is missing also appeared when I ran it. And together with your statements in post #20, I have now suddenly realised what the error means.
So a victory point for the Script Debugger! Why it runs as a script in the Scripteditor without errors is strange, but ultimately it doesn’t matter.

During my research on the net, I had even read that NSContactsUsageDescription must be added to the Info.plist … which I have now also tried. But as you already wrote, it doesn’t work that way and only terminates the application.

And thanks of course for adding the colon to the slash (which I had overlooked until now)!
In fact, Contact app’s Applescript can not only save notes and images, but also export contact cards with slashes in their names without replacing them.
Funnily enough, both slashes and colons are replaced by “-” when you export them manually. So your replacement of colons and slashes makes sense and is the safer way!

Your script runs really fast. Wow!
I, on the other hand, will need a bit longer to fully understand it… *gg
It’s a pity that it doesn’t work with the Contacts framework. But no matter: the main thing is that it works.

Thank you very much for your effort and research!
I really appreciate it.

I had to smile about “GMT”. In fact, I’m only an hour away…
Keita

Seems you are the “Contacts” expert, so here’s my question. After finding this thread to export vCards, I’d like to know if there’s an AppleScript that automates the export of a Contacts archive (*.abbu file). I know I can just open contacts and do an export, but is there a way to do this via an AppleScript? Thanks for any help.

Hello,

The easiest way to do this would be with a shell script. With pure Applescript it would be more code.
Split into three parts here:

set abbuPath to do shell script "currDate=$(date \"+%Y.%m.%d\"); echo \"$HOME/Desktop/Contacts - $currDate.abbu\""
do shell script "mkdir -p " & quoted form of abbuPath
do shell script "cp -pR ~/Library/Application\\ Support/AddressBook/ " & quoted form of abbuPath

If your contacts are synchronised with iCloud: Please note that the timestamps of the backed up contacts are of course always older than those of the newer iCloud contacts!
If you try to replace the contacts with those from the abbu archive, they will immediately be re-replaced by the newer iCloud contacts.
So you would have to delete all iCloud contacts first…

Best regards
Keita

Hi @Keita.

I’ve modified the writeToFile() handler and the calls to it in the latest script so that the files have any existing content cleared before they’re written to. This is in case they’re overwritten with shorter content on a later occasion. It ensures that the end of the earlier longer content doesn’t remain in the file. It’s something I should have remembered to do yesterday. I may revamp the handler to look more elegant if I get a moment. :wink:

Thank you, that works incredibly well. I was able to change the date format to what I normally use and the destination directory to where I store my Contacts exports.

“If you try to replace the contacts with those from the abbu archive, they will immediately be re-replaced by the newer iCloud contacts.
So you would have to delete all iCloud contacts first…”

You are of course correct, but as much as Apple does right, it seems that some things are just counterintuitive. I would think that seeing how iCloud is keeping things in sync, the dates of all the Contacts backups would be identical on all devices, but of course they’re not. My 2020 M1 MacBook Pro backup is early January of this year, and my 2014 Mac mini backup is early January of last year. And, it seems that although any changes made on either platform (or on my iPhone for that matter) are instantly in sync on all systems, the idea of deleting all contacts up on iCloud is not an attractive choice.

@Nigel_Garvey : I would love to see your even more elegant handler. I don’t need it for this exact purpose here, as I always name backups with dates, but it’s great and I’ll definitely use it!

@Homer712 : Of course, deleting all contacts in iCloud is definitely not an attractive choice!