GoogleAppsScript
gas
gmail
GSuite

Google Apps Script で Gmail Add-on を作ってみよう

先日 Tokyo GAS のイベントに行ってきた @wezardnet です。ちなみに東●ガス会社とはまったく関係ありませんw
久々に社外の勉強会に参加して来たのだけど、自社内(ウチだけかも知れないけど)に閉じこもってると、ホント世の中の技術から取り残されていくことを改めて痛感しました(ヤバイね):fearful:

1. Gmail Add-on って?

サードパーティー製のアプリをアドオンとして Gmail に組み込みできる機能で 2017 年の 10 月ごろに発表されたのですが、日本ではあまり浸透しなかった感がありますww

Gmail アドオンでは Gmail のサイドバーを独自にカスタマイズして機能を追加することができます。また Android の Gmail アプリからも利用することができるので機能次第では重宝しそうです。尚 iOS の Gmail アプリは現在のところ未対応なので、今後のアップデートに期待したいですね!

2018/04/09 追記
タグフィードを見ていたら、一足先に試された方が Qiita に記事をアップしてましたので、こちらも参考にすると良いです。日本ではまだまだ情報が少ないです、、、

2. 作ったモノ

Tokyo GAS で Gmail アドオンを Google Apps Script(GAS) で作れるってことを知り、たまたま社内に相談案件があって、アドオンの仕組みがマッチしそうだったので試してみることにしました。

表示中のメール本文と、(あれば)添付ファイルを Google ドライブに格納するアドオンを作ってみました♪
↓な感じで動きます。

sample.mov.gif

ドライブ エクスポートを実行すると、↓のように Google ドライブの「Gmail Export」というフォルダに PDF 化したメール本文と添付ファイルが格納されます。メール本文中の画像も添付ファイルとして扱われます:bulb:

image02.png

PDF 化したメール本文は↓な感じです。添付ファイルはリスト形式でドライブ内の実ファイルにリンクが張られます。

image03.png

3. スクリプトの解説

3.1. 新規スクリプト(GAS プロジェクト)を作る

まずは script.google.com から新規の GAS プロジェクトを作ります。超久々に触るのですが GAS は開発環境とかの準備が不要だったりという点で手間がかからずサクッと開発できるので好きです:grinning:

image00.png

3.2. マニフェストを書く

次に、これもいつの間にか GAS に追加された機能なのですが、マニフェストを書きます。スクリプトエディタのメニューから[表示]ー[マニフェスト ファイルを表示]で作成します。このマニフェストにアドオンがユーザーリソースの何にアクセスするかの OAuth スコープを記述します。

マニフェストの説明については、「Google Apps Scriptに追加されたマニフェストについて」に詳しく解説されているので割愛することにします。

appsscript.json
{
    "oauthScopes": [
        "https://www.googleapis.com/auth/gmail.addons.execute", 
        "https://www.googleapis.com/auth/gmail.readonly", 
        "https://www.googleapis.com/auth/gmail.modify", 
        "https://mail.google.com/", 
        "https://www.googleapis.com/auth/drive", 
        "https://www.googleapis.com/auth/script.external_request", 
        "https://www.googleapis.com/auth/userinfo.email"
    ],
    "gmail": {
        "name": "Gmail Add-on Drive Export", 
        "logoUrl": "{アドオンのアイコンが配置された URL}", 
        "contextualTriggers": [
            {
                "unconditional": {}, 
                "onTriggerFunction": "buildAddOn"
            }
        ], 
        "openLinkUrlPrefixes": [
            "https://mail.google.com/"
        ],
        "primaryColor": "#4285F4", 
        "secondaryColor": "#4285F4", 
        "version": "TRUSTED_TESTER_V1"
    }
}

logoUrl で指定したアイコンは Gmail サイドバーの↓に表示されます。アイコンなどの静的ファイルは Google Cloud Storage(GCS) などに配置しておくとキャッシュも効いて良いですょ♪

image.png

