スポンサーリンク

node.jsでGmail APIを使ってメール一覧や本文を取得するサンプル

前回の記事、「GmailでIMAPが使えなくなるので自作プログラム用に認証キーを取得する」の続きです。

前回はGoogle Cloud Platformでプロジェクトを作成し、認証情報の入ったJSONファイルをダウンロードしましたので、今回はそれを使ってGmailへOAuth2.0で接続し、メールの一覧を取得するサンプルプログラムを作ってみます。

以前は「PHPでIMAPを使いメールの本文を取得する例」で書いたとおり、PHPでIMAP接続を使い、Gmailからメールを取得していました。

ただ、メンテナンス用の定期タスクで動かすことが多いプログラムですので、今後はChromeブラウザを自動操縦するPuppeteerも組み合わせて使っていきたいな、という目論見もあります。実際、今でも「puppeteerでAmazonのレポートページに自動ログインしてSSを撮る」の例にあるとおり、Amazonアソシエイトのレポートを自動でDLするようなタスクを組んで運用していたりします。

なので、Gmailの取得も、Chromeの自動操縦も全部Node.jsで一本化できたらスマートかな、とそんなふうに思ったわけです。

スポンサーリンク

高機能なGoogleの公式ドキュメント

身も蓋もない話ですが、英語アレルギーでない方はGoogle Developersの公式ドキュメントを参考にするのが一番良いです。

https://developers.google.com/gmail/api/quickstart/nodejs

ただ、この例ではGmailのラベル一覧の読み込みだけなので、題名や本文はどうするのかなーとわからなかったので、あらためて下記のGmail APIリファレンスを読みながら自分でコーディングしてみた次第です。

https://developers.google.com/gmail/api/reference/rest

このドキュメント、APIs Explorerという機能が付いていて、画面右側にパラメーターを入力するとすぐその場でAPIの実行を試せるのはすごく便利なのですが、そのせいかサンプルコードが少なく感じます。

…PHPのようなサンプルコードが豊富すぎるほどに豊富なドキュメントに慣れすぎてしまっただけかも知れませんw

Node.jsをポータブル環境で使う方法

本題に入る前に環境構築に関する余談を少し。

Node.jsはWindowsで使う場合でも下記ページから「推奨版」をダウンロードしてセットアッププログラムを実行し、[次へ][次へ]と進めるだけで何も難しいことを聞かれることもなく設定が完了します。

https://nodejs.org/ja/

しかし、毎回開発環境を移動したり、毎月のようにOSを初期化するような病気癖のあるぼくのような人の場合、環境はなるべくポータブルにしたい。セットアッププログラムを走らせずとも、単にフォルダコピーだけで環境構築が出来るようにしたい、そんな需要もあると思うのです。

幸いなことにNode.jsについてもそういった配慮から(かどうかは知りませんが)、セットアップ不要なモジュールも配布されています。

https://nodejs.org/ja/download/

↑のページにある「Windows Binary (.zip)」ですね。このZIPファイルを好きなフォルダに解凍すればすぐにnodejsが使用可能。

但し、パスが通っていないとコマンドが使いづらいですし、NODE_PATHという環境変数が設定されていないとnpmコマンドでモジュールをインストールするときに困ります。

よって、あくまで一例ですが、下記のようなバッチファイルを用意して、これを実行してからnodejsを使うようにすると便利かも知れません。

@echo off
set PATH=%PATH%;%cd%
set NODE_PATH=%cd%\node_modules\
cmd

実際、ぼくはネットワークドライブ上にnodejsを置いて、上記のバッチファイルを使うことで複数のマシンで同じ環境を共有できています。

まぁ、nodejsはモジュールが増えてくると細かい小さなファイルをたくさん読み込むせいか、ネットワークドライブだと動作が重いのが難点ですかね。

以上、node.jsをポータブル環境で使う話でした。

Node.jsでGoogle APIを使う準備

Linuxのことはよく知らないので、Windows環境での話です。

Node.jsは既にセットアップ済として、まずはテキトーなディレクトリを作って「npm init -y」でpackage.jsonを作り、次に「npm install googleapis」でパッケージをインストールするだけです。

■実際の実行例

