Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
19
Help us understand the problem. What is going on with this article?
@1ulce

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

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

19
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1ulce
Value Maker Co., Ltd. CEO

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
19
Help us understand the problem. What is going on with this article?