3.3. メインスクリプトを書く

はじめにアドオンの UI を構築するコンテキストトリガー関数を実装します。この関数はマニフェストの onTriggerFunction フィールドで指定します。このケースでは buildAddOn 関数がそれになります。

Gmail アドオンはサイドバーペインとして Gmail UI に表示されます。モバイル(現在のところは Android 版 Gmail アプリのみ対応)ではメニューを介して別のアクティビティ ウィンドウとして表示されます。UI 部分はガイドラインが公式ドキュメントにあるので目を通しておくと良いでしょう。

それでは具体的に中身のコードについて解説します。GAS においては Card サービスを使用して、アドオンの外観と動作を定義するコードを記述していきます。今回は以下のような[Drive Export]ボタンと、メールの送信者、添付ファイルの個数を表示するだけの単純な UI になります。

sample.png

main.gs
/**
 * マニフェスト 'onTriggerFunction' フィールドで指定されたアドオン起動トリガー
 * 
 * @param {Object} イベントオブジェクト
 * @return {Card} カードオブジェクト
 */
function buildAddOn(e){
    var accessToken = e.messageMetadata.accessToken;
    GmailApp.setCurrentMessageAccessToken(accessToken);

    var messageId = e.messageMetadata.messageId;
    var message = GmailApp.getMessageById(messageId);
    var subject = message.getSubject();                 // 件名
    var from = message.getFrom();                       // 送信元
    var attachments = message.getAttachments();         // 添付ファイル

    // UI を作る
    var buttonSet = CardService.newButtonSet();
    var exportButton = CardService.newTextButton()
        .setText('Drive Export')
        .setOnClickAction(CardService.newAction()
            .setFunctionName('driveExport')
            .setParameters({'messageId': messageId}));

    var card = CardService.newCardBuilder()
        .setHeader(CardService.newCardHeader()
            .setTitle(subject))
        .addSection(CardService.newCardSection()
            .addWidget(CardService.newKeyValue()
                .setTopLabel('送信元')
                .setContent(from))
            .addWidget(CardService.newKeyValue()
                .setTopLabel('添付ファイル')
                .setContent(attachments.length + ' 個'))
            .addWidget(exportButton))
        .build();

    return card;
}

/**
 * 指定されたメッセージ識別子に対応するメール本文と添付ファイルを Google ドライブに格納する
 * 
 * @param {Object} イベントオブジェクト
 */
function driveExport(e){
    var messageId = e.parameters['messageId'];
    var message = GmailApp.getMessageById(messageId);

    var driveFolder = 'Gmail Export';
    var folders = DriveApp.getFoldersByName(driveFolder);
    var folder = folders.hasNext() ? folders.next() : DriveApp.createFolder(driveFolder);

    var html = '';
    html += 'From: ' + message.getFrom() + '<br />';
    html += 'To: ' + message.getTo() + '<br />';
    html += 'Date: ' + message.getDate() + '<br />';
    html += 'Subject: ' + message.getSubject() + '<br />';
    html += '<hr />';
    html += message.getBody().replace(/<img[^>]*>/g, '');
    html += '<hr />';

    var attachments = [];
    var atts = message.getAttachments();
    for ( var i = 0; i < atts.length; i++ ) attachments.push(atts[i]);

    if ( attachments.length > 0 ) {
        var footer = '<strong>添付ファイル</strong><ul>';
        for ( var i = 0; i < attachments.length; i++ ) {
            var file = folder.createFile(attachments[i]);
            footer += '<li><a href="' + file.getUrl() + '">' + file.getName() + '</a></li>';
        }
        html += footer + '</ul>';
    }

    var tempFile = DriveApp.createFile('temp.html', html, 'text/html');
    folder.createFile(tempFile.getAs('application/pdf')).setName(message.getSubject() + '.pdf');
//  tempFile.setTrashed(true);
    deleteFile(tempFile.getId());
}

/**
 * 指定したファイルを Google ドライブから完全削除する
 * 
 * @param {String} ファイル ID
 */
