-
-
Save milch/f775ab001069da477ca81fd6704b6d4c to your computer and use it in GitHub Desktop.
require 'base64' | |
require 'digest' | |
require 'mail' | |
require 'nokogiri' | |
require 'shellwords' | |
file = ARGV.first | |
puts "Processing file #{file}" | |
contents = File.read(file) | |
mail = Mail.read(file) | |
subject = mail.subject | |
date = mail.date.to_time.utc | |
from = mail.from.first | |
folder = (subject.match(/#([\w ]*)/).captures.first.chomp rescue "Notes").capitalize | |
searchID = date.strftime("%s") | |
subject = subject.sub(/\s*#[\w ]*$/, "") | |
puts "Creating new Note with title \"#{subject}\" in folder \"#{folder}\"" | |
puts "Received on #{date} from #{from}" | |
match = contents.scan(/filename\s*=\s*([^\n]*)\n[\s\S]*Content-Id:\s*<([^>]+)>/mi) | |
attachments = Dir.chdir(File.dirname(File.dirname(file))) do | |
attachment_globs = match.map { |f, _id| "**/#{f}" } | |
hashes = Dir[*attachment_globs].map { |found| | |
{ match.detect { |f, _| found.end_with? f }[1].downcase => File.join(Dir.pwd, found)} | |
} | |
hashes.reduce({}, &:merge) | |
end if match | |
attachments ||= {} | |
html = Nokogiri::HTML((mail.html_part || mail.text_part || mail.body).decoded) { |c| c.strict } | |
abort "Couldn't find a valid html body" unless html | |
body = html.css('body').first | |
abort "The body doesn't actually contain any body tags, aborting..." unless body.to_s.include? "<body" | |
linked_attachment_count = 0 | |
attachment_nodes = body.css('img') | |
attachment_nodes.each do |a| | |
next unless a['src'].start_with? "cid" | |
content_id = a['src'].downcase.sub(/^cid:/, "") | |
path = attachments[content_id] | |
mime_type = `file --brief --mime-type #{path.shellescape}`.chomp | |
a.add_next_sibling "<en-media hash=\"#{Digest::MD5.hexdigest(File.read(path))}\" type=\"#{mime_type}\" />" | |
a.remove | |
linked_attachment_count += 1 | |
end | |
abort "There were more attachments in the source than in the actual HTML (#{attachments.length} > #{linked_attachment_count})" if attachments.length > linked_attachment_count | |
note_xml = Nokogiri::XML::Builder.new do |xml| | |
xml.send(:"en-note") { | |
xml.div { | |
xml.p | |
} | |
xml.font(searchID, color: "#FFFFFF", size: "1", style: "font-size:1px;") | |
} | |
end.doc | |
note_xml.css('p').first.add_child body.inner_html | |
inner_xml = <<~XML | |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"> | |
#{note_xml.root.to_xml} | |
XML | |
abort "Couldn't create the XML from the provided string" unless inner_xml | |
outer_xml = Nokogiri::XML(<<~XML, nil, "UTF-8") | |
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export3.dtd"> | |
<en-export export-date="#{date.strftime("%Y%m%dT%H%M%SZ")}" application="RubyExporter" version="1"> | |
</en-export> | |
XML | |
Nokogiri::XML::Builder.with(outer_xml.at('en-export')) do |xml| | |
xml.note { | |
xml.title subject | |
xml.content { xml.cdata inner_xml } | |
xml.created date.strftime("%Y%m%dT%H%M%SZ") | |
xml.updated date.strftime("%Y%m%dT%H%M%SZ") | |
xml.send(:"note-attributes") { | |
xml.author from | |
xml.source "Mail" | |
xml.send(:"reminder-order", 0) | |
} | |
attachments.each do |_id, path| | |
xml.resource { | |
xml.data(Base64.encode64(File.read(path)), encoding: "base64") | |
xml.mime `file --brief --mime-type #{path.shellescape}`.chomp | |
xml.send(:"resource-attributes") { | |
xml.send(:"file-name", File.basename(path)) | |
xml.timestamp "19700101T000000Z" | |
} | |
} | |
end | |
} | |
end | |
result = outer_xml.to_xml | |
abort "Couldn't export the generated XML: #{outer_xml}" unless result | |
result_file = File.expand_path File.join("~", "export.enex") | |
File.write(result_file, result) | |
import_script = <<EOF | |
set pathToImportItem to "#{result_file}" | |
set sortIntoFolder to "#{folder}" | |
set searchID to "#{searchID}" | |
set mailFilePath to "Macintosh HD#{file.tr("/", ":")}" | |
tell application "System Events" | |
tell application "Notes" to activate | |
click menu item "Import to Notes…" of (process "Notes")'s (menu bar 1)'s (menu "File") | |
delay 1.5 | |
keystroke "g" using {shift down, command down} | |
delay 1.5 | |
keystroke pathToImportItem | |
delay 1 | |
keystroke return | |
delay 1 | |
keystroke return | |
delay 1 | |
keystroke return | |
delay 1 | |
end tell | |
tell application "Notes" | |
set newNote to the first note whose body contains searchID | |
set myFolder to first folder whose name = sortIntoFolder | |
if not (exists folder sortIntoFolder) then | |
make new folder with properties {name:sortIntoFolder} | |
end if | |
move newNote to myFolder | |
delay 0.5 | |
delete (every folder whose name starts with "Imported Notes") | |
end tell | |
tell application "Finder" | |
set comment of (mailFilePath as alias) to "converted to note" | |
end tell | |
EOF | |
import_script.gsub!(/^\s+/, "") # Remove whitespace | |
script_lines = import_script.lines.delete_if { |l| l.empty? } | |
puts `osascript #{script_lines.map { |l| "-e #{l.shellescape}" }.join " "} 2>&1` |
This works by taking the mail, converting it to HTML and then saving it as an Evernote .enex
file. Notes.app can import such files natively, so all it has to do with that is use an AppleScript to actually run the import.
Unfortunately, importing is not supported in the regular Notes.app AppleScript suite, so this script actually has to click through the relevant menu items. Make sure Hazel or whatever you're using to run this actually has permissions to control your Mac (in the Privacy > Accessibility system preferences).
The subject of the mail is used as the title of the note, and it even supports moving the note to a specified folder, e.g. the subject "My new note #Misc" will be created in the "Misc" folder with the name "My new note".
Update: I improved the reliability by doing the following:
- After the note was successfully exported it adds a comment to the .eml file in Finder. This is useful if the rule was executed while the mac was sleeping
- The note body receives a timestamp at the end (with a white font colour), and the note will be found through that. This is much better than trying to get the note by title, since Notes sometimes changes the title from what was provided, and has trouble getting the correct note when there are multiple notes with the same title.
Here's a screenshot of the Hazel rule:
Note: needs the
nokogiri
andmail
gem to work.