こんにちは、バックエンドエンジニアの まる です。
以前、Gmail アドレスに送られたメールの内容を Slack のチャンネルに送るような Slackbot の開発をしていた際に、「Gmail の受信 BOX を監視するのって意外と複雑だな」という気づきがあったので、そこで得られた知見をみなさんに共有したいと思います。
前編では、特定の Gmail アドレスの受信トレイにある全メールを Gmail API を使って Cloud Functions から取得する方法についてご説明しました。
後編では、Gmail の受信トレイを監視してメールを受信した際にそのメールを取得して外部サービスに送信する方法についてご説明します。今回ご説明するのは方法と構成についてのみで、GCP上の設定やコードについては割愛させていただきます。
構成図|Gmail APIを使って外部サービスにメールの新着を知らせる
実現したいことは非常にシンプルです。特定のGmailアドレスに新規メールが届いた際に、何らかの方法でCloud Functionsがトリガーされて、メールの件名と本文をSlackなどの外部サービスSlackなどに送信する。これだけです。
しかし、現実はこのようにはなっていません。なぜなら、(2)のような便利な通知機能は存在しないからです。
実際の構成図は以下のようになります。
想像上の構成図と比べてずいぶんと複雑になりました。GCPのサービスがCloud Functions以外に2つ増えていますし、Gmail APIを3回も呼んでいます。
順番に見ていきましょう。
(1) users.watch API
Gmail APIのusers.watchメソッドは、Gmailアドレスのイベントを監視するメソッドです。具体的には、users.watch を呼んでから1週間、特定のイベントが生じたときにCloud Pub/Subトピックにメッセージがパブリッシュされます。 今回は、「メールを受信した」というイベント[“INBOX”]が生じたときにメッセージがパブリッシュされるように設定します。 リクエストとレスポンスの例は以下のようになります。
POST: <https://www.googleapis.com/gmail/v1/users/me/watch>
{
topicName: "projects/sample-gmail-project-xxxxxx/topics/gmail-topic",
labelIds: ["INBOX"],
}
{
"historyId": "2810",
"expiration": "1664789009025"
}
注意点として、users.watchの監視は1週間しか続かないため、永続的に監視を続けたい場合は1週間ごとにusers.watchを自動的に呼ぶ仕組みを作る必要があります。Cloud SchedulerとCloud Pub/Subを利用するのが簡単でよいかと思います。
(3)(4)Cloud Pub/Sub
Cloud Pub/Subは、トピックにメッセージがパブリッシュされた際に、サブスクライバーにメッセージを送ります。
Cloud Pub/Subは奥が深くここでは全て説明することができないので、詳細は公式ドキュメントを参照するようにしてください。
今回は、メールを取得して外部サービスに送信するCloud Functionsがサブスクライバーとなります。(2)(3)(4)によって、「メールを受信した際にCloud Functionsがトリガーされる」が実現できます。
(5)(6)Cloud Storage
Cloud Pub/Sub から Cloud Functions に送られてくるメッセージには、メール本文などの情報が含まれていません。代わりにHistory IDという情報が入っています。
Gmailの受信トレイを監視するにはHistory IDを理解することが不可欠なので、History IDについて詳しく見ていきましょう。
History ID とはメールに関するあらゆる変更履歴を一意に示すIDです。メールの受信、ラベルの付与、メールの削除など、あらゆる変更に対してHistory IDが採番され、History IDを指定することでその変更の詳細を知ることができます。
新しい History ID が採番される際には、過去のHistory IDより大きな値が被りなく割り当てられますが、連番とは限らないことに注意してください。
また、(7)でご説明する users.history.list メソッドの厄介な仕様として、指定したHistory IDより後から現在までに発生したイベントを取得することになります。
これが何を意味するかというと、新規メールBを受信したことで発生するイベントによってPub/Subにメッセージがパブリッシュされ、Cloud Function がHistory ID:”1104”というメッセージを受け取り、そのままHistory ID:”1104”を指定してusers.history.list APIを叩いたとしても、何のイベントも取得できません。なぜなら、History ID:”1104”より後には何のイベントも発生していないからです。(以降ではなくより後であることに注意してください)
History ID:”1104”で発生したイベントを取得したいのであれば、過去のHistory ID(例えば”1001”)を指定すればよいです。
したがって、CloudFunctionsがメッセージを受け取る前に過去のHistory IDをどこかに保存しておく必要がありますが、Cloud Functionsは永続ストレージを持たないため、CloudStorageなどの外部ストレージを使用することになります。
外部ストレージの種類は(文字列を保存することができれば)何でもいいので、他のGCPのサービス(Cloud Spanner, Cloud SQLなど)に置き換えても問題ありません。この記事では、前編でtoken.jsonを保存するためにCloud Storageを使用していたため、使いまわす形でCloud Storageにしています。
History IDを複数保存する必要はありません。保存しておくのは前回メールを受信したときのHistory IDだけでよいです。理由は(7)でご説明します。
(7) users.history.list
Gmail APIのusers.history.listメソッドは、前節でご説明したように、指定したHistory IDより後から現在までに発生したイベントを取得するメソッドです。
(5)(6)でご説明したように、特定のイベントより前のHistory IDを指定すればそのイベントを取得することができます。Cloud Functionsはメールを受信するたびに起動するため、無難に前回のメール受信イベントのHistory IDを使えばよいでしょう。
例えば、メールBを受信したとき、メールAを受信したときのHistory IDを使用してusers.history.listを呼び、メールBを受信したときのHistory IDを上書き保存します。こうすれば、メールCを受信したときはメールBを受信したときのHistory IDを使用してusers.history.listを呼ぶことができます。
また、クエリパラメータにhistoryTypeを指定することで、特定の種類のイベントだけを取得することができます。今回はhistoryType=MessageAddedを指定し、メールの受信イベントだけを取得します。
リクエストとレスポンスの例は以下のようになります。
GET: <https://gmail.googleapis.com/gmail/v1/users/example@gmail.com/history?startHistoryId=2552&labelId=UNREAD&historyTypes=MessageAdded>
{
"history": [
{
"id": "2626",
"messages": [
{
"id": "183647a74b0dafec",
"threadId": "183647a74b0dafec"
}
],
"messagesAdded": [
{
"message": {
"id": "183647a74b0dafec",
"threadId": "183647a74b0dafec",
"labelIds": [
"UNREAD",
"IMPORTANT",
"CATEGORY_PERSONAL",
"INBOX"
]
}
}
]
}
],
"historyId": "2679"
}
残念ながら、users.history.listではメールの本文を取得することはできません。代わりに取得できるのはmessageのID(183647a74b0dafec)です。
(8)のリクエストにmessage IDを指定することで、メールの本文を取得することができます。
(8) users.message.get
Gmail APIのusers.messages.getメソッドは、message IDを指定してmessageの情報を取得することができます。ここでいう「message」はメールと同一だと考えてよいです。
リクエストとレスポンスの例は以下のようになります。
GET: <https://gmail.googleapis.com/gmail/v1/users/example@gmail.com/messages/183647a74b0dafec?format=minimal>
{
"id": "183647a74b0dafec",
"threadId": "183647a74b0dafec",
"labelIds": [
"IMPORTANT",
"CATEGORY_PERSONAL",
"INBOX"
],
"snippet": "こんにちは",
"sizeEstimate": 4809,
"historyId": "2753",
"internalDate": "1663838078000"
}
メールの本文が”snippet”に入っていることが確認できます。メールのタイトルについてはformat=minimalでは取得できないので、format=fullなど指定しましょう。
おわりに
前編では、特定の Gmail アドレスの受信トレイにあるメールを Gmail API を使って Cloud Functions から取得する方法について、後編では、Gmail の受信トレイを監視してメールを受信した際にそのメールを取得して外部サービスに送信する方法についてご説明しました。
後編では概要の説明だけで終わってしまいましたが、前編の知識を使えば実際に動かすことも可能かと思います。
この記事がみなさんのお役に立てば幸いです。