function deleteFile(fileId){
    var token = ScriptApp.getOAuthToken();
    var response = UrlFetchApp.fetch(Utilities.formatString('https://www.googleapis.com/drive/v3/files/%s', fileId), {
        method: 'delete',
        headers: {
            'Authorization': 'Bearer ' + token
        },
        muteHttpExceptions: true
    });
}

次に[Drive Export]ボタン押下時の処理を書きます。この例では driveExport 関数になります。引数のイベントオブジェクトでメッセージ ID を受け取り GAS の Gmail サービスを使ってメールの中身にアクセスします。
メール本文を PDF 化したり、添付ファイルをドライブに保存する仕組みは、ちょうどイイ感じのスクリプトをネットで見つけることができたので、それをパクり参考にしました (・ω<)

このスクリプトでは、メール本文を PDF に変換するため、一時的に html ファイルを作って、最後にゴミ箱:wastebasket:に入れていますが、ゴミ箱が溢れちゃうとイヤなので、完全に削除させるため deleteFile 関数を加えて改良しました。GAS の組み込みにはファイルを完全削除する機能はないので Drive API を直接叩いて削除します。

4. アドオンをデプロイする

作成したアドオンをデプロイします。スクリプトエディタのメニューから[公開]ー[マニフェストから配置]を選択すると、以下のような画面が表示されます。
Latest Version (最新バージョン)の「Get ID」からデプロイメント ID を表示して控えておきます。

image.png

5. Gmail にアドオンをインストする

Gamil アドオンは Gamil もしくは G Suite Marketplace 経由でインストールすることができます。後者は以下のようなマーケットプレイスで、すべての Gmail ユーザーに公開することができますが Google に申請や審査が必要だったりします。グーグルへの申請フォームはこちらになりますが、まだまだ数は少ないですねー

余談ですが、マーケットプレイスへの公開は過去に Google Sheets 向けの電子印鑑アドオン(DigitalStamp4Sheet)を作ったことがありますが、審査を通すのに苦労した経験があります:disappointed_relieved:

MarketPlace.png

今回は試作品で、一部の人のみなので、未公開アドオンとして Gmail にインストールします。Gmail の設定画面で[アドオン]タブから「アドオン」の ご利用中のアカウントで、デベロッパー アドオンを有効にします にチェックを入れます。

image22.png

次に「デベロッパー アドオン」で先ほど控えたデプロイメント ID を入力してインストールします。

image12.png

インストールが成功すると、「インストール済みのデベロッパー アドオン」にインストールした自分のアドオンが表示されるようになります。

未公開アドオンの注意点として G Suite 組織のユーザーは、同じ組織内のユーザーが作成した未公開のアドオンのみをインストールして実行できますが GAS プロジェクトそのものをドメイン全体で共有(閲覧権限でOK)しておかないと、次のように 無効なアドオン と認識され、自分以外の他の人は利用することができません:confounded:

image00.png

ダッシュボードで次のように表示されていれば、ドメイン内で利用可能です。

image.png

6. アドオンのアクセスを承認する(初回のみ)

インストール後の初回は、アドオンがユーザーの Gmail にアクセスしてもよいかを許可するための アクセスを承認 が必要になります。

image02.png

承認が済めばアドオンはいつでも使えるようになります:thumbsup:

7. おわりに、、、

勉強会やイベントでセッションを聴くだけでは技術は身に付きません。自分でドキュメント読んで、コード書いて、わからないことはネットで調べてみたりして、手を動かして実体験することが大切です!

Gmail アドオンにはクイックスタートというチュートリアルが用意されています。英語でハードルが高いと感じられる方は以下の記事を併せて読むと良いです:point_up:

全国の GAS ユーザーのみなさん、Gmail だけでなく Sheets, Docs, Forms, Slides で日本発のアドオンを増やして G Suite をより便利にしていけたら良いですね!:stuck_out_tongue_closed_eyes: