使用 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. 如要建立新專案,請按一下左上方的「New Project」。新專案隨即開啟,並顯示名為 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 函式會建立一張資訊卡,用來取得電子郵件中的支出詳細資料。資訊卡中有一個部分包含相關資料的文字輸入欄位。函式會傳回外掛程式資訊卡的陣列。在本例中,傳回的陣列只包含一張資訊卡。

部署費用之前!外掛程式,則需要有 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. 右側面板就是「開銷!」畫面上會顯示「實戰完畢!收據圖示」外掛程式。你可能要點選「更多外掛程式」圖示 更多外掛程式,才能找到這項功能。
  3. 開啟電子郵件 (建議提供有費用的收據)。
  4. 如要開啟外掛程式,請在右側面板中,按一下「Expense It!」實戰完畢!收據圖示
  5. 快去申辦!請按一下「授權存取權」並按照提示操作,即可存取 Google 帳戶。

這個外掛程式會在開啟的 Gmail 郵件旁顯示簡易表單。此時這項新功能還不會執行其他操作,但您將在下一節建構相關功能。

如要在使用本研究室期間持續查看外掛程式的更新內容,請先儲存程式碼並重新整理 Gmail。不需要其他部署項目。

4. 存取電子郵件

新增可擷取電子郵件內容的程式碼,以及對程式碼進行模組化,適用於更有組織的架構。

按一下「檔案」旁邊的「新增」圖示 新增檔案 >「指令碼」,建立名為 Cards 的檔案。建立第二個指令碼檔案,名為 HelpersCards.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 試算表

開銷!外掛程式能提供使用者輸入費用詳細資料的表單,但這些詳細資料現在不在此處。我們接下來要新增按鈕,以便將表單資料傳送至 Google 試算表。

如要新增按鈕,我們會使用 ButtonSet 類別。如要整合 Google 試算表,請使用 Google 試算表服務

修改 createFormSection,傳回標籤為「Submit」的按鈕可顯示在資訊卡的表單部分中按照下列步驟操作:

  1. 使用 CardService.newTextButton() 建立文字按鈕,將按鈕標示為「提交」使用 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() 在資訊卡的表單部分中新增按鈕設定小工具。

只要加入幾行程式碼,我們就能依網址開啟試算表,然後在該工作表中附加一列資料。請注意,表單輸入內容會做為事件 e 的一部分傳遞至函式,而且我們會檢查使用者是否已提供所有欄位。如果未發生任何錯誤,我們會建立一個狀態良好的空白支出資訊卡。如果發生錯誤,系統會傳回原本填寫的資訊卡和錯誤訊息。objToArray 輔助函式可讓您輕鬆將表單回應轉換為陣列,然後附加至試算表。

最後,更新 appsscript.json 中的 oauthScopes 區段,再次要求範圍 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/ 建立試算表。

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

6. 使用屬性服務儲存值

使用者通常會將多筆支出記錄在同一份試算表,因此建議您提供最新的試算表網址,做為資訊卡的預設值。為得知最新試算表的網址,每次使用外掛程式時,我們都需要儲存該資訊。

屬性服務可讓我們儲存鍵/值組合。在我們的例子中,合理的金鑰為「SPREADSHEET_URL」而值就是網址本身如要儲存這類值,您需要修改 Cards.gs 中的 submitForm,讓系統在工作表中附加新資料列時,就會將試算表的網址儲存為屬性。

請注意,屬性可包含三個範圍之一:指令碼、使用者或文件文件範圍不適用於 Gmail 外掛程式,不過是與不同類型的外掛程式儲存特定 Google 文件或試算表相關的資訊。外掛程式設定的目的,是讓個別使用者 (而非他人) 最新的試算表做為表單的預設選項。因此,我們選取的是 user 範圍,而非 script 範圍。

使用 PropertiesService.getUserProperties().setProperty() 儲存試算表網址。將以下內容新增至 Cards.gs 中的 submitForm

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. 清除包含資訊卡動作的表單

如果支出的話會發生什麼事呢!在未結電子郵件中誤認費用,卻在表單中預先填入錯誤資訊?使用者清除表單。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);

現在使用者點選「新工作表」時按鈕會產生新的試算表,其中含有凍結的標題列,方便隨時顯示。使用者會在表單中指定新試算表的標題,不過如果表單為空白,建議您使用預設值。實作 createExpensesSheet 時,請將近乎相同的資訊卡傳回現有的資訊卡,並加入適當的狀態訊息,並預先填入新試算表的網址。

10. 恭喜!

您成功設計並導入 Gmail 外掛程式,可以查詢電子郵件費用,使用者只要短短幾秒,就能將費用記錄到試算表。您已使用 Google Apps Script 處理多個 Google API,並在外掛程式多次執行之間保留資料。

可能的改善項目

不妨發揮想像力,大力提升「成本」!不過,請參考以下幾個點子,瞭解如何打造更實用的產品:

  • 在使用者記錄費用後開啟試算表的連結
  • 新增編輯/取消支出記錄的功能
  • 整合外部 API,讓使用者付款及請款

瞭解詳情