Google Workspace アドオンでメールの実用性を高める

1. 概要

この Codelab では、Google Apps Script を使用して、Gmail の Google Workspace アドオンを作成します。このアドオンを使用すると、ユーザーはメールの領収書データを Gmail 内のスプレッドシートに直接追加できます。メールで領収書を受け取ったユーザーがアドオンを開くと、メールから関連する経費情報が自動的に取得されます。ユーザーは経費情報を編集し、送信して Google スプレッドシートに経費を記録できます。

学習内容

  • Google Apps Script を使用して Gmail 用の Google Workspace アドオンを作成する
  • Google Apps Script でメールを解析する
  • Google Apps Script を介して Google スプレッドシートを操作する
  • Google Apps Script のプロパティ サービスを使用してユーザー値を保存する

必要なもの

  • インターネット アクセスとウェブブラウザ
  • Google アカウント
  • Gmail のメッセージ(メールの領収書など)

2. サンプルコードを取得する

この Codelab の作業を進めるにあたって、記述するコードの動作するバージョンを参照すると役立つ場合があります。GitHub リポジトリには、参考として使用できるサンプルコードが含まれています。

サンプルコードを取得するには、コマンドラインから次のコマンドを実行します。

git clone https://github.com/googleworkspace/gmail-add-on-codelab.git

3. 基本的なアドオンを作成する

まず、メールの横に経費フォームを表示するアドオンのシンプルなバージョンのコードを記述します。

まず、新しい Apps Script プロジェクトを作成し、そのマニフェスト ファイルを開きます。

  1. script.google.com に移動します。ここから、Apps Script プロジェクトの作成、管理、モニタリングを行うことができます。
  2. 新しいプロジェクトを作成するには、左上の [新しいプロジェクト] をクリックします。新しいプロジェクトが開き、Code.gs という名前のデフォルト ファイルが表示されます。Code.gs は後で使用するので、今はそのままにしておきます。
  3. [無題のプロジェクト] をクリックし、プロジェクトに「Expense It!」という名前を付けて、[名前を変更] をクリックします。
  4. 左側の [プロジェクト設定 ] プロジェクトの設定 をクリックします。
  5. [「appscript.json」マニフェスト ファイルをエディタで表示する] チェックボックスをオンにします。
  6. [エディタ] 編集者 をクリックします。
  7. マニフェスト ファイルを開くには、左側の appscript.json をクリックします。

appscript.json で、アドオンに関連付けられたメタデータ(名前や必要な権限など)を指定します。appsscript.json の内容を次の構成設定に置き換えます。

{
  "timeZone": "GMT",
  "oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute"
  ],
  "gmail": {
    "name": "Expense It!",
    "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/receipt_black_24dp.png",
    "contextualTriggers": [{
      "unconditional": {
      },
      "onTriggerFunction": "getContextualAddOn"
    }],
    "primaryColor": "#41f470",
    "secondaryColor": "#94f441"
  }
}

マニフェストの contextualTriggers と呼ばれる部分に特に注意してください。マニフェストのこの部分では、アドオンが最初に有効になったときに呼び出すユーザー定義関数を指定します。この場合、getContextualAddOn が呼び出され、開いているメールの詳細が取得され、ユーザーに表示するカードのセットが返されます。

getContextualAddOn 関数を作成する手順は次のとおりです。

  1. 左側の Code.gs にポインタを合わせ、メニュー アイコン その他のメニュー > [名前を変更] をクリックします。
  2. GetContextualAddOn」と入力して Enter キーを押します。Apps Script ではファイル名に .gs が自動的に付加されるため、ファイル拡張子を入力する必要はありません。GetContextualAddOn.gs と入力すると、Apps Script によってファイルに GetContextualAddOn.gs.gs という名前が付けられます。
  3. GetContextualAddOn.gs で、デフォルトのコードを getContextualAddOn 関数に置き換えます。
