はじめに
この記事は Goodpatch Advent Calendar 2019の22日目です.
私が現在担当しているWebサービスの開発において、Puppeteerを用いたe2eテストを用いてQAの効率化を図っています。
この記事では Node.js と Gmail API を使い、アカウント作成時のメール認証を自動化する方法について共有したいと思います。
注:この記事ではPuppeteerには触れません!
環境準備
メールをNode.jsで取得するためには、Gmail APIの設定と、各種ファイルの取得・生成が必要です。
基本的にはNode.js Quickstartに従って作業します。
フォルダの準備
あらかじめ、各種ファイルを保存するフォルダの準備しておきます。
例として、以下のような構造にします。
Gmail Credentialの取得と配置
あらかじめ、利用するGmailのアカウントでログインしておきます。
ログイン後、以下のページを開きます
Node.js Quickstart | Gmail API
モーダルが表示されるので、ボタンを押してcredensial.json
をダウンロードし、
トークンの取得
Node.js Quickstart | Gmail APIの Step2 に従って、以下をインストールします。
$ npm install googleapis@39 --save
Node.js Quickstart | Gmail API
中のStep 3 のコードをコピーし、 e2e/scripts/get-token.js
と名付けて保存します。
フォルダ構造を合わせるため、credentials.js
のファイルパスと、トークンの出力先パス TOKEN_PATH
を以下のように修正します。
constTOKEN_PATH=__dirname+'/../env/token.json';// Load client secrets from a local file.fs.readFile(__dirname+'/../env/credentials.json',(err,content)=>{
保存後、以下のコマンドを実行します。
$ node e2e/scripts/get-token
すると、以下ようにURLがあらわれるので、言われた通りこのページを開きます。
ログインするアカウントを選択すると、以下の画面がでて来るので、詳細を表示
し Quickstartに移動
します
ターミナルに戻って Enter the code from that page here:
のあとに貼り付け、Enterします。
すると、Gmailで利用しているラベルの一覧が表示され、e2e/env
フォルダに token.jsファイルが生成されます。
これで、Node.jsからGmailを利用する準備は完了です。
Gmailから目的のメールを取得する
準備が長かった気がしますが、ここからが本番です。
数多くのメールの中から、認証リンクを含んだメールを探し、本文からリンク取得します。
取得すべきメール
取得すべきメールは以下のようなものです。
APIのフィルタ機能と、本文への正規表現を用いた検索を使ってこのメールを探します。
- 未読状態である。
- Webサービスのメール送信用アドレスから送信されている。
(ここではhoge@piyo.jp
とします) - 登録したメールアドレスに送信されている。
(登録に利用したメールアドレスを引数として渡す) - 本文に認証リンクを含む。
(ここではhttps://hoge.piyo.jp/mail/XXXXXX
のフォーマットであるとします)
環境準備
追加で以下のpackageをインストールします。
$ npm i google-auth-library -s
最終的なコード
先に最終的なコードを貼っておきます。
以下で説明していきます。
constfs=require('fs')const{promisify}=require('util')const{google}=require('googleapis')const{OAuth2Client}=require('google-auth-library')constgmail=google.gmail('v1')constTOKEN_PATH=__dirname+'/../env/token.json'constSECRET_PATH=__dirname+'/../env/credentials.json'constsleep=msec=>newPromise(resolve=>setTimeout(resolve,msec))constMAX_RETRY=10//Promise 化constreadFileAsync=promisify(fs.readFile)constgetMessageList=params=>{returnnewPromise((resolve,reject)=>{gmail.users.messages.list(params,(error,response)=>{if(error){reject(error)return}resolve(response.data)})})}constgetMessage=params=>{returnnewPromise((resolve,reject)=>{gmail.users.messages.get(params,(error,response)=>{if(error){reject(error)return}resolve(response.data)})})}constgetRegisterToken=async({email})=>{//クレデンシャル情報の取得constcontent=awaitreadFileAsync(SECRET_PATH)//クライアントシークレットのファイルを指定constcredentials=JSON.parse(content)//クレデンシャル//認証constclientSecret=credentials.installed.client_secretconstclientId=credentials.installed.client_idconstredirectUrl=credentials.installed.redirect_uris[0]constoauth2Client=newOAuth2Client(clientId,clientSecret,redirectUrl)consttoken=awaitreadFileAsync(TOKEN_PATH)oauth2Client.credentials=JSON.parse(token)//API経由でシートにアクセスtry{constgetToken=async()=>{// メッセージリスト取得constdata=awaitgetMessageList({auth:oauth2Client,userId:'me',q:`is:unread from:piyo@hoge.jp to:${email}`,})if(!data.messages||data.messages.length===0){console.log('no message')return}constmessage=awaitgetMessage({auth:oauth2Client,userId:'me',id:data.messages[0].id,})consttext=Buffer.from(message.payload.parts[1].body.data,'base64').toString('utf8')constregex=newRegExp(/https:\/\/hoge\.piyo\.jp\/mail\/([A-Za-z0-9]+)/)constresult=text.match(regex)if(!result){console.log('not matched')return}console.log('matched',result[1])returnresult[1]}letretry=MAX_RETRYlettoken=nullwhile(retry>0){token=awaitgetToken()if(token){break}retry--awaitsleep(10000)}console.log('token',token)returntoken}catch(err){return''}}module.exports={getRegisterToken}
Promise化
async/awaitで書きたいので、各種関数をPromiseでラップします。promisify
が使えるものについては、promisifyを使い、そうでないものはベタに書いていきます。
//promisifyでプロミス化constreadFileAsync=promisify(fs.readFile)constgetMessageList=params=>{returnnewPromise((resolve,reject)=>{gmail.users.messages.list(params,(error,response)=>{if(error){reject(error)return}resolve(response.data)})})}constgetMessage=params=>{returnnewPromise((resolve,reject)=>{gmail.users.messages.get(params,(error,response)=>{if(error){reject(error)return}resolve(response.data)})})}
認証データの準備
認証データをファイルから読み出し、OAuth2の認証クライアントを生成します。
//クレデンシャル情報の取得constcontent=awaitreadFileAsync(SECRET_PATH)//クライアントシークレットのファイルを指定constcredentials=JSON.parse(content)//クレデンシャル//認証constclientSecret=credentials.installed.client_secretconstclientId=credentials.installed.client_idconstredirectUrl=credentials.installed.redirect_uris[0]constoauth2Client=newOAuth2Client(clientId,clientSecret,redirectUrl)consttoken=awaitreadFileAsync(TOKEN_PATH)oauth2Client.credentials=JSON.parse(token)
メッセージリストの取得
メッセージリストを検索クエリを付与してフィルタリングし、取得します。
指定 | 意味 |
---|---|
is:unread | 未読 |
from:hoge@piyo.jp | hoge@piyo.jpから送信されている |
to:${email} | ${email} へ送信されている |
// メッセージリスト取得constdata=awaitgetMessageList({auth:oauth2Client,userId:'me',q:`is:unread from:piyo@hoge.jp to:${email}`,})
メール本文の取得
メール本文を取得します。
ここでは、メッセージリストで複数候補があっても1つ目のメールのみを取得しています。
constmessage=awaitgetMessage({auth:oauth2Client,userId:'me',id:data.messages[0].id,//1つ目のメールを指定})
デコード
Gmailでは本文は、UTF-8の文字列バイトデータがBase64エンコードされたものになっています。
そのため、Base64からバイト配列に変換し、UTF-8に変換、といったデコードが必要です。
consttext=Buffer.from(message.payload.parts[1].body.data,'base64').toString('utf8')
文字列の抽出
ここまできたら正規表現で文字列を検索・抽出してあげるだけです!
constregex=newRegExp(/https:\/\/hoge\.piyo\.jp\/mail\/([A-Za-z0-9]+)/)constresult=text.match(regex)
リトライ
メールが直に送信されるとは限らないので、リトライの仕組みを入れています。
ここでは、10秒毎に最大10回リトライするようにしています。
letretry=MAX_RETRYlettoken=nullwhile(retry>0){token=awaitgetToken()if(token){break}retry--awaitsleep(10000)}
最後に
まだ追記するべき点がありますので、後ほど更新します!
時間切れにてここまで。何かのお役に立ちますように〜〜〜!