2016-06-09T21:48:48+09:00
メール.app で選択されているメッセージの添付ファイルを IMAP サーバーからダウンロードする(添付ファイルの文字化け回避)
OS X 付属のメール.app には昔から添付ファイルが文字化けするという持病がある。OS X 10.8 までは、MIMEfix という文字化けを修正してくれる秀逸なプラグインがあり、重宝していた。大変残念なことに開発が停止してしまい、OS X 10.9 以降に対応した MIMEfix プラグインが存在しない。
仕様がないから、iCloud の Web メールから添付ファイルをダウンロードしていた。大変めんどくさい。Web メールだと文字化けしないのだから、技術的に難しいことがあるわけでもない。単純に Apple の怠慢でバグが放置されていると思われる(しかし、そうとも言い切れない事情は後ほど)。
添付ファイルのファイル名は、MIME の Content-Disposition: フィールドの filename パラメータに RFC2231 もしくは MIME B でエンコーディングされて記述される。添付ファイルのファイル名が長いと、filename パラメータは複数行に分割されることがある。自分の調べた限りでは、filename フィールドが MIME B でエンコーディングされて、なおかつ複数行に分割されている場合は、ほとんど文字化けが起きる。しかし、そうでない場合もわずかにあるので悩ましい。自分が受け取る添付ファイルのファイル名は、ほとんどは MIME B でエンコードされているので文字化けしまくりである。
ちなみに、メール.app は添付ファイルのファイル名を RFC2331 でエンコードしている。そして、どんなに長いファイル名でも複数行に分割したりしない。Thunderbird もメール.appと同じように、RFC2331 でエンコードするとのこと。そして、メール.app と違ってファイル名が長いと複数行に分割するとのこと。その昔は、Thunderbird から複数行に分割されてエンコードされた添付ファイル名も文字化けしていたが、自分の試した限りでは OS X 10.11 のメール.app ではバグフィックスされている模様。
先ほど、文字化けするのは Apple の怠慢などと偉そうなことを言ったが、そもそも添付ファイル名は MEME B ではなく、RFC2331でエンコードしなければならない決まりになっている。MIME B でファイル名をエンコードして送りつけてくる 行儀の悪いWindows 系のメーラーが諸悪の根源である、というのが本当のところで、単純に Apple をせめてはかわいそうとも思う。
そうも言っていられないので、手軽に文字化けしていない添付ファイルを入手すべく、メール.app で選択されいるメッセージの添付ファイルを IMAP サーバーからダウンロードするスクリプトを書いた。もっぱら、ruby で書き始めたが、半分以上がヒアドキュメントで埋め込まれた AppleScript という不恰好なものになってしまった。
なお、実行には、gem で Mail ライブラリをインストールする必要があります。
#!/usr/bin/env ruby # coding: utf-8 # Download attachments of selected message in Apple Mail.app # form IMAP server. require 'net/imap' require 'pp' require 'mail' require 'yaml' require 'pathname' require 'optparse' def main opts = ARGV.getopts('', 'raw_attachments', 'source') minfo = mail_info if minfo.nil? then exit 0 end imap = Net::IMAP.new(minfo['server'], minfo['port'], minfo['use_ssl']) begin imap.login(minfo['user'], minfo['password']) rescue => e display_alert("Failed to login with error: #{e.message}") exit 0 end imap.select(minfo['mailbox']) msg_ids = imap.search(["HEADER", "Message-Id", minfo['message_id']]) a_msg = imap.fetch(msg_ids[0], "RFC822") if (opts['source']) then print a_msg[0].attr['RFC822'] end m = Mail.new(a_msg[0].attr["RFC822"]) if m.multipart? then saved_files = [] m.attachments.each do |attachment| # 添付ファイルの種類とファイル名 if opts['raw_attachments'] then print attachment next end # 添付ファイルの保存処理 Dir.chdir(minfo['location']) filename = attachment.filename begin File.open(filename, "w+b") {|f| f.write attachment.body.decoded saved_files.push(Pathname.new(minfo['location'])+filename) } rescue => e #puts "添付ファイルの保存に失敗 #{e.message}" display_alert("Failed to save attachments with error: #{e.message}") end end if saved_files.length > 0 then final_message(saved_files) end end imap.disconnect end def mail_info mail_result = `osascript << EOS tell application id "com.apple.Mail" set msgs to selection if (count msgs) < 1 then display alert "No selected messages." return "" end if set msgs_with_attachments to {} repeat with a_msg in msgs try set has_attachments to exists mail attachments of a_msg on error set has_attachments to true end try if has_attachments then set end of msgs_with_attachments to a_msg end if end repeat end tell set nmsg to count msgs_with_attachments if nmsg < 1 then display alert "No attachements in the selected message." return "" end if if nmsg > 1 then set subject_list to {} tell application id "com.apple.Mail" set msg_idx to 1 repeat with a_msg in msgs_with_attachments set dt to date received of a_msg set end of subject_list to (msg_idx as text) & space ¬ & (subject of a_msg) & tab ¬ & (short date string of dt) & space ¬ & (time string of dt) set msg_idx to msg_idx + 1 end repeat end tell set a_result to choose from list subject_list with prompt "Choose a message to save attachments" without multiple selections allowed if class of a_result is not list then return "" end if set msg_idx to (word 1 of item 1 of a_result) as number set target_msg to item msg_idx of msgs_with_attachments else set target_msg to first item of msgs_with_attachments end if tell application id "com.apple.Mail" tell target_msg try set has_attachments to exists mail attachments on error set has_attachments to true end try if not has_attachments then tell current application display alert "No attachements in the selected message." end tell return "" end if set msgid to message id set mbox to its mailbox set mbox_name to name of (its mailbox) set server_name to server name of account of (its mailbox) set use_ssl to uses ssl of account of (its mailbox) tell account of (its mailbox) set account_name to name set user_name to user name set port_number to port end tell end tell repeat set a_container to (get container of mbox) if (class of a_container) is not container then exit repeat end if set mbox to a_container set mbox_name to (name of mbox) & "/" & mbox_name end repeat end tell try set a_location to choose folder with prompt "Choose a location to save attachments" on error return "" end try try set a_result to display dialog "Enter password for " & account_name default answer "" with hidden answer on error return "" end try set a_password to text returned of a_result set lf to ascii character 10 return "---" & lf & ¬ "server: " & server_name & lf & ¬ "port: " & port_number & lf & ¬ "use_ssl: " & (use_ssl as text) & lf & ¬ "user: " & user_name & lf & ¬ "password: " & a_password & lf & ¬ "mailbox: " & mbox_name & lf & ¬ "message_id: " & msgid & lf & ¬ "location: " & (POSIX path of a_location) & lf & ¬ "---" EOS` if mail_result.length == 1 then return nil end #pp mail_result return YAML.load(mail_result) end def display_alert(msg) `osascript << EOS display alert "#{msg}" EOS` end def final_message(saved_files) saved_files_joined = saved_files.join("\n") `osascript << EOS set file_paths to "#{saved_files_joined}" set a_result to display alert "Attachments are saved." message file_paths ¬ buttons {"Cancel", "Reveal", "OK"} if button returned of a_result is "Reveal" then tell application id "com.apple.finder" reveal (paragraph 1 of file_paths as POSIX file) activate end tell end if EOS` end main
もう、Thunderbird に乗り換えようかな?