Python
GAE
gmail
PubSub

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

この記事はCAMPHOR- advent calender 2018の23日目の記事です

動機

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

pub/subの設定

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

gmail apiの認証

公式のPython Quickstartを参考に進めます。上記のページに出てくるquickstart.pyを少し変えて、

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が作成されて、トークンがその中に保存されます。そのため、2回目以降はブラウザで認証する必要がありません

Pub/Subから送られてくるデータを加工する

ここまでの過程がうまくいくと、GAEで、pub/subから送られるjsonを受け取れます。公式のPush Notificationのページを参考にします

受け取るjsonの例
{
    "message": {
        "data": "eyJlbWFpbEFkZHJlc3MiOiAidXNlckBleGFtcGxlLmNvbSIsICJoaXN0b3J5SWQiOiAiMTIzNDU2Nzg5MCJ9",
        "message_id": "1234567890"
    },
    "subscription": "projects/myproject/subscriptions/mysubscription"
}

"data"をbase64でデコードすると、以下のようになります

{"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

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を保存するようにしました

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から本文や宛先などを取得するのは、マイナビニュースのページ34が詳しいです。

linebotを作る

developer trialならpush_messageが送れます。line-bot-sdk-pythonのREADMEを参考にしましょう

完成したもの

こんな感じ
スクリーンショット (39).png