C:\>mkdir gmail
C:\>cd gmail
C:\gmail>npm init -y
Wrote to C:\gmail\package.json:

C:\gmail>npm install googleapis

added 42 packages, and audited 43 packages in 8s

9 packages are looking for funding
run `npm fund` for details

found 0 vulnerabilities

あとはここにJavaScript用のテキストファイルを作り、実際にコーディングしていきましょう。

Gmail APIでトークンを取得する例

Googleのサンプルではトークンの取得~ラベル一覧の取得まで1つのスクリプトで実行していましたが、そこは別にひとつにする必要はなく、一度取ったトークンはしばらく使いまわすと思うので、まずはトークン取得部分のみ作りました。

■get_token.js

//設定
const SCOPES = ['https://www.googleapis.com/auth/gmail.modify'];
const TOKEN_PATH = 'token.json';
const {google} = require('googleapis');
const fs = require('fs');

//クライアントID、秘密キー、リダイレクトURLが入ったJSONファイルを読み込む
const content = fs.readFileSync('credentials.json');
credentials = JSON.parse(content);
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);

//認証用URLの生成
let url = oAuth2Client.generateAuthUrl({access_type:'offline', scope:SCOPES});
console.log('自動でブラウザが開かない場合は手動でURLを開いてください:' + url);

//ブラウザで認証用URLを開く
const exec = require('child_process').exec;
exec('start ' + url.replaceAll('&', '^&')); //^&は&のエスケープ処理

//コード入力用
const readline = require('readline');
const rl = readline.createInterface({input: process.stdin,output: process.stdout});
rl.question('表示されたコードを貼り付けてください:', (code) => {
	rl.close();
	oAuth2Client.getToken(code, (err, token) => {
		if (err) {
			return console.error('アクセストークンの取得に失敗しました', err);
		}
		const fs = require('fs');
		fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
			if (err) {
				return console.error(err);
			}
			console.log('トークンを ' + TOKEN_PATH + ' に保存しました');
		});
	});
});

解説:スコープの設定

const SCOPES = ['https://www.googleapis.com/auth/gmail.modify'];

この部分ですが、前回の記事のOAuth同意画面で設定したスコープです。要するに権限。この例ではgmail.modifyですが、本当に読み取りしかしないならgmail.readonlyでも良いです。どちらにせよ、Google Cloud Platformの画面であらかじめ権限を設定しておく必要があります。ここが間違っていると認証画面で「お前にその権限はねーよ」的なことを言われて怒られます。

解説:認証用のJSONファイル

const content = fs.readFileSync('credentials.json');

これは認証用のJSONファイルを読み込んでいる部分ですが、credentials.jsonとハードコーディングしてしまっているので、Google Cloud PlatformでダウンロードしたJSONファイルをcredentials.jsonとリネームしておいてください。

ちなみに、このjsonファイルの中身は、↓こんなふうになっています。

{
	"installed":{
		"client_id":"[クライアントID]",
		"project_id":"[プロジェクトID]",
		"auth_uri":"https://accounts.google.com/o/oauth2/auth",
		"token_uri":"https://oauth2.googleapis.com/token",
		"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
		"client_secret":"[シークレットキー]",
		"redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]
	}
}

この中で使っている値は client_secret, client_id, redirect_uris[0] だけですし、redirect_urisに至っては固定なので、わざわざJSONファイル読み込むのがめんどくせーと思う人は client_secret と client_id の2つをスクリプト中にハードコーディングしても構いません。

今回はトークン保存用のスクリプトを get_token.js、Gmail読み込み用のスクリプトを gmail.js として2つのスクリプトを作る予定なので、両方に固定値書くのもアレだしなぁーということで、JSONファイルを読むことにしました。

そもそもGoogle Cloud PlatformでわざわざJSONファイルをDLする機能があるし、公式ドキュメントでもそういう使い方をしているので、従っておきましょ。

解説:ブラウザ起動

//ブラウザで認証用URLを開く
const exec = require('child_process').exec;
exec('start ' + url.replaceAll('&', '^&')); //^&は&のエスケープ処理

この部分はGoogleのサンプルから大きく変えました。Googleサンプルでは単にURLを表示して「ブラウザで開いてね」とメッセージを出しているだけですが、URLコピーしてブラウザ開いてアドレス欄に貼り付ける、なんて操作、クソ面倒くさくありませんか?