/**
 * Returns the contextual add-on data that should be rendered for
 * the current e-mail thread. This function satisfies the requirements of
 * an 'onTriggerFunction' and is specified in the add-on's manifest.
 *
 * @param {Object} event Event containing the message ID and other context.
 * @returns {Card[]}
 */
function getContextualAddOn(event) {
  var card = CardService.newCardBuilder();
  card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense'));

  var section = CardService.newCardSection();
  section.addWidget(CardService.newTextInput()
    .setFieldName('Date')
    .setTitle('Date'));
  section.addWidget(CardService.newTextInput()
    .setFieldName('Amount')
    .setTitle('Amount'));
  section.addWidget(CardService.newTextInput()
    .setFieldName('Description')
    .setTitle('Description'));
  section.addWidget(CardService.newTextInput()
    .setFieldName('Spreadsheet URL')
    .setTitle('Spreadsheet URL'));

  card.addSection(section);

  return [card.build()];
}

すべての Google Workspace アドオンのユーザー インターフェースは、1 つ以上のセクションに分割されたカードで構成されています。各セクションには、ユーザーから情報を表示して取得できるウィジェットが含まれています。getContextualAddOn 関数は、メールで見つかった費用の詳細を取得する 1 つのカードを作成します。カードには、関連データのテキスト入力フィールドを含むセクションが 1 つあります。この関数は、アドオンのカードの配列を返します。この場合、返される配列には 1 つのカードのみが含まれます。

Expense It! アドオンをデプロイする前に、Apps Script プロジェクトで承認、高度なサービス、その他の詳細を管理するために使用する Google Cloud Platform(GCP)プロジェクトが必要です。詳細については、Google Cloud Platform プロジェクトをご覧ください。

アドオンをデプロイして実行するには、次の手順に沿って操作します。

  1. GCP プロジェクトを開き、プロジェクト番号をコピーします。
  2. Apps Script プロジェクトの左側にある [プロジェクトの設定 ] プロジェクトの設定 をクリックします。
  3. [Google Cloud Platform(GCP)プロジェクト] で、[プロジェクトを変更] をクリックします。
  4. GCP プロジェクトのプロジェクト番号を入力し、[プロジェクトを設定] をクリックします。
  5. [デプロイ] > [デプロイをテスト] をクリックします。
  6. デプロイ タイプが [Google Workspace アドオン] であることを確認します。必要に応じて、ダイアログの上部にある [デプロイタイプを有効にする] デプロイタイプを有効にする をクリックし、デプロイタイプとして [Google Workspace アドオン] を選択します。
  7. [アプリケーション: Gmail] の横にある [インストール] をクリックします。
  8. [完了] をクリックします。

これで、Gmail の受信トレイにアドオンが表示されます。

  1. パソコンで Gmail を開きます。
  2. 右側のサイドパネルに、Expense It! Expense It! の領収書アイコン アドオンが表示されます。必要に応じて、[その他のアドオン] その他のアドオン をクリックして確認します。
  3. メールを開きます(できれば、費用が記載された領収書)。
  4. アドオンを開くには、右側のサイドパネルで [Expense It!] をクリックします。Expense It! の領収書アイコン
  5. [Authorize Access] をクリックして、Expense It! に Google アカウントへのアクセス権を付与し、表示される手順に沿って操作します。

アドオンは、開いている Gmail メッセージの横にシンプルなフォームを表示します。まだ何も行いませんが、次のセクションで機能を構築します。

このラボを進めながらアドオンの更新を確認するには、コードを保存して Gmail を更新するだけで済みます。追加のデプロイは必要ありません。

4. メール メッセージにアクセスする

メールのコンテンツを取得するコードを追加し、コードをモジュール化して整理します。

[ファイル] の横にある追加アイコン ファイルを追加 > [スクリプト] をクリックし、Cards という名前のファイルを作成します。Helpers という名前の 2 つ目のスクリプト ファイルを作成します。Cards.gs はカードを作成し、Helpers.gs の関数を使用して、メールの内容に基づいてフォームのフィールドに入力します。

Cards.gs のデフォルトのコードを次のコードに置き換えます。

