見出し画像

【GAS】スプシをメール送信キューにする


メール送信の統一

小規模なら、わざわざサーバ側言語等でメール送信処理を作らずとも
サーバ→スプシへ値を連携し、GASからメール送信することで
GASをメールサーバ扱いできる。

発火タイミング

メール送信処理の発火タイミングが大切。
今回は、特定のセルが更新されたときに、トリガーをセットする処理でいく。

F1セルに値が入ると、
G1セルにはGASで作った関数を読んでいて、引数でF1を参照しているので再計算が走り
関数をキックできる。

/** キック処理 シート編集で起動 */
function ignite(flg) {
  if (flg === 'ok') setTrig() // ※「setTrig関数」は自作のもの。あとで説明します

  // 本当は ↓ こうしたいが
  // SpreadsheetApp.getActiveSheet().getRange('F1').setValue('done')

  // Exception: You do not have permission to call setValue(行 32)が発生しました。
  // になる

  return 'regist'
}

コメント中にも記載したが、肝はignite関数の再計算のタイミング。
スプシの再計算タイミングは
1️⃣シートロード時
2️⃣関数の引数で参照するセルが更新された時
であり、現状2️⃣でキックしているが、1️⃣でも動いてしまう。

とはいえ、ignite関数のようにスプシ上で読んでいる関数は、
図形に登録して実行したときのGAS実行を認証する画面が出ないので、permissionエラーでsetValueができない。現状これが最適解。

トリガー処理を準備

空回りで実行してしまうことの制御やロジック部の外だし、実行時間6分の制限がGASにあることを含め
トリガーを用いることを強く推奨します。

トリガーとはGAS上でのキューのようなもので、
1️⃣関数名と
2️⃣トリガー登録〜関数実行までの遅延時間 (時間ベースなら)
を登録できる。
以下では、トリガーを登録する処理 = setTrig、登録済みトリガーをクリアする処理 = delTrigを作った。

/**
 * ★トリガーの呼ばれ方
 * 「ignite」関数をスプシで呼ぶと
 * 引数セルが更新されたタイミングで
 * 「exe」関数が実行される。
 * 
 * ★機能実装の方法
 * 「exe」関数にて機能を実装する。
 * exe関数冒頭でdelTrig関数を実行すること
 */
/** トリガー設定 */
const TRIGGER_FUNCTION = 'exe'
const TRIGGER_KEY = 'mail_trigger_key'
const RETRY_DELAY = 10 * 1000
/** トリガーセット */
const setTrig = () => ScriptApp.newTrigger(TRIGGER_FUNCTION).timeBased().after(RETRY_DELAY).create().getUniqueId()
/** トリガー削除 */
const delTrig = () => {
  let triggers = ScriptApp.getProjectTriggers()
  if (triggers.length === 0) {
    Logger.log(`削除対象のトリガーidがない`)
    return
  }
  triggers.forEach(v => {
    Logger.log(`トリガーを削除 id: ${v.getUniqueId()}`)
    ScriptApp.deleteTrigger(v)
  })
}

よってセルが更新されてGAS関数がキックされたら、直でロジックでなく
トリガーを起動し、ちょっと待ってからロジックが走るようになりました。

メール送信の準備

Drive上のファイルを添付ファイルとしたいことも含め、もろもろアクセスの許可をする

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.scriptapp",
    "https://www.googleapis.com/auth/gmail.send"
  ]
}

メール送信の処理

さて、exeという名前の関数がトリガーから呼ばれるようにしたので
ロジックはexeという関数の中に書いていきましょう。
なんかの場合に重複してキックされてトリガーがいっぱい登録されちゃってると悲しいので
先頭で、念の為トリガーをクリアしておきます。

function exe() {
  delTrig() // トリガーをクリア

トリガー経由で読んでいるので、SpreadsheetApp.getActiveSpreadSheet()ではなく
ID指定で開きます

  const SHEET_ID = 'XXXXX'
  let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName('XXXX')
  let getVal = cell => sheet.getRange(cell).getValue()
  // 「A1」とかを入れると直で値とれる関数をつくってみた

メール送信部は、キューとなっているスプシの表を全量読み

  1. セルから値を取得

  2. 指定されている場合、添付ファイルを Driveから取得する(名前重複ファイルがあったら最初の一つ)

  3. メール送信。ファイルが画像であれば、インライン要素としてhtml形式にする

  4. キュートしていたスプシの表と、添付し終わったファイルを削除して終了

  let lastRow = sheet.getLastRow()
  // キューなし
  if (lastRow === 1) {
    Logger.log('no queue')
    return
  }
  // 2行目から下のキューを送信
  [...Array(lastRow - 1)].map((_, i) => i + 2).map(r => {

    // 1. セルから値を取得
    let to = getVal(`A${r}`)
    let cc = getVal(`B${r}`)
    let subject = getVal(`C${r}`)
    let body = getVal(`D${r}`)
    let attNm = getVal(`E${r}`)

    // 2. 指定されている場合、添付ファイルを取得する
    let attF = ""
    let attFId = ""
    if (attNm !== "") {
      const files = DriveApp.getFolderById('ファイルがあるDriveフォルダのID').getFiles()
      while (files.hasNext()) {
        let file = files.next()
        if (file.getName() === attNm) { // ファイル名で最初に一致したものを添付. ファイル名称は一意でないので、命名に注意
          attF = DriveApp.getFileById(attFId = file.getId()).getBlob()
          break
        }
      }
    }

    // 3. 送信
    let options = {cc: cc, name: 'おしらせbot'}
    if (attF !== "") { options.attachments = attF }
    if (attF !== "" && attNm.endsWith("png") || attNm.endsWith("jpg")) {
      body = body.replaceAll('\n', '<br>')
      body += `<br><img src='cid:inlineImg' style="width: 100%;">`
      options["htmlBody"] = body
      options["inlineImages"] = { inlineImg: attF }
    }

    GmailApp.sendEmail(to, subject, body, options)
    Logger.log(`send queue row#${r}`)

    // 4. 添付ファイル、キューを削除
    if (attFId !== "") {
      DriveApp.getFileById(attFId).setTrashed(true)
    }
    sheet.getRange(`A${r}:E${r}`).clearContent()
  })
}

レポ

secret系があるのでGASをそのまま公開はできませんが
一応githubに挙げているのでおいておきます。

ALHについて知る



↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓


↓ ↓ ↓ コーポレートサイトはこちら ↓ ↓ ↓


↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