imapでメールを受信したい!

とあるシステムを作るためにRailsでgmailを素のimapで受信することになりました
gmail受信するならgmail api使えばいいじゃないかという意見はごもっともですが、今回それをしない理由が二つあります
1. google api経由だと制限がかかったときに大変
2. gmail以外のメールを使うとなったときにgmail取得のためのロジックになっているため移行できない

メール受信に関する記事がなかなか見当たらない

imapでメールを受信するにあたって多くの情報を調べましたが、なかなかこれだというものが見つかりませんでした
世の中にはメール送信の記事はたくさんありますが、受信の記事は少ないみたいです(今回の記事はそういった経緯からも執筆しているのです)
余談ですが、同様のことを嘆いている方がいらっしゃいました
本記事の流れとしては私が想定していたデータを取得するまでに辿った道順に書きますので、受信のためのコードが知りたい人は最後まで読み飛ばしてください

環境

  • Ruby 2.5.0
  • Rails 5.1.6
  • CentOS 7.4

imapでメールを受信できるはずが

Rubyを使ってメールを受信するためにte2u/imap.rbを参考にしました
今回はgmailを利用したので、

imap_host = 'imap.gmail.com'
search_criterias = ['ALL'] # 検索条件:今回は全てのメールを受信したかったのでALLに

私はまずCentOS上ではなく、macの環境でお試し程度に動かしてみました

imap.search(search_criterias).each do |msg_id|
    msg = imap.fetch(msg_id, [subject_attr_name, body_attr_name]).first
    subject = msg.attr[subject_attr_name].toutf8.strip
    body = msg.attr[body_attr_name].toutf8.strip
    puts subject # 件名を表示
    puts body # 本文を表示
 end

すると見事に件名と本文が表示されていました
あとはCentOS上のRailsで実装すればやりたいことの多くは達成されるはず!
でした…

Base64とはなんぞや?

実際は、日本文ではなく、よくわからない文字コードに変換されてしまいました
色々調べた結果Base64という文字コードで取得されていました
例えば、「私は福岡の大学院生です」は以下のような文字列になるみたいです
56eB44Gv56aP5bKh44Gu5aSn5a2m6Zmi55Sf44Gn44GZ

それならばデコードすれば良いではないか!ということでRubyでBase64をデコードできないか調べるとQiitaでRubyでBase64URLエンコーディングという記事が出てきたので試してみました
しかし、なぜか変化なし
色々と試しても変わらないので私の上司に助けを求めました
すると、BODY(本文)の一部をBase64でデコードすると元の文章となって取得できることが判明したのでデコードできるはず、とのこと
また、headerがあるせいでデコードできないのでは?との指摘をいただきました
見直してみると

Content-Type: text/html; charset="UTF-8" # この部分が
Content-Transfer-Encoding: base64        # 邪魔をしているみたい

56eB44Gv56aP5bKh44Gu5aSn5a2m6Zmi55Sf44Gn44G

header、ありますね
すいません、僕の知識不足で全く気付きませんでした

header削除すれば良さそう

しかし、調べたところこのheaderを判別するのはなかなか厄介みたいです(参照した記事
しかも、添付ファイルがある場合は今回参照した記事では対応できないパターンがありました

ここにきていきなりの解決

あれこれ試していると上司からmikel/mailなるライブラリがあるとのこと
imap通信の処理ではないけど、fetchしたメールのパース処理に使えるのでは?と

imapで受信してmikel/mailでパースすれば解決できる!

今回使用したコードをいかに載せておきます。
記事作成時にはRuby、Rails歴ともに1ヶ月も経っていないのでどんどんご指摘いただけると嬉しいです。

require 'net/imap'
require 'kconv'
require 'mail'

# imapに接続
imap_host = 'imap.gmail.com' # imapをgmailのhostに設定する
imap_usessl = true # imapのsslを有効にする
imap_port = 993 # ssl有効なら993、そうでなければ143
imap = Net::IMAP.new(imap_host, imap_port, imap_usessl)
# imapにログイン
imap_user = 'your email address'
imap_passwd = 'email password'
imap.login(imap_user, imap_passwd)

imap.select('INBOX') # 対象のメールボックスを選択
ids = imap.search(['ALL']) # 全てのメールを取得

imap.fetch(ids, "RFC822").each do |mail|
  m = Mail.new(mail.attr["RFC822"])
  # multipartなメールかチェック
  if m.multipart?
    # plantextなメールかチェック
    if m.text_part
      body = m.text_part.decoded
    # htmlなメールかチェック
    elsif m.html_part
      body = m.html_part.decoded
    end
  else
    body = m.body
  end
end

m.multipartは複数のデータを持ったメールを見分けます
例えば、添付資料がある場合やhtmlメールとテキストメール両方を持った場合などです
これによってメールの本文を取得することができました!
ちなみに、本文以外も色々取得できるので試してみてください!(もしかしたら今後追記するかも)

余談

imap.fetchの第二引数を配列にして複合条件にするとなぜか取得できない(時間がかかる?)状況に出会いました
→引数を単体にして別々に取得すれば対応できた

msg = imap.fetch(msg_id, [subject_attr_name, from_attr_name, to_attr_name]).first

ではなく、

msg = imap.fetch(msg_id, 'BODY[TEXT]')

とすることで解決した、が、mikel/mailライブラリ使ったので結局この処理は使っていないです

imap.fetchする数が大きいとロックがかかる
→fetchする数を分けて対応した

ids = imap.search(['ALL'])
ids.each_slice(100).to_a.each do |id_block| # 100件ごとにメールをfetchする
    imap.fetch(id_block, "RFC822").each do |mail|
        # なんか色々処理する
    end
end

mikel/mailライブラリをgemでインストールすると、たまたま今回作っていたMailモデルと競合してしまいました
→結局、開発初期ということもあり自作のMailモデルの名称を変更して対応しました

おわりに

結局メールを受信する処理を作るために丸2日かけてしまいました
まだまだ未熟者ですね
今回初版(2018年4月2日時点)では、忘れて書ききれていない事もあるかもしれないので追記する可能性高いです。
なので、ご指摘いただけたら対応します

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.