var FIELDNAMES = ['Date', 'Amount', 'Description', 'Spreadsheet URL'];

/**
 * Creates the main card users see with form inputs to log expenses.
 * Form can be prefilled with values.
 *
 * @param {String[]} opt_prefills Default values for each input field.
 * @param {String} opt_status Optional status displayed at top of card.
 * @returns {Card}
 */
function createExpensesCard(opt_prefills, opt_status) {
  var card = CardService.newCardBuilder();
  card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense'));
  
  if (opt_status) {
    if (opt_status.indexOf('Error: ') == 0) {
      opt_status = '<font color=\'#FF0000\'>' + opt_status + '</font>';
    } else {
      opt_status = '<font color=\'#228B22\'>' + opt_status + '</font>';
    }
    var statusSection = CardService.newCardSection();
    statusSection.addWidget(CardService.newTextParagraph()
      .setText('<b>' + opt_status + '</b>'));
    card.addSection(statusSection);
  }
  
  var formSection = createFormSection(CardService.newCardSection(),
                                      FIELDNAMES, opt_prefills);
  card.addSection(formSection);
  
  return card;
}

/**
 * Creates form section to be displayed on card.
 *
 * @param {CardSection} section The card section to which form items are added.
 * @param {String[]} inputNames Names of titles for each input field.
 * @param {String[]} opt_prefills Default values for each input field.
 * @returns {CardSection}
 */
function createFormSection(section, inputNames, opt_prefills) {
  for (var i = 0; i < inputNames.length; i++) {
    var widget = CardService.newTextInput()
      .setFieldName(inputNames[i])
      .setTitle(inputNames[i]);
    if (opt_prefills && opt_prefills[i]) {
      widget.setValue(opt_prefills[i]);
    }
    section.addWidget(widget);
  }
  return section;
}

createExpensesCard 関数は、フォームに事前入力する値の配列をオプションの引数として受け取ります。この関数は、オプションのステータス メッセージを表示できます。ステータスが「Error:」で始まる場合は赤色、それ以外の場合は緑色で表示されます。各フィールドをフォームに手動で追加する代わりに、createFormSection というヘルパー関数がテキスト入力ウィジェットを作成するプロセスをループ処理し、setValue で各デフォルト値を設定してから、ウィジェットをカードのそれぞれのセクションに追加します。

次に、Helpers.gs のデフォルトのコードを次のコードに置き換えます。

/**
 * Finds largest dollar amount from email body.
 * Returns null if no dollar amount is found.
 *
 * @param {Message} message An email message.
 * @returns {String}
 */
function getLargestAmount(message) {
  return 'TODO';
}

/**
 * Determines date the email was received.
 *
 * @param {Message} message An email message.
 * @returns {String}
 */
function getReceivedDate(message) {
  return 'TODO';
}

/**
 * Determines expense description by joining sender name and message subject.
 *
 * @param {Message} message An email message.
 * @returns {String}
 */
function getExpenseDescription(message) {
  return 'TODO';
}

/**
 * Determines most recent spreadsheet URL.
 * Returns null if no URL was previously submitted.
 *
 * @returns {String}
 */
function getSheetUrl() {
  return 'TODO';
}

Helpers.gs の関数は、フォームの事前入力された値を決定するために getContextualAddon によって呼び出されます。現時点では、これらの関数は文字列「TODO」のみを返します。これは、後のステップで入力候補のロジックを実装するためです。

次に、Cards.gsHelpers.gs のコードを活用するように GetContextualAddon.gs のコードを更新します。GetContextualAddon.gs 内のコードを次のコードで置き換えます。

/**
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Returns the contextual add-on data that should be rendered for
 * the current e-mail thread. This function satisfies the requirements of
 * an 'onTriggerFunction' and is specified in the add-on's manifest.
 *
 * @param {Object} event Event containing the message ID and other context.
 * @returns {Card[]}
 */
