arrow-righthamburgerlogo-marksocial-facebooksocial-githubsocial-twitter
2018.02.21

Google公式ライブラリを利用してNode.jsからGmailの送受信をしてみよう

のびすけ

Node.js Magazine
   このエントリーをはてなブックマークに追加  

こんにちは、のびすけです。

Node.jsでGmailを制御する方法は、過去に色々な記事が出ているのですが、全体的に記事が古く、Gmail API Node.js Quickstartのチュートリアルも割とわかりにくいなぁという印象です。英語しかないし。

また、勝手ライブラリも多いのですがメンテナンスされてなく利用していく際の不安も多いです。

ということで、今日はNode.jsでGmailを使う方法の紹介です。

以前書いたSpreadSheets記事も同様のGoogle APIライブラリを利用しているので基本的な手順は同じです。

Gmail APIの有効化とクライアントシークレットの取得

こちらのリンクからウィザード開始します。ログインしているGoogleアカウントが利用するGmailのアカウントになるので複数のGoogleやG Suiteのアカウントを持っている人は注意しましょう。

チェックをして進みます。

少し待つとAPIが有効になるので進みます。

認証情報に進みますが……

例によってこの画面は何もせずにキャンセルボタンを押しましょう。

このダッシュボード画面になりますが、OAuth同意画面のタブを選択します。

ユーザーに表示するサービス名の箇所に任意の名前を入力して、保存を押して進みます。

認証情報タブに戻り、認証情報を作成 -> OAuthクライアントIDを選択します。

クライアントID作成画面ではその他を選択し、任意の名前を入力します。

作成すると最初のダッシュボード画面に戻りますが、作成したクライアントIDが表示されています。ここの右下のダウンロードボタンを押すとクライアントシークレットが保存されたJSONファイルがダウンロードされます。

任意の場所にclient_secret.jsonという名前にリネームして保存しましょう。

これで、 クライアントシークレットの保存が出来ました。

Node.jsプロジェクトの準備

Node.jsはv9.5.0です。

mkdir gmail
cd gmail
npm init -y

先ほどのclient_secret.jsonをこのディレクトリに移動します。

ls

package.json client_secret.json

ライブラリのインストールをすれば準備完了です。google-auth-libraryのバージョンは1.3.1になります。

$ npm i googleapis google-auth-library --save

これで準備完了です。

アクセストークンの保存

本家とは少し違うやり方です。

本家のコードだとアクセストークンの作成&保存作業と実際のAPI呼び出しが一緒になったコードですがトークンの作成&保存は最初のみ行えば良いので手順を分けて紹介します。

getAndStoreToken.jsを作成します。

前回の記事のコードとほぼ同様ですが、APIライブラリのバージョンの違いにより、呼び出し方が少し異なります。注意しましょう。

//getAndStoreToken.js
'use strict';

const fs = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});
const {promisify} = require('util');

const {google} = require('googleapis');
const {OAuth2Client} = require('google-auth-library');

//promisifyでプロミス化
const readFileAsync = promisify(fs.readFile);
const writeFileAsync = promisify(fs.writeFile);
const rlQuestionAsync = promisify(rl.question);

const SCOPES = ['https://www.googleapis.com/auth/gmail.send','https://www.googleapis.com/auth/gmail.readonly'];
const TOKEN_DIR = __dirname;
const TOKEN_PATH = TOKEN_DIR+'/gmail-nodejs-quickstart.json';

const main = async () => {
    const content = await readFileAsync(__dirname+'/client_secret.json');
    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);

    //get new token
    const authUrl = oauth2Client.generateAuthUrl({
        access_type: 'offline',
        scope: SCOPES
    });

    console.log('Authorize this app by visiting this url: ', authUrl);

    rl.question('Enter the code from that page here: ', (code) => {
        rl.close();

        oauth2Client.getToken(code, async (err, token) => {
            if (err) {
                console.log('Error while trying to retrieve access token', err);
                return;
            }

            oauth2Client.credentials = token;

            try {
                fs.mkdirSync(TOKEN_DIR);
            } catch (err) {
                if (err.code != 'EEXIST') throw err;
            }

            await writeFileAsync(TOKEN_PATH, JSON.stringify(token));
            console.log('Token stored to ' + TOKEN_PATH);
        });
    });
};

main();

