Gmailでプッシュ配信されるメールを取得

Gmailをはじめとする、IMAPのメールサーバ*1から、プッシュ配信されるメールを取得する方法を調べたのでメモ。新規メールを監視してリアルタイムに何らかの処理を実行するといったことが可能になります。

使うライブラリは

require 'net/imap'


まず接続と認証、それから監視したいメールボックスGmailなら"INBOX"や特定のラベル*2など)をselectしたのち、IDLEコマンドを発行してイベントが起こるのを待機します。新たなメールが追加されるとEXISTSイベントで新しいメールのIDが通知されるので、imap.idleの中でそれを検知し、idleを抜けます(そうしないとメール取得コマンドなどがデッドロックする)。また、しばらくイベントがないとサーバから切断されることがあるので、その場合は再接続します。

last_id = -1
while true
  begin
    unless $imap
      $imap = Net::$imap.new('$imap.gmail.com', 993, true)
      $imap.login(mail_user, mail_password)
      $imap.select(mail_label)
      puts "connected to $imap server"
    end

    $imap.idle do |resp|
      if resp.name == "EXISTS"
       last_id = resp.data
       $imap.idle_done
      else
        p resp
      end
    end
  rescue Net::$imap::Error => e
    if e.inspect.include? "connection closed"
      puts "connection closed: reconecting..."
      $imap = nil
    else
      raise
    end
  end
  
  next unless $imap

  fetch_mail last_id..-1 
end

この実装だとIDLEコマンド実行中以外に新規メールが届いたらイベントを取り逃すような気がするので、イベントハンドラを別途登録してその中で処理すべきかも。


メールの取得はfetch_mailという関数で。HTMLメールの場合、マルチパートになっているので、下記ではプレインテキストの部分をとるようにしています。part.subtype == "HTML"とすれば、HTML部分がとれます。BODYというattributeをfetchすると、そこにマルチパートの情報が入っているので、それを解析して"BODY[n]"を改めてfetchしています。QUOTED-PRINTABLEでエンコードされている場合はそのデコードも行っています。

def fetch_mail(range)
  mails = $imap.fetch(range, ["UID","BODY","FLAGS"])
  mails.each do |mail|
    uid = mail.attr["UID"]
    seen = mail.attr["FLAGS"].include? :Seen
    body = mail.attr["BODY[1]"]
    if mail.attr["BODY"].multipart?
      n = mail.attr["BODY"].parts.map.with_index{|part,idx|
        idx if part.media_type == "TEXT" and part.subtype == "PLAIN"
      }.select{|x| x}[0]
      if not n
        raise "NO PLAINTEXT PART"
      end
      body = $imap.uid_fetch(uid, "BODY[#{n+1}]")[0].attr["BODY[#{n+1}]"]
      if mail.attr["BODY"].parts[n].encoding == "QUOTED-PRINTABLE"
        body = body.unpack('M')[0]
      end
    end
  end

  process_body body
end


あとは、process_bodyに本文が渡されるので、適当な処理をすればOK。seenは既読かどうかのフラグです。ちなみに、BODY[n]をとると既読になります。そういえばタイトルを取り忘れましたが、fetchするattributeに"ENVELOPE"を追加し、mail.attr["ENVELOPE"].subjectをとってくれば良いでしょう。


ちなみにこれを書いた目的は、Ingressのポータル破壊通知メールが飛んできたときに、それをGmailの振り分けでラベルをつけた後アーカイブすることで新着メール通知が飛んでこないようにし、別途このスクリプトでポータルの位置と画像付きでリアルタイムにTwitterに通知する、というものです。まとまってたくさん来たりするから他のメール通知に混ざってほしくない、という。。

HTMLメールなので、nokogiriを使ってHTMLをパースして、画像ファイルやIntel MapのURLなどを抽出しています。

*1:ただしプッシュ配信に必要なIDLEに対応したIMAPサーバ

*2:自動振り分け機能でラベル付けすることを想定しています