function getContextualAddOn(event) {
  var message = getCurrentMessage(event);
  var prefills = [getReceivedDate(message),
                  getLargestAmount(message),
                  getExpenseDescription(message),
                  getSheetUrl()];
  var card = createExpensesCard(prefills);

  return [card.build()];
}

/**
 * Retrieves the current message given an action event object.
 * @param {Event} event Action event object
 * @return {Message}
 */
function getCurrentMessage(event) {
  var accessToken = event.messageMetadata.accessToken;
  var messageId = event.messageMetadata.messageId;
  GmailApp.setCurrentMessageAccessToken(accessToken);
  return GmailApp.getMessageById(messageId);
}

Gmail から提供されたイベントを使用して、ユーザーが現在開いているメッセージを読み取る新しい getCurrentMessage 関数に注目してください。この関数を機能させるには、Gmail メッセージへの読み取り専用アクセスを許可する追加のスコープをスクリプト マニフェストに追加します。

appscript.json で、https://www.googleapis.com/auth/gmail.addons.current.message.readonly スコープもリクエストするように oauthScopes を更新します。

"oauthScopes": [
  "https://www.googleapis.com/auth/gmail.addons.execute",
   "https://www.googleapis.com/auth/gmail.addons.current.message.readonly"
],

Gmail でアドオンを実行し、Expense It! がメール メッセージを表示できるようにアクセスを承認します。フォーム フィールドに「TODO」が事前入力されるようになりました。

5. Google スプレッドシートを操作する

Expense It! アドオンには、ユーザーが費用の詳細を入力するためのフォームがありますが、その詳細の送信先がありません。フォームデータを Google スプレッドシートに送信するボタンを追加しましょう。

ボタンを追加するには、ButtonSet クラスを使用します。Google スプレッドシートと連携するには、Google スプレッドシート サービスを使用します。

createFormSection を変更して、カードのフォーム セクションの一部として「送信」というラベルの付いたボタンを返します。手順は次のとおりです。

  1. CardService.newTextButton() を使用してテキスト ボタンを作成し、CardService.TextButton.setText() を使用してボタンに「送信」というラベルを付けます。
  2. ボタンがクリックされたときに、CardService.TextButton.setOnClickAction() を介して次の submitForm アクションが呼び出されるようにボタンを設計します。
/**
 * Logs form inputs into a spreadsheet given by URL from form.
 * Then displays edit card.
 *
 * @param {Event} e An event object containing form inputs and parameters.
 * @returns {Card}
 */
function submitForm(e) {
  var res = e['formInput'];
  try {
    FIELDNAMES.forEach(function(fieldName) {
      if (! res[fieldName]) {
        throw 'incomplete form';
      }
    });
    var sheet = SpreadsheetApp
      .openByUrl((res['Spreadsheet URL']))
      .getActiveSheet();
    sheet.appendRow(objToArray(res, FIELDNAMES.slice(0, FIELDNAMES.length - 1)));
    return createExpensesCard(null, 'Logged expense successfully!').build();
  }
  catch (err) {
    if (err == 'Exception: Invalid argument: url') {
      err = 'Invalid URL';
      res['Spreadsheet URL'] = null;
    }
    return createExpensesCard(objToArray(res, FIELDNAMES), 'Error: ' + err).build();
  }
}

/**
 * Returns an array corresponding to the given object and desired ordering of keys.
 *
 * @param {Object} obj Object whose values will be returned as an array.
 * @param {String[]} keys An array of key names in the desired order.
 * @returns {Object[]}
 */
function objToArray(obj, keys) {
  return keys.map(function(key) {
    return obj[key];
  });
}
  1. CardService.newButtonSet() を使用してボタンセット ウィジェットを作成し、CardService.ButtonSet.addButton() を使用してテキスト ボタンをボタンセットに追加します。
  2. CardService.CardSection.addWidget() を使用して、ボタンセット ウィジェットをカードのフォーム セクションに追加します。