ここで'https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/gmail.send'の二つのスコープを指定していますが、表示用のgmail.readonlyとメール送信用のgmail.sendとなっています。表示だけしたい場合や送信だけしたい場合などで使い分けましょう。スコープについては Choose Auth Scopesで細かく確認できます。

では、このプログラムを実行しましょう。

node getAndStoreToken.js

https://accounts.google.com/~で始まるURLが表示されるのでこれをコピーしてブラウザで開きます。

アカウント選択画面になります。 複数Googleアカウントを持っている人は、今回アクセスしようとしているアカウントを選択します。

許可で進みます。

認証用のコードが発行されるので、ターミナルのEnter the code from that page here:と書いてある箇所にコピぺしてEnterで進みます。

Token stored to /Users/path/to/myapp/gmail-nodejs-quickstart.json

などの表示が出て、gmail-nodejs-quickstart.jsonが保存されます。

これでアクセストークンの保存が完了しました。

4. いよいよGmailにアクセス

いよいよGmailにアクセスします。

ラベル一覧の取得

本家チュートリアルを参考に、ラベルの一覧を取得します。 getLabels.jsを作成します。

ここから先に出てくるuserId: 'me'meは指定されている文字列らしく、自分のメアドやアカウント名をあえて指定する必要はなくそのままmeと書いておけば問題なさそうです。

//getLabels.js
'use strict';

const fs = require('fs');
const {promisify} = require('util');
const {google} = require('googleapis');
const {OAuth2Client} = require('google-auth-library');
const gmail = google.gmail('v1');

//promisifyでプロミス化
const readFileAsync = promisify(fs.readFile);
const gmailListLabesAsync = promisify(gmail.users.labels.list); //Gmailのラベル一覧

const TOKEN_DIR = __dirname;
const TOKEN_PATH = TOKEN_DIR + '/gmail-nodejs-quickstart.json'; //アクセストークンのファイルを指定

const main = async () => {
    //クレデンシャル情報の取得
    const content = await readFileAsync(__dirname+'/client_secret.json'); //クライアントシークレットのファイルを指定
    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経由でシートにアクセス
    const response = await gmailListLabesAsync({
        auth: oauth2Client,
        userId: 'me',
    });
    //結果を表示
    console.log(response.data);
};

main();

Async/Awaitで利用できるようにconst gmailListLabesAsync = promisify(gmail.users.labels.list);の箇所でPromisifyを利用してプロミス化しています。

実行するとラベル一覧が取得できます。

node getLabels.js

