Last active
April 8, 2024 01:38
-
-
Save milch/f775ab001069da477ca81fd6704b6d4c to your computer and use it in GitHub Desktop.
Ruby script to convert a mail (.eml/.emlx) file to a note in macOS Notes.app.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Update: I improved the reliability by doing the following:
Here's a screenshot of the Hazel rule: