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

1. 概要

この Codelab では、Google Apps Script を使用して、Gmail 内でメールの領収書データをスプレッドシートに直接追加できる Gmail 用の Google Workspace アドオンを作成します。領収書をメールで受け取ると、関連する経費情報がメールから自動的に取得されるアドオンが開きます。ユーザーは経費情報を編集して提出し、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. [Show "appscript.json"]manifest file in editor] チェックボックスをオンにします。
  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. 右側のパネルには、出費する領収書アイコン アドオンが表示されます。必要に応じて [その他のアドオン] その他のアドオン をクリックします。
  3. メール(できれば経費が記載された領収書)を開きます。
  4. アドオンを開くには、右側のサイドパネルで [出費する] をクリックします。出費する領収書アイコン
  5. お出かけのお供に![Authorize Access] をクリックして Google アカウントにアクセスし、画面の指示に沿って操作します。

開いている Gmail のメールの横にシンプルなフォームが表示されます。これ以外のことはまだ何も起こりませんが、次のセクションで機能を構築します。

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

4. メールへのアクセス

メールの内容を取得するコードを追加し、もう少し整理できるようにコードをモジュール化します。

ファイルの横にある追加アイコン ファイルを追加 をクリック >Script を実行し、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);
}

新しい getCurrentMessage 関数に注目してください。この関数は、Gmail から提供されたイベントを使用して、現在開いているメッセージを読み取ります。この機能を動作させるには、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 スプレッドシートを操作する

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

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

「Submit」というラベルのボタンを返すように createFormSection を変更します。カードのフォームセクションに 追加できます手順は次のとおりです。

  1. CardService.newTextButton() を使用してテキストボタンを作成し、ボタンに「Submit」というラベルを付けます。CardService.TextButton.setText() を使用します。
  2. ボタンがクリックされたときに、次の submitForm アクションが CardService.TextButton.setOnClickAction() を介して呼び出されるようにボタンを設計します。
/**
 * 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 を変更する必要があります。

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

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 を変更して、返されるカードに「Clear form」というカード アクションが含まれるようにします。クリックすると、次の 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);

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

10. 完了

Gmail アドオンの設計と実装は成功です。このアドオンは、メール内の経費を見つけ出し、ユーザーがわずか数秒で経費をスプレッドシートに記録できるようにするものです。ここでは、Google Apps Script を使用して複数の Google API とやり取りし、アドオンの複数回の実行間でデータを保持しました。

可能な改善

Expense It! を強化するときは、想像力を働かせてください。さらに便利な製品にするためのアイデアもいくつかあります。

  • ユーザーが経費を記録した後のスプレッドシートへのリンク
  • 経費の記録を編集/元に戻す機能を追加
  • 外部 API を統合して、ユーザーが支払いや請求を行えるようにする

詳細