わずか数行のコードで、URL を指定してスプレッドシートを開き、そのシートにデータの行を追加できます。フォームの入力はイベント e の一部として関数に渡され、ユーザーがすべてのフィールドを入力したかどうかがチェックされます。エラーが発生しなかった場合、ステータスが「承認」の空の経費カードが作成されます。エラーが検出された場合は、エラー メッセージとともに元の入力済みカードが返されます。objToArray ヘルパー関数を使用すると、フォームの回答を配列に簡単に変換し、スプレッドシートに追加できます。

最後に、appsscript.jsonoauthScopes セクションを更新し、スコープ https://www.googleapis.com/auth/spreadsheets を再度リクエストします。このスコープが承認されると、アドオンはユーザーの Google スプレッドシートを読み取り、変更できるようになります。

"oauthScopes": [
  "https://www.googleapis.com/auth/gmail.addons.execute",
  "https://www.googleapis.com/auth/gmail.addons.current.message.readonly",
  "https://www.googleapis.com/auth/spreadsheets"
],

新しいスプレッドシートをまだ作成していない場合は、https://docs.google.com/spreadsheets/ で作成します。

アドオンを再度実行し、フォームを送信してみます。[スプレッドシートの URL] フォーム フィールドに、リンク先 URL の完全な URL を入力してください。

6. Properties サービスを使用して値を保存する

ユーザーは同じスプレッドシートに多くの費用を記録することが多いため、カードのデフォルト値として最新のスプレッドシートの URL を提供すると便利です。最新のスプレッドシートの URL を取得するには、アドオンが使用されるたびにその情報を保存する必要があります。

プロパティ サービスを使用すると、Key-Value ペアを保存できます。この場合、妥当なキーは「SPREADSHEET_URL」で、値は URL 自体になります。このような値を保存するには、シートに新しい行を追加するときにスプレッドシートの URL がプロパティとして保存されるように、Cards.gssubmitForm を変更する必要があります。

プロパティには、スクリプト、ユーザー、ドキュメントの 3 つのスコープのいずれかを設定できます。ドキュメント スコープは、Gmail アドオンには適用されません。ただし、特定の Google ドキュメントまたはスプレッドシートに固有の情報を保存する場合は、別のタイプのアドオンに関連します。アドオンでは、フォームのデフォルト オプションとして、自分自身の(他のユーザーのではなく)最新のスプレッドシートが個々のユーザーに表示されることが望ましい動作です。そのため、スクリプト スコープではなく、ユーザー スコープを選択します。

PropertiesService.getUserProperties().setProperty() を使用してスプレッドシートの URL を保存します。以下を Cards.gssubmitForm に追加します。

PropertiesService.getUserProperties().setProperty('SPREADSHEET_URL', 
    res['Spreadsheet URL']);

次に、Helpers.gsgetSheetUrl 関数を変更して、保存されたプロパティを返すようにします。これにより、ユーザーがアドオンを使用するたびに最新の URL が表示されるようになります。PropertiesService.getUserProperties().getProperty() を使用してプロパティの値を取得します。

/**
 * Determines most recent spreadsheet URL.
 * Returns null if no URL was previously submitted.
 *
 * @returns {String}
 */
function getSheetUrl() {
  return PropertiesService.getUserProperties().getProperty('SPREADSHEET_URL');
}

最後に、プロパティ サービスにアクセスするには、スクリプトの承認も必要になります。アドオンがプロパティ情報を読み書きできるように、これまでと同様にスコープ https://www.googleapis.com/auth/script.storage をマニフェストに追加します。

7. Gmail メッセージを解析する

ユーザーの時間を節約するため、メールから費用に関する関連情報を取得してフォームに事前入力しましょう。Helpers.gs には、この役割を果たす関数がすでに作成されていますが、これまでのところ、費用の日付、金額、説明については「TODO」のみが返されています。

たとえば、メールを受信した日付を取得し、それを経費の日付のデフォルト値として使用できます。

/**
 * Determines date the email was received.
 *
 * @param {Message} message - The message currently open.
 * @returns {String}
 */
function getReceivedDate(message) {
  return message.getDate().toLocaleDateString();
}

