Export selected notes from Notes.app w/ attachments, import to EN

This version populates the attachment file list with aliases to where Notes keeps the attachments. It works for me, BUT it carries an enormous health warning. It parses Notes’s database file using sed, which isn’t the appropriate tool for the job. So while it works with my test notes, there may be cases where it doesn’t correctly identify the ends of the file URLs. In such cases, it should error when it tries to coerce the NSURLs derived from them to aliases. Hopefully someone who knows how to script database queries will be able to improve it.

Edit: Script reposted with redundant code from the previous version removed, the getAttachmentLookup() handler rewritten entirely in ASObjC, and the Evernote code re-enabled.

(*
	====================================================
	  [EN] Import Apple Notes into Evernote
	====================================================
	
	DATE:    2013-10-24
	AUTHOR: d.b.walker
	
	REVISED BY:  JMichaelTX on 2016-03-28 to make BUG fix. <https://discussion.evernote.com/topic/64814-apple-notes-app/#comment-395941>
	
	REF:
	  ¢ Importing from Apple Mail.app's Notes - Mac Help - Evernote User Forum       
	  ¢ https://discussion.evernote.com/topic/4046-importing-from-apple-mailapps-notes/?do=findComment&comment=236445
	
	Posted 24 Oct 2013
	Modified this script to work with Mavericks Notes, which is no longer in the mail app.
	Added the original creation and modification dates
	Added multiple tags - replace with your own
	Did not add the long note name fix (I needed to preserve my note names)
	====================================================
	
	FURTHER DEVELOPED BY: Nigel Garvey 2017-03-21/22/23, based on information in the Evernote fora, to allow a choice of Notes source folder(s) and to handle attachments.
	REVISION BY NG, 2017-03-29: "Note" folder names in the temporary desktop hierarchy for attachments now based on the notes' names. Any path delimiters in potential folder names now replaced with dashes.
	FURTHER REVSION 2017-04-03/4: The attachment files list is now populated with aliases to Notes's copies of the files.
	
	CAVEATS:
		1. I don't have Evernote and can't test that part of the code.
		2. Only intended for use with Notes's "On my Mac" account.
		3. Any attachments are simply "appended" to the Evernote notes in the order they happen to be returned by Notes.
		4. The effect in Evernote of Notes's references to the attachments in the note HTML is unknown.
		5. Although the script works for me, there's a possibility it may not correctly identify some file URLs. 
*)

use AppleScript version "2.5" -- Mac OS 10.11 (El Capitan) or later. (For NSURL coercions to alias.)
use framework "Foundation"
use scripting additions

main()

on main()
	-- User choice of one or more Notes folders (by name).
	tell application "Notes"
		activate
		set folderNames to name of folders
		set chosenFolderNames to (choose from list folderNames with multiple selections allowed)
		if (chosenFolderNames is false) then error number -128 -- Cancel button.
	end tell
	
	-- Get an NSDictionary which has the attachment CIDs as keys and the corresponding file URLs as values.
	set attachmentLookup to getAttachmentLookup()
	
	-- Repeat with each chosen folder name:
	repeat with i from 1 to (count chosenFolderNames)
		-- Get all the notes in the folder with this name.
		set thisFolderName to item i of chosenFolderNames
		tell application "Notes" to set theNotes to notes of folder thisFolderName
		
		-- Repeat with each note in the folder:
		repeat with j from 1 to (count theNotes)
			set thisNote to item j of theNotes
			
			tell application "Notes"
				-- Get the relevant note data.
				set myTitle to the name of thisNote
				set myText to the body of thisNote
				set myCreateDate to the creation date of thisNote
				set myModDate to the modification date of thisNote
				set myAttachments to the attachments of thisNote
			end tell
			
			-- Get alias specifiers for any attachments.
			set attachmentFiles to {}
			repeat with thisAttachment in myAttachments
				-- Get this attachment's content identifier (cid) from Notes.
				tell application "Notes" to set thisCID to content identifier of thisAttachment
				-- Use it to look up the corresponding NSURL in the lookup dictionary and store the result as a file specifier.
				set end of attachmentFiles to (attachmentLookup's valueForKey:(thisCID)) as alias
			end repeat
			
			tell application "Evernote"

				set myNote to create note with text myTitle title myTitle notebook "Imported From Notes" tags ["imported_from_notes"]
				set the HTML content of myNote to myText
				
				repeat with thisFile in attachmentFiles
					tell myNote to append attachment thisFile
				end
				
				set the creation date of myNote to myCreateDate
				set the modification date of myNote to myModDate
            
			end tell
			
		end repeat
		
	end repeat
	
end main

-- Create a lookup dictionary (NSDictionary) which has all the available attachment CIDs as keys and URLs to the corresponding files as values.
on getAttachmentLookup()
	set |⌘| to current application
	set fileManager to |⌘|'s class "NSFileManager"'s defaultManager()
	-- Get an NSURL to the user Library folder and thence to the folder containing Note's database file(s).
	set userLibraryFolderURL to (fileManager's URLForDirectory:(|⌘|'s NSLibraryDirectory) inDomain:(|⌘|'s NSUserDomainMask) appropriateForURL:(missing value) create:(false) |error|:(missing value))
	set NotesDBFolderURL to userLibraryFolderURL's URLByAppendingPathComponent:("Containers/com.apple.Notes/Data/Library/Notes")
	-- Or of course:
	(* set NotesDBFolderPath to |⌘|'s class "NSString"'s stringWithString:("~/Library/Containers/com.apple.Notes/Data/Library/Notes")
	set NotesDBFolderPath to NotesDBFolderPath's stringByExpandingTildeInPath()
	set NotesDBFolderURL to |⌘|'s class "NSURL"'s fileURLWithPath:(NotesDBFolderPath) *)
	-- On my system, the database file of interest has a name ending with "-wal".
	set databaseCandidates to fileManager's contentsOfDirectoryAtURL:(NotesDBFolderURL) includingPropertiesForKeys:({}) options:(|⌘|'s NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	set walFilter to |⌘|'s class "NSPredicate"'s predicateWithFormat:("path ENDSWITH '-wal'")
	set databaseURL to (databaseCandidates's filteredArrayUsingPredicate:(walFilter))'s firstObject()
	
	-- The file's contents are binary data, but this hack involves treating them as ISO Latin1 encoded text.
	set databaseText to |⌘|'s class "NSString"'s stringWithContentsOfURL:(databaseURL) encoding:(|⌘|'s NSISOLatin1StringEncoding) |error|:(missing value)
	-- Set a regex to scan for instances of attachment content identifiers (CIDs) in angle brackets ("<xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx@home>" on my machine, simply "<xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>" on Yvan's) and of URLs to files in Notes's attachments folder hierarchy ("file:///Users/username/Library/Containers/com.apple.Notes/Data/Library/CoreData/Attachments/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/filename").
	set searchRegex to |⌘|'s class "NSRegularExpression"'s regularExpressionWithPattern:("(?<=<)[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}(?:@home)?(?=>)|file:///Users/[^/]++/Library/Containers/com\\.apple\\.Notes/Data/Library/CoreData/Attachments/[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}/[\\u0021-\\u007f]++") options:(0) |error|:(missing value)
	-- For additional security, find out where the first CID occurs in the "text".
	set searchStart to (databaseText's rangeOfString:("<[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}(?:@home)?>") options:(|⌘|'s NSRegularExpressionSearch))'s location()
	-- Start the search proper from there and get the ranges of all the matches. (No harm if none.)
	set matchRanges to (searchRegex's matchesInString:(databaseText) options:(0) range:({searchStart, (databaseText's |length|()) - searchStart}))'s valueForKey:("range")
	
	-- The matches should be alternating instances of CIDs and file URLs, but the code below can easily be modified if this turns out not always to be the case.
	-- Extract the CIDs to one array and NSURL versions of the file URLs to another.
	set cidPrefix to |⌘|'s class "NSString"'s stringWithString:("cid:")
	set theCIDs to |⌘|'s class "NSMutableArray"'s new()
	set theURLs to |⌘|'s class "NSMutableArray"'s new()
	set i to 1
	set j to 2
	set matchCount to matchRanges's |count|()
	repeat until (j > matchCount)
		set thisCID to databaseText's substringWithRange:(item i of matchRanges)
		set thisURL to databaseText's substringWithRange:(item j of matchRanges)
		tell theCIDs to addObject:(cidPrefix's stringByAppendingString:(thisCID))
		tell theURLs to addObject:(|⌘|'s class "NSURL"'s URLWithString:(thisURL))
		set i to j + 1
		set j to i + 1
	end repeat
	
	-- Make and return an NSDictionary with the CIDs as the keys and the NSURLs as the values.
	-- The CID/NSURL pairs will no doubt be duplicated in the lists, but not in the dictionary.
	return |⌘|'s class "NSDictionary"'s dictionaryWithObjects:(theURLs) forKeys:(theCIDs)
end getAttachmentLookup

Just checking back here, seems no-one has had any joy in getting the Notes part to work or even reporting the Notes behaviour as a bug (as Yvan suggested).

I won’t play with your latest script @Nigel Garvey, as I am heeding your “enormous health warning”!

PS Since the postings I have upgraded to Sierra.

Jonathan

Have the opportunity to test with Evernote. Just tried the latest version of the script. The script stops for an error:

error “unable to set argument 4 - the AppleScript value <NSAppleEventDescriptor: [ 9.22337e+18, -9.22337e+18 ]> could not be coerced to type {_NSRange=QQ}.” number -10000

I am not very familiar (yet) with applescript, it doesn’t give me a clue.
Anyone?

Model: MacBook Air
AppleScript: 2.9
Browser: Safari 537.36
Operating System: Mac OS X (10.10)

What you got is not surprising.
The script posted by Nigel carefully states :
[format]use AppleScript version “2.5” – Mac OS 10.11 (El Capitan) or later. (For NSURL coercions to alias.)[/format]

Your signature tell us that you are running 10.10, so, if this signature is right it’s normal that the script fails.

I highlighted the words if this signature is right because if my memory is right, the script is supposed to refuse to compile on a 10.10 machine.

Yvan KOENIG running Sierra 10.12.5 in French (VALLAURIS, France) jeudi 1 juin 2017 21:04:34

Hi. Welcome to MacScripter.

Much of the script’s in AppleScriptObjC, which is an extension to AppleScript allowing it to use some of the operating system’s Objective-C methods directly.

The error looks like an AppleScriptObjC one which occurs when a script fails to find a substring in a text but tries to extract it anyway. The offending object’s usually [ 9.22337e+18, 0 ].

At a quick guess, I’d say the error was happening in these lines because your Notes database is empty or the script’s not reading it properly:

set searchStart to (databaseText's rangeOfString:("<[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}(?:@home)?>") options:(|⌘|'s NSRegularExpressionSearch))'s location()
-- Start the search proper from there and get the ranges of all the matches. (No harm if none.)
set matchRanges to (searchRegex's matchesInString:(databaseText) options:(0) range:({searchStart, (databaseText's |length|()) - searchStart}))'s valueForKey:("range")

