サーバにメッセージを保存したままGmailからIMAPでメッセージを取得する
経緯
Raspberry PIでサーバを立てたので、こいつにメールサーバを立ち上げてGmailのバックアップにしようとたくらんだ。 普段のメールはGmailからPCやスマホのアプリで見るので、Gmail上にメッセージを残し既読フラグも一切更新せずにメッセージをダウンロードする方法が欲しかった。 欲しいツールがなかったので、Rubyでちゃちゃっとツールを作った。
GmailからIMAPでメッセージをダウンロードするためには
まず最初にアプリパスワードの発行が必要。
fetchmailで試行錯誤したがダメだった
fetchmailで試行錯誤したのだが、次の問題が解決できなかった。
- メッセージをダウンロードするときに既読(Seen)フラグが付くのを止めることができないので、fetchmailでダウンロードすると他のメールクライアントから既読になってしまう。今回はバックアップ目的でメッセージを取得するので、勝手に既読が付くと困る。
- サーバにメッセージを残したままダウンロードすると、毎回、全メッセージをダウンロードしてしまう。
しょうがないので、自分でツールを作ることにした。
ツール(imapfetch)の作成
ツールの名前はimapfetchにした。Githubで公開済み。
とにかくすぐに動くツールが欲しかったので、gem化やユニットテストの手間暇も惜しみ、All in Oneのスクリプトでちょっとずつ書き足して動かしデバッグしながら作成、半日くらいでできた。
作戦は次のとおり。
- Rubyの各種ライブラリ(imap、logger、yaml、pstore)をうまく活用して、自分がやりたいことの実装に集中した。
- コマンドラインオプションは面倒なので実装しない。cronで定期実行したいので、設定は全部YAMLの設定ファイルに書くことにした。
- 前回ダウンロードしたメッセージを覚えておくためにUIDを保存することにした。IMAPではメッセージごとにUIDという固有のID番号がつく。UIDはメールボックス毎に一意かつ必ず増加するので、前回ダウンロードした最後のUIDから増えていた分をダウンロードする。ツールがUIDを覚えておくことで、サーバ上のメッセージの既読(Seen)等のフラグを更新することなく差分だけをダウンロードできる。
imapfetch実装の簡単な解説
注意: わかりやすくするため、コードは余分な処理を削って引用している。
IMAPサーバへ接続する。
imap = Net::IMAP.new(c['server'], port: c['port'], ssl: c['ssl'])
ログインする。Gmailの場合、パスワードはアプリパスワードを使う。
imap.login(c['username'], c['password'])
メールボックスを選択。 読み出し専用でアクセスするためSELECT命令ではなくEXAMINE命令を使う。IMAPのメールボックス名はUTF-7で取り扱うため変換する。
imap.examine(Net::IMAP.encode_utf7(c['mailbox']))
前回のUIDの取り出し。 YAML書式のPStoreにサーバ/ユーザ/メールボックス/UIDと階層化して保存しておく。
store = YAML::Store.new(File.join(ENV['HOME'], '.imapfetchids')) last_uid = store.transaction{ store[host] = {} unless (store.root? host) store[host][c['username']] = {} unless (store[host].key? c['username']) store[host][c['username']][c['mailbox']] = {} unless (store[host][c['username']].key? c['mailbox']) store[host][c['username']][c['mailbox']]['last_uid'] || 0 }
サーバの最後尾のUIDの確認。
IMAPでは*
で最後尾のメッセージを取得できる。
max_uid_msg = imap.fetch('*', 'UID').last
UIDの一覧を取得。 ついでにメッセージのサイズも取得。
msg_list = imap.uid_fetch(min_uid..max_uid, %w[ UID RFC822.SIZE ])
取得したリストでループをまわす。 メッセージをダウンロードする毎に最新のUIDを保存するので、pstoreのトランザクションを発行する。
for target in msg_list target_uid = target.attr['UID'] target_octets = target.attr['RFC822.SIZE'] store.transaction{ ...省略... } end
メッセージをダウンロードする。 UID指定なので必ず1メッセージのダウンロードになるが、配列で結果が返るので一応ループをまわしておく。
for message in imap.uid_fetch(target_uid, 'RFC822') ...省略... end
IO.popen(c['mda'], 'w') {|output| output << message.attr['RFC822'] }
MDAの実行結果を確認。
成功したらpstoreにUIDを保存する。
失敗したらabort
で即終了、続行すると次回取得時に失敗したUIDを取りこぼすので、続行できない。
if ($?.exitstatus == 0) then store[host][c['username']][c['mailbox']]['last_uid'] = target_uid else fetch_log(Logger::FATAL, "MDA failed: #{c['mda']} (exit #{$?.exitstatus})") abort('MDA failed') end
以上で終了。
*1:Mail Delivery Agent