残りの 2 つの関数を実装します。

  1. getExpenseDescription では、送信者の名前とメッセージの件名の両方を結合することが考えられますが、メッセージ本文を解析してさらに正確な説明を返す方法もあります。
  2. getLargestAmount の場合は、お金に関連する特定の記号を探すことを検討してください。領収書には、税金やその他の手数料など、複数の値が記載されていることがよくあります。正しい金額を特定する方法を考えてみましょう。正規表現も便利です。

さらにアイデアが必要な場合は、GmailMessage のリファレンス ドキュメントを確認するか、Codelab の冒頭でダウンロードした解答コードをご覧ください。Helpers.gs のすべての関数に独自の実装を考案したら、アドオンを試してみましょう。領収書を開いて、スプレッドシートに記録しましょう。

8. カード アクションでフォームをクリアする

Expense It! が開いているメールの費用を誤って識別し、フォームに誤った情報が自動入力された場合はどうなりますか?ユーザーがフォームをクリアします。CardAction クラスを使用すると、アクションがクリックされたときに呼び出される関数を指定できます。これを使用して、ユーザーがフォームをすばやくクリアできるようにしましょう。

createExpensesCard を変更して、返されるカードに [フォームをクリア] というラベルの付いたカード アクションが含まれるようにします。このカード アクションをクリックすると、次の clearForm 関数が呼び出されます。この関数は Cards.gs に貼り付けることができます。フォームがクリアされたときにステータス メッセージが残るようにするには、opt_status を「Status」という名前のパラメータとしてアクションに渡す必要があります。アクションの省略可能なパラメータは Object.<string, string> 型である必要があります。そのため、opt_status が使用できない場合は、{'Status' : ''} を渡す必要があります。

/**
 * Recreates the main card without prefilled data.
 *
 * @param {Event} e An event object containing form inputs and parameters.
 * @returns {Card}
 */
function clearForm(e) {
  return createExpensesCard(null, e['parameters']['Status']).build();
}

9. スプレッドシートを作成する

Google Apps Script を使用して既存のスプレッドシートを編集するだけでなく、完全に新しいスプレッドシートをプログラムで作成することもできます。このアドオンでは、ユーザーが経費のスプレッドシートを作成できるようにします。まず、createExpensesCard が返すカードに次のカード セクションを追加します。

var newSheetSection = CardService.newCardSection();
var sheetName = CardService.newTextInput()
  .setFieldName('Sheet Name')
  .setTitle('Sheet Name');
var createExpensesSheet = CardService.newAction()
  .setFunctionName('createExpensesSheet');
var newSheetButton = CardService.newTextButton()
  .setText('New Sheet')
  .setOnClickAction(createExpensesSheet);
newSheetSection.addWidget(sheetName);
newSheetSection.addWidget(CardService.newButtonSet().addButton(newSheetButton));
card.addSection(newSheetSection);

ユーザーが [New Sheet] ボタンをクリックすると、アドオンはヘッダー行が固定されて常に表示されるように書式設定された新しいスプレッドシートを生成します。ユーザーはフォームで新しいスプレッドシートのタイトルを指定します。フォームが空白の場合に備えて、デフォルト値を含めることをおすすめします。createExpensesSheet の実装では、既存のカードとほぼ同じカードを返します。ただし、適切なステータス メッセージを追加し、URL フィールドに新しいスプレッドシートの URL を事前入力します。

10. 完了

メール内の経費を検出し、ユーザーが数秒で経費をスプレッドシートに記録できるようにする Gmail アドオンを設計して実装しました。Google Apps Script を使用して複数の Google API とのインターフェースを構築し、アドオンの複数の実行間でデータを永続化しました。

考えられる改善

Expense It! を強化する際は、想像力を働かせてください。さらに便利なプロダクトにするためのアイデアをいくつかご紹介します。

  • ユーザーが経費を記録した後のスプレッドシートへのリンク
  • 費用の記録を編集/取り消しできる機能を追加
  • 外部 API を統合して、ユーザーが支払いを行ったり、送金をリクエストしたりできるようにします。

詳細