Cloud Pub/Subを使ってgmailの通知を受け取るlinebot

この記事は[CAMPHOR- advent calender 2018](https://advent.camph.net/)の23日目の記事です

# 動機
私の所属している東洋医学研究会（通称:東医研）には、専用のgmailアカウントがありますが、ほとんどの人がメールを見ていないので、今までは受信や送信メールがあれば、人力でグループLINEに投稿していました。これをbotでなんとかしたかった。

## pub/subの設定
まずは、pub/subの設定をします。この設定をすることで、gmailに何らかの変更があった場合には、pub/subを通じてpostリクエストが届きます。
Gmail APIとPub/Subでリアルタイムメール受信 on ruby[^1]の記事がとても参考になりました。この通りやったらうまくいきます
1. トピックを設定する
2. サブスクリプションを設定する
私の場合は、サブスクリプションのurlはhttps://***プロジェクト名***.appspot.com/pubsub/pushで受け取るようにしました

## gmail apiの認証
公式の[Python Quickstart](https://developers.google.com/gmail/api/quickstart/python)を参考に進めます。上記のページに出てくるquickstart.pyを少し変えて、

```python
store = file.Storage('token.json')
creds = store.get()
if not creds or creds.invalid:
    flow = client.flow_from_clientsecrets('credentials.json', SCOPES)
    creds = tools.run_flow(flow, store)
service = build('gmail', 'v1', http=creds.authorize(Http()))

#追加したコード
request = {
        'labelIds': ["INBOX","SENT"],
        'topicName': 'projects/******/topics/mail_for_toiken'
    }
service.users().watch(userId='me', body=request).execute()
```
を実行すると、これでwatchメソッドが叩けたことになります。その際、crediential.jsonが作成されて、トークンがその中に保存されます。そのため、２回目以降はブラウザで認証する必要がありません

## Pub/Subから送られてくるデータを加工する
ここまでの過程がうまくいくと、GAEで、pub/subから送られるjsonを受け取れます。公式の[Push Notification](https://developers.google.com/gmail/api/guides/push)のページを参考にします

```json:受け取るjsonの例
{
	"message": {
		"data": "eyJlbWFpbEFkZHJlc3MiOiAidXNlckBleGFtcGxlLmNvbSIsICJoaXN0b3J5SWQiOiAiMTIzNDU2Nzg5MCJ9",
		"message_id": "1234567890"
	},
	"subscription": "projects/myproject/subscriptions/mysubscription"
}
```
"data"をbase64でデコードすると、以下のようになります

```json
{"emailAddress": "user@example.com", "historyId": "1234567890"}
```
gmail apiで重要なのは、historyIdだけです。

## GoogleAppEngineでgmail apiを使えるようにする。
実は、「gmail api の認証」の項で使ったコードはそのまま使用できません。GAEにアップロードしたファイルはread_only属性がつくので、認証の際に```IOError(errno.EROFS, 'Read-only file system', filename)```が返って来てしまいます。
```oauth2client.client.GoogleCredentials```を使うとうまくいきます[^2]

```python:example
secret = json.load(open("client_secret_for_touiken.json"))
creds = GoogleCredentials(secret["access_token"], secret["client_id"], secret["client_secret"],
                      secret["refresh_token"], secret["token_expiry"], secret["token_uri"],
                      secret["user_agent"], secret["revoke_uri"])
service = build('gmail', 'v1', http=creds.authorize(Http()), cache_discovery=False)
```
## historyIdからmessageを取得する
参考記事[^1]にも書かれていますが、start_history_idをhistoryIdと同じにしてはいけません。私は、適当なclassを作って、前回のhistoryIdを保存するようにしました

```python:historyIdからメッセージリストを取得
class PubSubReceiver:
    def __init__(self, start_history_id=None, history_id=None):
        self.start_history_id = start_history_id
        self.historyId = history_id

    def data(self, data):
        history_id = int(json.loads(base64.b64decode(data))["historyId"])
        if self.start_history_id is None:
            self.start_history_id = history_id - 60
        elif self.historyId < history_id:
            self.start_history_id = self.historyId
        else:
            pass
        self.historyId = history_id
        return PubSubReceiver(start_history_id=self.start_history_id,
                              history_id=history_id)

    def to_array(self):
        logging.info("to_array")
        try:
            service = Service.create_service()
            logging.debug("service_create")
            history = (service.users().history().list(userId="me",
                                                      startHistoryId=self.start_history_id)
                       .execute())
            logging.debug("get_history")
            logging.debug(str(self.historyId) + ":" + str(history["historyId"]) + ":" + str(
                self.historyId == int(history["historyId"])) + str("nextPageToken" in history))
            history_list = history['history'] if 'history' in history else []

            if self.historyId == int(history["historyId"]):
                return history_list

            while 'nextPageToken' in history:
                print(history, history_list[0], history["historyId"])
                page_token = history['nextPageToken']
                history = (service.users().history().list(userId="me",
                                                          startHistoryId=self.start_history_id,
                                                          pageToken=page_token).execute())
                history_list = history['history'] if 'history' in history else []
                if self.historyId == int(history["historyId"]):
                    return history_list

        except errors.HttpError as error:
            logging.info('An error occurred: %s' % error)
```
history_idを解析すれば、messageのidを入手できます。idから本文や宛先などを取得するのは、マイナビニュースのページ[^3][^4]が詳しいです。

## linebotを作る
developer trialなら```push_message```が送れます。[line-bot-sdk-python](https://github.com/line/line-bot-sdk-python)のREADMEを参考にしましょう

# 完成したもの
こんな感じ
![スクリーンショット (39).png](https://qiita-image-store.s3.amazonaws.com/0/251318/1c37f63c-43db-be1d-f131-6babfb5353b6.png)

[^1]: https://qiita.com/1ulce/items/672ae85d8c23bd9c478e
[^2]: https://stackoverflow.com/questions/49753494/access-to-google-search-console-api-through-google-app-engine-read-only-file-sy
[^3]: https://news.mynavi.jp/article/zeropython-22/
[^4]: https://news.mynavi.jp/article/zeropython-23/