使用 Google Workspace 外掛程式讓電子郵件變得更加實用

1. 總覽

在本程式碼研究室中,您將使用 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. 取得程式碼範例

在完成本程式碼研究室的過程中,您可能會需要參考所編寫程式碼的運作版本。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 外掛程式的使用者介面都包含資訊卡,這些資訊卡會分成一或多個區塊,每個區塊都包含小工具,可顯示及取得使用者的資訊。getContextualAddOn 函式會建立單一資訊卡,顯示電子郵件中找到的費用詳細資料。這張資訊卡有一個部分,內含相關資料的文字輸入欄位。此函式會傳回外掛程式的卡片陣列。在這個情況下,傳回的陣列只包含一張卡片。

部署 Expense It! 外掛程式前,您需要 Google Cloud Platform (GCP) 專案,Apps Script 專案會使用這項專案管理授權、進階服務和其他詳細資料。如要瞭解詳情,請參閱 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. 按一下「授權存取」,然後按照提示操作,將 Google 帳戶存取權授予「Expense It!」。

外掛程式會在開啟的 Gmail 郵件旁顯示簡單的表單。目前還不會執行任何其他動作,但您會在下一節中建構其功能。

如要查看外掛程式的更新,您只需要儲存程式碼並重新整理 Gmail。不需要額外部署。

4. 存取電子郵件訊息

加入可擷取電子郵件內容的程式碼,並將程式碼模組化,讓程式碼更有條理。

在「檔案」旁邊,依序點選「新增」圖示 新增檔案 >「指令碼」,然後建立名為 Cards 的檔案。建立名為 Helpers 的第二個指令碼檔案。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」字串,因為您會在後續步驟中實作預先填入邏輯。

接著,更新 GetContextualAddon.gs 中的程式碼,使其運用 Cards.gsHelpers.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 中,更新 oauthScopes,使其也要求 https://www.googleapis.com/auth/gmail.addons.current.message.readonly 範圍。

"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() 將按鈕集小工具新增至資訊卡的表單區段。

只需幾行程式碼,我們就能透過網址開啟試算表,然後將一列資料附加到該試算表。請注意,表單輸入內容會做為事件 e 的一部分傳遞至函式,我們會檢查使用者是否已提供所有欄位。假設沒有發生任何錯誤,我們會建立空白的費用卡,並顯示有利的狀態。如果我們發現錯誤,會連同錯誤訊息一併退回原始填寫的卡片。objToArray 輔助函式可輕鬆將表單回覆內容轉換為陣列,然後附加至試算表。

最後,請再次要求 https://www.googleapis.com/auth/spreadsheets 範圍,更新 appsscript.json 中的 oauthScopes 區段。授權這個範圍後,外掛程式就能讀取及修改使用者的 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/ 建立。

現在請重新執行外掛程式,然後嘗試提交表單。請務必在「試算表網址」表單欄位中輸入到達網頁網址的完整網址。

6. 使用 Properties 服務儲存值

使用者通常會將許多費用記錄在同一個試算表中,因此在資訊卡中提供最近使用的試算表網址做為預設值,會很方便。為了取得最新試算表的網址,我們每次使用外掛程式時都必須儲存該資訊。

屬性服務可讓我們儲存鍵/值組合。在本範例中,合理的鍵會是「SPREADSHEET_URL」,值則是網址本身。如要儲存這類值,請修改 Cards.gs 中的 submitForm,以便在將新資料列附加至試算表時,將試算表網址儲存為屬性。

請注意,屬性可以有三種範圍:指令碼、使用者或文件文件範圍不適用於 Gmail 外掛程式,但如果儲存特定 Google 文件或試算表的專屬資訊,則適用於其他類型外掛程式。就我們的外掛程式而言,我們希望使用者在表單上看到自己最近的試算表,而不是其他人的試算表。因此,我們選取「user」範圍,而非「script」範圍。

使用 PropertiesService.getUserProperties().setProperty() 儲存試算表網址。在 Cards.gssubmitForm 中新增下列內容:

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

然後修改 Helpers.gs 中的 getSheetUrl 函式,傳回儲存的屬性,讓使用者每次使用外掛程式時,都能看到最新的網址。使用 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();
}

實作其餘兩個函式:

  1. getExpenseDescription 可能會一併加入寄件者名稱和郵件主旨,但也有更精細的方法可剖析郵件內文,並提供更準確的說明。
  2. getLargestAmount建議尋找與金錢相關的特定符號。收據通常會列出多個值,例如稅金和其他費用。請思考如何找出正確金額。規則運算式也可能派上用場。

如需更多靈感,請參閱 GmailMessage參考說明文件,或查看您在本程式碼研究室一開始下載的解決方案程式碼。為 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 的實作中,傳回與現有資訊卡幾乎相同的資訊卡,並新增適當的狀態訊息,以及預先填入新試算表的網址。

10. 恭喜!

您已成功設計及實作 Gmail 外掛程式,可找出電子郵件中的費用,並協助使用者在幾秒內將費用記錄到試算表中。您已使用 Google Apps Script 與多個 Google API 建立介面,並在多次執行外掛程式時保留資料。

可能的改善做法

請盡情發揮想像力,進一步強化 Expense It!,以下提供一些建議,協助您打造更實用的產品:

  • 使用者記錄支出後,系統會提供試算表連結
  • 新增編輯/復原費用記錄的功能
  • 整合外部 API,讓使用者付款及要求付款

瞭解詳情