Posted at

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

More than 1 year has passed since last update.

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通信が必須)


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



Gemfile

source "https://rubygems.org"

gem 'sinatra'


Config.ru

require './app_main'

run Sinatra::Application


Procfile

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



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)
    が「分かり辛い」です。(Tryが用意されてるくせに、このTryは絶対に失敗します。当たり前だけど。)


  • こちらにもPush Notificationの説明と言いつつ、実はpushに対応しているのはGmailAPIの中でもwatchメソッドしか存在しないので、watchメソッドの説明が書かれています。


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


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


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)

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メソッドに対しての情報があまりにもネットに転がっていません。頭のいい人、上記の問題、助けてください。