If databaseText is an empty string, the regex search in the first line will return {location:9.22337203685478E+18, |length|:0}, which should be the range of the match, but that large location value is a code meaning “not found”. Since the script’s not expecting the substring not to be found, it goes ahead and calculates another search range from the result, using the error code as the new start location and databaseText’s zero length minus that for the range length. Ir’s probably the negative length that’s the immediate cause of the system’s complaint.

It’s late at night here and I’ve largely forgotten what the script’s supposed to do, but I’ll look at it in the morning.

Hi Alcedo.

Yes. I’m pretty sure the error’s happening in the lines I pinpointed last night. But it doesn’t necessarily mean your database file’s empty, only that the regex couldn’t be matched in the “text” from it. (The error code 9.22337203685478E+18 is such a large number that you have to subtract it from a pretty large amount before the truncated form of the result is anything other than -9.22337e+18.)

So, either:
Your database is empty, or
It doesn’t contain any note CIDs, or
The CIDs are in a different format from those used in El Capitan and Sierra (“xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx@home” or “” ” where each “x” is a hexadecimal digit character), or
You have more than one database file in ~/Library/Containers/com.apple.Notes/Data/Library/Notes whose name ends with “-wal” and the script’s reading the wrong one, or
ISOLatin1StringEncoding isn’t the best interpretation to impose when reading the file, or
Something else I haven’t thought of.

The search regexes could be made tighter by changing every instance of “[[:alnum:]]” in them to “[[:xdigit:]]”, but otherwise any fix would depend on what’s causing the CIDs not to be identified.

@Nigel, sorry, but I’m not following the meaning of “BUT it carries an enormous health warning.”.
Could you please explain?

Looks like a great script, and shows a lot of innovation. I haven’t had a chance to test it yet in my system (macOS 10.11.6), but I hope to soon.

Thanks for sharing.

Hi JMichaelTX.

Pretty much what it says in the rest of that paragraph and in the caveats in the script comments. The script uses regex to parse a database file, which isn’t the most foolproof idea in the world, and was written by someone who doesn’t use Notes and doesn’t have Evernote or an iCloud account. The Notes side of it worked in 10.11.6 on my machine with the on-board notes I created to research the problem, but otherwise I was hoping that someone with more knowledge of database and/or Evernote scripting would be able to expand my groundwork into something more solid.

Since the post before yours, MacScripter’s site software’s been updated and some characters in the posts above are now appearing as gibberish strings through being interpreted with different encodings from the ones under which they were posted. The ‘|⌘|’ label I use as a shortcut for ‘current application’ looks like ‘|⌘|’ (although this doesn’t actually sabotage the script) and there are various other things throughout the thread such as ‘”’ for ‘—’ and the misrendering of the diacriticals in Yvan’s results.

By which I mean my contribution, of course. I didn’t mean to imply any disregard for the original code by either d.b.walker or JMichaelTX.

Just checking to see if there’s been any activity forward on this. The two scripts do not seem to work with High Sierra. The first one produces the 0 byte file and the second failed with

error “Can’t make missing value into type alias.” number -1700 from missing value to alias

on the

set end of attachmentFiles to (attachmentLookup’s valueForKey:(thisCID)) as alias

line.

Thanks!