URLわかってるんなら勝手にブラウザで開いてくれよ…と思ったのでそのようにしました。

やってることは「start [URL]」というコマンドを実行してるだけです。普通、WindowsでstartコマンドにURLを渡せばデフォルトのブラウザが勝手に開くので、その動作に任せているだけですね。

注意点としてはコマンドライン上での & は特別な意味を持つので、^& という記号に変換しています。いわゆるエスケープ処理。それだけです。

正常にブラウザが起動すると下記のようなログイン画面が出てくるはずです。

■Googleのログイン画面
Googleのログイン画面

「Gmail用テスト」という表示がありますが、これは前回の記事で登録したアプリの名称。

■警告画面
警告画面

ログインに成功すると、お次は「テスト中のアプリだけど大丈夫か?招待元の開発者は信用できるんか?」と聞かれます。青字のボタンを押したくなりますが、そこはグッと堪えて[続行]ボタンを押します。

■更に警告画面
更に警告画面

このアプリにアクセス権限、具体的にはGmailアカウントのメール閲覧、作成、送信の権限を与えようとしてるけど大丈夫?という警告が出ます。スコープ設定でgmail.modifyを選んでいるからですね。これがgmail.readonlyだと「メール閲覧」だけ表示されるのだと思います。

自分で作ったアプリで自分のアドレスにアクセスするだけなので、気にせず[続行]ボタンを押します。

■認証コード、ゲットだぜ!
認証コード

ここまできて、ようやく認証コードがもらえました。上の画面の黒塗りした部分が認証コードですが、右にあるアイコンでクリップボードにコピーできるので、そのままNode.jsの画面に戻って貼り付ければOK。

Node.js側では token.js というテキストファイルにここで取得したトークン情報を保存します。

Gmail APIでメールの一覧とタイトル/本文を取得する例

■gmail.js

//設定
const TOKEN_PATH = 'token.json';
const {google} = require('googleapis');
const fs = require('fs');

//クライアントID、秘密キー、リダイレクトURLが入ったJSONファイルを読み込む
const content = fs.readFileSync('credentials.json');
credentials = JSON.parse(content);
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);

//トークンの読み込み
const token = fs.readFileSync(TOKEN_PATH);
oAuth2Client.setCredentials(JSON.parse(token));
enumMails(oAuth2Client);

//指定したラベルIDのメール一覧を取得
async function enumMails(auth)
{
	const gmail = google.gmail({version:'v1', auth});
	const list = await gmail.users.messages.list({userId: 'me'});

	if (!list.data.messages) {
		return;
	}
	for (let i = 0; i < list.data.messages.length; i++) {
		const msg = await gmail.users.messages.get({userId:'me',id:list.data.messages[i].id});
		//サブジェクトをキーに出来ないっぽいのでヘッダーから探す(ホントにこれであってる…?)
		for (let j = 0; j < msg.data.payload.headers.length; j++) {
			if (msg.data.payload.headers[j].name.toLowerCase() == 'subject') {
				console.log(list.data.messages[i].id + ':' + msg.data.payload.headers[j].value);
				break;
			}
		}
		//本文の取得
		const body = new Buffer.from(msg.data.payload.body.data, 'base64').toString(); //Base64変換
		console.log(body);
		if (i > 10) {
			break;
		}
	}
}

解説:トークンの設定

最初のcredentials.jsonを読んで、OAuth2オブジェクトを初期化する部分はget_token.jsと同じなので省略します。

const token = fs.readFileSync(TOKEN_PATH);
oAuth2Client.setCredentials(JSON.parse(token));

ここでは認証キーを貼り付けて取得した token.json を連想配列に戻してsetCredentialsに渡していますが、基本これだけで認証が通るハズです。ちなみに、token.json の中身は↓こんな感じ。

{
	"access_token":"[アクセストークン]",
	"refresh_token":"[リフレッシュトークン]",
	"scope":"https://www.googleapis.com/auth/gmail.modify",
	"token_type":"Bearer",
	"expiry_date":1646985096343
}

解説:メールの検索

const list = await gmail.users.messages.list({userId: 'me'});

