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のコンソールから、プロジェクトを作成
設定
-
作成後のページ、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?を参照。
- GCPのPub/Subのコンソールを開き、上部に目的のプロジェクトが表示されてることを確認する
- 適当な名前でトピックを作成
- トピックを選択し、右上の
情報パネルを表示
を選択後、gmail-api-push@system.gserviceaccount.com
というメンバーをPub/Subパブリッシャー
の役割に設定し、追加
3,あと一歩! トピックにWebhookでのSubscriptionを作成する
- まずは単純なSubscribeが出来るか確認したいので、Herokuに適当にサーバーを立てる。
(もちろんHerokuでなくても構わないが、HTTPS通信が必須)
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
source "https://rubygems.org"
gem 'sinatra'
require './app_main'
run Sinatra::Application
web: bundle exec rackup config.ru -p $PORT
-
最低限、これら4つをHerokuにデプロイすればとりあえず動きます。デプロイの仕方はQiitaのこちらの記事を参考にしてみて下さい。
-
先ほど作成したPub/Subのトピックに対し、トピックの詳細画面から
サブスクリプションを作成
する。配信タイプはエンドポイント URLにpush
で、URLはHerokuのURL(https://○○.herokuapp.com/)の末尾にWebhook受信用に設定した(/callback)をくっつけて、作成。 -
先ほど作成したPub/Subのトピックに対し、トピックの詳細画面から手動で
メッセージを公開
をする。
$ Heroku logs -t
等でログを見て、受信できていれば成功。
4,最後にGmail APIからPublishできるように!
困ったらいつでも頼る公式のガイドはこちら
メール送信、メール受信、設定の変更まで、gmailの殆どの操作を行えます。watchメソッドでは、リファレンス上は、リアルタイムにgmailの変更(メール受信、メール削除、その他なんでも)をお知らせしてくれます。ひゅー、夢あるぅ
-
公式のクイックスタートに沿って進めましょう。英語だけど頑張って。進めていくと、前述したGCPの認証情報を作成もします。
-
Step4まで完了し、上手く動いたなら、今回のプロジェクト用に途中にあるコードは、こちらに変更しましょう
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)が「分かり辛い」です。(Tryが用意されてるくせに、このTryは絶対に失敗します。当たり前だけど。) -
こちらにもPush Notificationの説明と言いつつ、実はpushに対応しているのはGmailAPIの中でもwatchメソッドしか存在しないので、watchメソッドの説明が書かれています。
-
ユーザーがテスト出来ないので、テストした私が噛み砕くと、watchメソッドは、「gmail上のどんな些細な変更も順番に記録されているhistoryIDの中から、有る特定のイベント(正確な条件は私は調べても分からなかった)が起きた際に、指定されたPub/Sub上のトピックに、historyIDとその他些細なデータを含めたメッセージをPublishすることを試みることを一定期間において継続的に実行する」ことを起動するメソッドである。あまりにもわかりづらいね。
なのでwatchメソッドは定期的(最低7日間に1回)に叩いてあげなければならないみたいです。クソ。考えるのが嫌なら以下推奨。
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)
Google APIのruby-doc
(投げたわけじゃなく、ユーザーによってやりたいこと違うだろうから、書けないだけだよ)
次にやること
- 私は、次にwatchを通して届いたhistoryIdからメールタイトルを取得して、LINEのAPIと連携して、LINEの指定のグループに投稿させるようにしました。LINEと連携し、指定グループに投稿は別内容なので別記事にしようと思います。(そんな記事は世の中に溢れてるけど)
メモ
Googleのクイックスタートの手順に
click the Cancel button.
が入っているのは不思議なものです。
あとがきと落とし穴
ここまで読んで下さってありがとうございます。
一つ、最大の落とし穴を今更お伝えします。watchを通して飛んでくるhistoryIdは様々なユーザーが欲しいであろうイベントが起こった後のhistoryIdを投げてきます。そして、historyIdからイベントを補足するメソッドは、様々な条件を付けれますが、なんと対象historyId後のイベントに対して検索をかけます。watchで飛んでくるhistoryIdは最新に近いモノなはずなので、検索かけてもヒットしません。私は解決策がわからず、watchから60程度を引いたhistoryIdを検索に使っています。なんとアホらしい…。
メソッド名がありきたりすぎるせいか、こんなことする人がマイナーなのか、日本のみならず、海外も含めてwatchメソッドに対しての情報があまりにもネットに転がっていません。頭のいい人、上記の問題、助けてください。