{ labels:
   [ { id: 'Label_125',
       name: '研修/青山学院大学',
       messageListVisibility: 'show',
       labelListVisibility: 'labelShow',
       type: 'user',
       color: [Object] },
     { id: 'Label_40',
       name: '取引/養老乃瀧',
       messageListVisibility: 'show',
       labelListVisibility: 'labelShow',
       type: 'user',
       color: [Object] },

       ・
       ・
       ・

会社のメールの内容なので出せるところだけ笑

メールの内容を取得

メールの内容はUsers.messages: listUsers.messages: getを利用します。

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

必要な箇所だけ抜粋しますが、gmail.users.messages.list()gmail.users.messages.get()というメソッドがあるので利用します。 Async/Awaitで利用できるように、先ほど同様Promisifyします。

const gmailGetMessagesAsync = promisify(gmail.users.messages.get);
const gmailListMessagesAsync = promisify(gmail.users.messages.list);

メッセージリストを取得し、メッセージごとのIDを取得します。メッセージIDをもとにメッセージ本文を取得。 最後に本文はBase64変換されてるのでデコードして表示します。

省略

    //メッセージリストの取得
    let res = await gmailListMessagesAsync({
        auth: oauth2Client,
        userId: 'me'
    });
    const newestMessageId = res.messages[0].id; //最新のメッセージID

    //メッセージの取得
    res = await gmailGetMessagesAsync({
        auth: oauth2Client,
        userId: 'me',
        id: newestMessageId
    });
    //結果を表示
    const base64mailBody = res.payload.parts[0].body.data; //parts[0]がテキスト、parts[1]がHTMLメールっぽい(?)
    const mailBody = new Buffer(base64mailBody, 'base64').toString(); //メール本文はBase64になってるので変換
    console.log(mailBody);//やっとメール本文が表示される

省略

メールの送信

送信が割と大変でした。

Users.messages: sendを利用します。

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

gmail.users.messages.send()のメソッドを利用します。 先ほどと同様にPromisifyします。

const gmailSendMessagesAsync = promisify(gmail.users.messages.send);

次にメール送信時の処理です。

省略

    const makeBody = (params) => {
        params.subject = new Buffer(params.subject).toString("base64"); //日本語対応

        const str = [
            `Content-Type: text/plain; charset=\"UTF-8\"\n`,
            `MIME-Version: 1.0\n`,
            `Content-Transfer-Encoding: 7bit\n`,
            `to: ${params.to} \n`,
            `from: ${params.from} \n`,
            `subject: =?UTF-8?B?${params.subject}?= \n\n`,
            params.message
        ].join('');
        return new Buffer(str).toString("base64").replace(/\+/g, '-').replace(/\//g, '_');
    }

    const messageBody = `
        XXXX 様

        商品のご購入、誠にありがとうございます。
        ・Nefry BT x 1
        の商品を本日発送しましたのでご連絡差し上げます。

        こちらから配送状況をご確認頂けます。
        (このメール送信の時点では反映されていない可能性もあります)

        https://trackings.post.japanpost.jp/xxxxxxxxxx`;

    const raw = makeBody({
        to: '相手のメールアドレス',
        from: '送信者のメールアドレス',
        subject: '件名(日本語可)',
        message: messageBody
    });

    const res = await gmailSendMessagesAsync({
        auth: oauth2Client,
        userId: 'me',
        resource: {
          raw: raw
        }
    });
    console.log(res.data);

省略

sendMail.jsなどを作成し、この内容で実行するとメールが送信できます。

node sendMail.js

{ id: 'xxxxxxxxxxxxxxxxx',
  threadId: 'xxxxxxxxxxxxxxxx',
  labelIds: [ 'SENT' ] }

Gmail側で受信確認をすると、しっかりと送られてきています。

また、暗号化もされているようで、

送受信時のメールの暗号化 - Gmailヘルプ

Node.jsのメール送信ライブラリはいくつかあって、いくつか試したときに暗号化されてない場合も多かった印象です。暗号化無しだと、この赤いアラートっぽいメール表示になります。

今回のやり方はGoogle公式のライブラリで、特に意識せずに暗号化も行われているので暗号化に対するアラートなども受け取り手には表示されないので迷惑メールなどに振り分けられてしまう心配もたぶん少ないのでは。 (Gmailの迷惑メールアルゴリズムは色々な要素がありそうなので断言は出来ないですが)

所感

実装途中の感想ですが、メール送信が割と苦労した印象です。

メール送信のNode.jsサンプルが無かったので、JavaScript Sampleを見ると、送信パラメータにemail RFC 5322 formatted String.という記述があってRFCのフォーマットを確認する必要がありました。

この手のライブラリだと

const params = {
    to: '',
    from: '',
    subject: '',
    body: ''
}

みたいな形式のObjectを突っ込めばいい感じにやってくれる印象があったので……苦笑

Stack Over FlowのGmail API for sending mails in Node.jsがかなり参考になりました。

また、日本語でのSubject送信に=?文字コード?メソッド?Base64変換した文字列?=という形式で渡すなどはメールのSubjectヘッダのエンコードの記事が参考になりました。

とはいえ、問題なくメール送信出来たので、この記事を参考に使ってみてください。

それでは!

補足: 本家のチュートリアルのエラー

今回紹介した手順では問題ないですが、もともとのチュートリアルのコードだと以下の二つのエラーが出ました。(2018年2月時点)

google.gmail is not a function

google.gmail() is giving an error TypeError: google.gmail is not a function?

ってエラーが出ました。

Stack Over Flowのgoogle.gmail() is giving an error TypeError: google.gmail is not a function?を参考に

var google = require('googleapis');

これを↓に変更するとエラー解消されます。

var {google} = require('googleapis');

GoogleAuth is not a constructor

こちらはIssueがあがっていてGoogleAuth is not a constructor #251を見ると、

呼び出し方が変更されてて、以下の修正が必要です。

const { GoogleAuth } = require('google-auth-library');

const { OAuth2Client } = require('google-auth-library');

また、今の変更に伴いOAuthの処理も変更が必要です。

const auth = new googleAuth();
const oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);

//const auth = new googleAuth();
const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUrl);

これで問題なく利用できるようになりました。

   このエントリーをはてなブックマークに追加