この部分でメールの一覧を取得していますが、パラメーターを渡すことで文言で絞り込んだり、ラベル指定したりも可能です。

https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list

基本的にGmailのWebアプリと同じ用法で良いので、例えば未読のメール一覧が欲しいときは is:unread で検索すればOK。

const list = await gmail.users.messages.list({userId: 'me',q:'is:unread'});

ラベルを指定して検索したい場合はラベルのIDを指定する必要があります。

const list = await gmail.users.messages.list({userId: 'me',q:'is:unread',labelIds:'IMPORTANT'});

デフォルトで存在するラベルはIDが決まっているようで、うちのアカウントで調べた限りは下記のとおりでした。

ラベル名ラベルID
受信トレイINBOX
重要IMPORTANT
スター付きSTARRED
未読UNREAD
下書きDRAFT
迷惑メールSPAM
ゴミ箱TRASH
送信済みSENT
チャットCHAT
そのほかの自分で作ったラベルは Label_[ほにゃらら] と番号が付くIDになっているので、そういったラベルで検索したい場合はあらかじめラベルIDを調べておくか、ラベル名からIDを取得する関数でも作るしかなさそうです。

ラベル名からラベルIDを取得する関数

//ラベル一覧からラベルIDを探す関数
async function getLabelId(auth, label_name) {
	const gmail = google.gmail({version:'v1', auth});
	const res = await gmail.users.labels.list({userId: 'me'});
	for (let i = 0; i < res.data.labels.length; i++) {
		if (res.data.labels[i].name == label_name) {
			return res.data.labels[i].id;
		}
	}
	return '';
}

なんだかひっじょ~~~に無駄なことしている気もしますが、うちのプログラムではどうしてもラベル名で指定したい部分があったので、こういう getLabelId という関数を作って、ラベル一覧からラベル名を検索してIDを取得するようにしています。

解説:メールの件名を取得

const msg = await gmail.users.messages.get({userId:'me',id:list.data.messages[i].id});
//サブジェクトをキーに出来ないっぽいのでヘッダーから探す(ホントにこれであってる…?)
for (let j = 0; j < msg.data.payload.headers.length; j++) {
	if (msg.data.payload.headers[j].name.toLowerCase() == 'subject') {
		console.log(list.data.messages[i].id + ':' + msg.data.payload.headers[j].value);
		break;
	}
}

思わずコメント欄でも不安を書いてしまいましたが、メールの件名取得でもひっじょ~~~に無駄なことをしている気がしてなりません。

IMAPならメールの一覧取得時に件名も入ってくるので一覧表示は容易でした。

でも、Gmail APIのusers.messages.listの中には件名はなく、メッセージIDのみなんですよ。

だから、そのメッセージIDを使ってusers.messages.getでデータを取得するのですが、この中には素のヘッダーが配列で格納されています。

もっと効率的な取得方法があるのではないか、と疑問に思いつつも愚直にヘッダー配列を全件見ながらsubjectを探しています。ちなみにメールの送信元によって subject だったり Subject だったり、大文字小文字が混在するので小文字に変換してから探しています。

……………………ホントにこんな面倒なことやらなきゃいけないの?

解説:メールの本文を取得

メール本文の部分は必ずbase64エンコードされているので、これを変換する必要があります。とはいえ、関数がやってくれるのでここは1行で済みます。

const body = new Buffer.from(msg.data.payload.body.data, 'base64').toString(); //Base64変換

まとめ

無駄に長くなってしまった気がしますが、とりあえず、このコードでうちの環境ではうまく動作しています。

PHP+IMAPでGmailを読んでいたときに比べると体感できるほど遅くなったので、とても無駄な動きをしている気がしてなりませんが、速度が重要な部分ではないので、まぁ…いい…の…かなぁ…?

セキュリティの部分が面倒くさくなるのは仕方ないと思うんですよ。ある程度は。

でも、Gmail APIは…なんだろう…なんでこんな仕様なんだろうか。IMAPよりだいぶ使いづらくないですかね…?

う~~~ん、なんか使い方が間違っている気もします。

とはいえ、今のところはこの程度の知識しかないので、備忘録として残しておきます。

もしレベルアップできたらもう少しまともなサンプルを掲載しますね。

コメント

タイトルとURLをコピーしました