Gmail APIとPub/Subでリアルタイムメール受信 on ruby

Gmail API, Google Cloud Pub/Sub(以下Pub/Sub), Herokuを使った、リアルタイムメール受信がしたい…したくない？
普通、どうしてもメールの受信にはラグが発生します。pull型なので。pull間隔を短くしたりして、リアルタイムに近づけたりしますよね。
今回は、pullではなくpushを使って、gmailに届いたメールをリアルタイムで感知する方法を行います。わぁ、イマドキ。
**注:完璧な動作というわけではなく、落とし穴があります。ないしは私の実装で変な部分があります。あとがきに記載しておきました。**

# 目的
特定のメールが届いたらリアルタイムにLINEに通知が来るようにしたいから。
(ifttt信者の私が、不満に思っている点である、LINEやdiscord,slackとの連携が出来ないのを改善するため。)

# 流れ
1, GoogleCloudPlatform(GCP)にプロジェクト作成、設定する
2, Pub/Subにプロジェクトのトピックを作成する
3, トピックにWebhookでのSubscriptionを作成する
4, Gmail APIからトピックにPublishする

# 1,まずはGCPにプロジェクトを設定

[GCPのコンソール](https://console.cloud.google.com/)から、プロジェクトを作成
### 設定
- 作成後のページ、IAMと管理者の`GCP のプライバシーとセキュリティ`の確認と同意をする

- GCPの お支払い→概要→`この請求先アカウントにリンクされているプロジェクト` に作ったプロジェクトがリンクされてるかの確認。

- GCPの APIとサービス→ダッシュボード にデフォルトで使用可能になっているAPIを全部無効に(する必要もないけど)し、`APIとサービスの有効化`からGmail APIとGoogle Cloud Pub/Sub APIを有効にする。

- 認証情報は、Gmail APIの設定時に設定しますので、今は触らず。

# 2,お次はPub/Subにプロジェクトのトピックを作成する
Pub/Subの仕組みは[What is Google Cloud Pub/Sub?](https://cloud.google.com/pubsub/overview)を参照。

- GCPの[Pub/Subのコンソール](https://console.cloud.google.com/cloudpubsub)を開き、上部に目的のプロジェクトが表示されてることを確認する
- 適当な名前でトピックを作成
- トピックを選択し、右上の`情報パネルを表示`を選択後、`gmail-api-push@system.gserviceaccount.com`というメンバーを`Pub/Subパブリッシャー`の役割に設定し、追加

# 3,あと一歩！ トピックにWebhookでのSubscriptionを作成する
- まずは単純なSubscribeが出来るか確認したいので、Herokuに適当にサーバーを立てる。
(もちろんHerokuでなくても構わないが、HTTPS通信が必須)

```ruby:app_main.rb
require 'sinatra'
require "json"
require "base64"

post "/callback" do
  message = JSON.parse request.body.read
  raw_data = Base64.decode64 message["message"]["data"]
  data = raw_data.force_encoding("UTF-8")
  logger.info "Pushed Message: #{data}"
  response.status = 204
end
```

```ruby:Gemfile
source "https://rubygems.org"
gem 'sinatra'
```

```ruby:Config.ru
require './app_main'
run Sinatra::Application
```

```ruby:Procfile
web: bundle exec rackup config.ru -p $PORT
```

- 最低限、これら4つをHerokuにデプロイすればとりあえず動きます。デプロイの仕方は[Qiitaのこちらの記事](http://qiita.com/Arashi/items/b2f2e01259238235e187)を参考にしてみて下さい。

- 先ほど作成したPub/Subのトピックに対し、トピックの詳細画面から`サブスクリプションを作成`する。配信タイプは`エンドポイント URLにpush`で、URLはHerokuのURL(https://○○.herokuapp.com/)の末尾にWebhook受信用に設定した(/callback)をくっつけて、作成。

- 先ほど作成したPub/Subのトピックに対し、トピックの詳細画面から手動で`メッセージを公開`をする。

``` 
$ Heroku logs -t
``` 
等でログを見て、受信できていれば成功。

# 4,最後にGmail APIからPublishできるように！
困ったらいつでも頼る[公式のガイドはこちら](https://developers.google.com/gmail/api/guides/)
メール送信、メール受信、設定の変更まで、gmailの殆どの操作を行えます。watchメソッドでは、リファレンス上は、リアルタイムにgmailの変更(メール受信、メール削除、その他なんでも)をお知らせしてくれます。ひゅー、夢あるぅ

- 公式の[クイックスタート](https://developers.google.com/gmail/api/quickstart/ruby)に沿って進めましょう。英語だけど頑張って。進めていくと、前述したGCPの認証情報を作成もします。

- Step4まで完了し、上手く動いたなら、今回のプロジェクト用に途中にあるコードは、こちらに変更しましょう

```ruby:quickstart.rb
require 'sinatra' ## ここを追加
class Quickstart < Sinatra::Base ## ここを追加
require 'google/apis/gmail_v1'
require 'googleauth'
require 'googleauth/stores/file_token_store'

require 'fileutils'

OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'
APPLICATION_NAME = 'Gmail API Ruby Quickstart'
CLIENT_SECRETS_PATH = 'client_secret.json'
CREDENTIALS_PATH = File.join(Dir.home, '.credentials',
                             "gmail-ruby-quickstart.yaml")
SCOPE = Google::Apis::GmailV1::AUTH_GMAIL_READONLY

##
# Ensure valid credentials, either by restoring from the saved credentials
# files or intitiating an OAuth2 authorization. If authorization is required,
# the user's default browser will be launched to approve the request.
#
# @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials
class << self  ## ここを追加
def authorize
  FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))

  client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
  token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
  authorizer = Google::Auth::UserAuthorizer.new(
    client_id, SCOPE, token_store)
  user_id = 'default'
  credentials = authorizer.get_credentials(user_id)
  if credentials.nil?
    url = authorizer.get_authorization_url(
      base_url: OOB_URI)
    puts "Open the following URL in the browser and enter the " +
         "resulting code after authorization"
    puts url
    code = gets
    credentials = authorizer.get_and_store_credentials_from_code(
      user_id: user_id, code: code, base_url: OOB_URI)
  end
  credentials
end
## 上記まで変更は3行追加。ここからはサンプルのここ以下を削除し、下記を追加
def start_watch
  # Initialize the API
  service = Google::Apis::GmailV1::GmailService.new
  service.client_options.application_name = APPLICATION_NAME
  service.authorization = authorize
  # Start watch
  req = Google::Apis::GmailV1::WatchRequest.new
  req.topic_name="projects/□□/topics/〇〇"
  service.watch_user('me', req)
end
end
end ## インデントは自分でお願いします。
```

- watchメソッドは[公式リファレンス(
Users: watch)](https://developers.google.com/gmail/api/v1/reference/users/watch)が「分かり辛い」です。(Tryが用意されてるくせに、このTryは絶対に失敗します。当たり前だけど。) 

- こちらにも[Push Notificationの説明](https://developers.google.com/gmail/api/guides/push)と言いつつ、実はpushに対応しているのはGmailAPIの中でもwatchメソッドしか存在しないので、watchメソッドの説明が書かれています。

- ユーザーがテスト出来ないので、テストした私が噛み砕くと、watchメソッドは、「**gmail上のどんな些細な変更も順番に記録されているhistoryIDの中から、有る特定のイベント**(正確な条件は私は調べても分からなかった)**が起きた際に、指定されたPub/Sub上のトピックに、historyIDとその他些細なデータを含めたメッセージをPublishすることを試みる**ことを一定期間において継続的に実行する」ことを起動するメソッドである。あまりにもわかりづらいね。

なのでwatchメソッドは定期的(最低7日間に1回)に叩いてあげなければならないみたいです。クソ。考えるのが嫌なら以下推奨。

```ruby:app_main.rb
require 'sinatra'
require "json"
require "base64"
require "./quickstart.rb" ## これを追加。

post "/callback" do
  message = JSON.parse request.body.read
  raw_data = Base64.decode64 message["message"]["data"]
  data = raw_data.force_encoding("UTF-8")
  logger.info "Pushed Message: #{data}"
  Quickstart.start_watch ## これを追加。
  response.status = 204
end
```
- QuickStartも同じディレクトリに入れてデプロイしちゃいたいけど、その際は、

``CLIENT_SECRETS_PATH = 'client_secret.json'``

``CREDENTIALS_PATH = File.join(Dir.home, '.credentials',"gmail-ruby-quickstart.yaml")``
の2つのファイルも上手く一緒にデプロイし、PATHも変えてあげてね。
***
以上でリアルタイムで、よくわからない条件下の元、historyIdの入手に成功したはず。そしてhistoryIdからメール本文等を入手するのはさほど難しくないはず。説明もwatchより遥かに分かりやすいし。
[公式リファレンス(Users.history: list)](https://developers.google.com/gmail/api/v1/reference/users/history/list)
[Google APIのruby-doc](http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/GmailV1/GmailService)

(投げたわけじゃなく、ユーザーによってやりたいこと違うだろうから、書けないだけだよ)

# 次にやること
- 私は、次にwatchを通して届いたhistoryIdからメールタイトルを取得して、LINEのAPIと連携して、LINEの指定のグループに投稿させるようにしました。LINEと連携し、指定グループに投稿は別内容なので別記事にしようと思います。(そんな記事は世の中に溢れてるけど)

# メモ
Googleのクイックスタートの手順に
> click the Cancel button.

が入っているのは不思議なものです。

# あとがきと落とし穴
ここまで読んで下さってありがとうございます。
一つ、最大の落とし穴を今更お伝えします。watchを通して飛んでくるhistoryIdは様々な**ユーザーが欲しいであろうイベント**が起こった**後のhistoryId**を投げてきます。そして、historyIdからイベントを補足するメソッドは、様々な条件を付けれますが、なんと**対象historyId後のイベント**に対して検索をかけます。watchで飛んでくるhistoryIdは最新に近いモノなはずなので、検索かけても**ヒットしません**。私は解決策がわからず、watchから60程度を引いたhistoryIdを検索に使っています。なんとアホらしい…。

メソッド名がありきたりすぎるせいか、こんなことする人がマイナーなのか、日本のみならず、海外も含めてwatchメソッドに対しての情報があまりにもネットに転がっていません。頭のいい人、上記の問題、助けてください。