はじめに
この記事は 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
を以下のように修正します。
const TOKEN_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
最終的なコード
先に最終的なコードを貼っておきます。
以下で説明していきます。
const fs = require('fs')
const { promisify } = require('util')
const { google } = require('googleapis')
const { OAuth2Client } = require('google-auth-library')
const gmail = google.gmail('v1')
const TOKEN_PATH = __dirname + '/../env/token.json'
const SECRET_PATH = __dirname + '/../env/credentials.json'
const sleep = msec => new Promise(resolve => setTimeout(resolve, msec))
const MAX_RETRY = 10
//Promise 化
const readFileAsync = promisify(fs.readFile)
const getMessageList = params => {
return new Promise((resolve, reject) => {
gmail.users.messages.list(params, (error, response) => {
if (error) {
reject(error)
return
}
resolve(response.data)
})
})
}
const getMessage = params => {
return new Promise((resolve, reject) => {
gmail.users.messages.get(params, (error, response) => {
if (error) {
reject(error)
return
}
resolve(response.data)
})
})
}
const getRegisterToken = async ({ email }) => {
//クレデンシャル情報の取得
const content = await readFileAsync(SECRET_PATH) //クライアントシークレットのファイルを指定
const credentials = JSON.parse(content) //クレデンシャル
//認証
const clientSecret = credentials.installed.client_secret
const clientId = credentials.installed.client_id
const redirectUrl = credentials.installed.redirect_uris[0]
const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUrl)
const token = await readFileAsync(TOKEN_PATH)
oauth2Client.credentials = JSON.parse(token)
//API経由でシートにアクセス
try {
const getToken = async () => {
// メッセージリスト取得
const data = await getMessageList({
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
}
const message = await getMessage({
auth: oauth2Client,
userId: 'me',
id: data.messages[0].id,
})
const text = Buffer.from(
message.payload.parts[1].body.data,
'base64'
).toString('utf8')
const regex = new RegExp(
/https:\/\/hoge\.piyo\.jp\/mail\/([A-Za-z0-9]+)/
)
const result = text.match(regex)
if (!result) {
console.log('not matched')
return
}
console.log('matched', result[1])
return result[1]
}
let retry = MAX_RETRY
let token = null
while (retry > 0) {
token = await getToken()
if (token) {
break
}
retry--
await sleep(10000)
}
console.log('token', token)
return token
} catch (err) {
return ''
}
}
module.exports = { getRegisterToken }
Promise化
async/awaitで書きたいので、各種関数をPromiseでラップします。
promisify
が使えるものについては、promisifyを使い、そうでないものはベタに書いていきます。
//promisifyでプロミス化
const readFileAsync = promisify(fs.readFile)
const getMessageList = params => {
return new Promise((resolve, reject) => {
gmail.users.messages.list(params, (error, response) => {
if (error) {
reject(error)
return
}
resolve(response.data)
})
})
}
const getMessage = params => {
return new Promise((resolve, reject) => {
gmail.users.messages.get(params, (error, response) => {
if (error) {
reject(error)
return
}
resolve(response.data)
})
})
}
認証データの準備
認証データをファイルから読み出し、OAuth2の認証クライアントを生成します。
//クレデンシャル情報の取得
const content = await readFileAsync(SECRET_PATH) //クライアントシークレットのファイルを指定
const credentials = JSON.parse(content) //クレデンシャル
//認証
const clientSecret = credentials.installed.client_secret
const clientId = credentials.installed.client_id
const redirectUrl = credentials.installed.redirect_uris[0]
const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUrl)
const token = await readFileAsync(TOKEN_PATH)
oauth2Client.credentials = JSON.parse(token)
メッセージリストの取得
メッセージリストを検索クエリを付与してフィルタリングし、取得します。
指定 | 意味 |
---|---|
is:unread | 未読 |
from:hoge@piyo.jp | hoge@piyo.jp から送信されている |
to:${email} | ${email} へ送信されている |
// メッセージリスト取得
const data = await getMessageList({
auth: oauth2Client,
userId: 'me',
q: `is:unread from:piyo@hoge.jp to:${email}`,
})
メール本文の取得
メール本文を取得します。
ここでは、メッセージリストで複数候補があっても1つ目のメールのみを取得しています。
const message = await getMessage({
auth: oauth2Client,
userId: 'me',
id: data.messages[0].id, //1つ目のメールを指定
})
デコード
Gmailでは本文は、UTF-8の文字列バイトデータがBase64エンコードされたものになっています。
そのため、Base64からバイト配列に変換し、UTF-8に変換、といったデコードが必要です。
const text = Buffer.from(
message.payload.parts[1].body.data,
'base64'
).toString('utf8')
文字列の抽出
ここまできたら正規表現で文字列を検索・抽出してあげるだけです!
const regex = new RegExp(
/https:\/\/hoge\.piyo\.jp\/mail\/([A-Za-z0-9]+)/
)
const result = text.match(regex)
リトライ
メールが直に送信されるとは限らないので、リトライの仕組みを入れています。
ここでは、10秒毎に最大10回リトライするようにしています。
let retry = MAX_RETRY
let token = null
while (retry > 0) {
token = await getToken()
if (token) {
break
}
retry--
await sleep(10000)
}
最後に
まだ追記するべき点がありますので、後ほど更新します!
時間切れにてここまで。何かのお役に立ちますように〜〜〜!