GoogleAppsScriptを使って、Gmailで特定のラベルが付いたメールがあったら、Slackへ通知した上で既読にしてみた

最近Slackからの通知をトリガーに作業を行うことが多いため、「メールをトリガーに作業を行う」という習慣がなくなりつつあります。

ただ、Webサービスからのメールなど、メールでしか通知を受け取れないこともあります。特に、「不定期に連絡があるけれど、見落とすとちょっと大変なこと」がメールに含まれている場合は、うっかり見落とすと気まずいことになります。

対策として、Gmailのラベル機能を使って見落とさないように努力していますが、それでも残念ながらうっかりしてしまいます。

 
そこで、「GoogleAppsScriptを使って、Gmailで特定のラベルが付いたメールがあったら、Slackへ通知した上で既読にする」ものを作ってみました。

 
目次

 

環境

なお、Slackへ通知するためのBot userはすでに作成済とします。

そのBotには scope として chat:write を付与し、 xoxb 始まりのトークンも取得済とします。

 

仕様

  • 受診したメールには、Gmailのラベルが設定済
  • メールの確認は急ぎでは無いので、1日数回というタイミングで良い
  • メール本文はSlackには通知しない
    • GmailのURLだけ通知してもらい、そのURLを踏んでメールを確認する
    • 「メール本文を解読してSlackに連携する」事も考えたが色々手間なので、メールを直接見るような導線だけ用意する
  • Slackへの通知を行ったら、メール自体は既読にする
    • 再度Slack通知されてしまうことを防ぐため

 

Gmail上のメールを一意に識別する値について

Gmail上のメールを一意に識別する値として、GASでは以下の2つの値が取得できるようです。

 
今回のSlack通知は自分宛のみなため前者が実装できれば十分です。

ただ、他の人にもURLを共有できるようにしたいかもしれないので、両方の値を使った実装を試してみます。

 

実装

全体

全体はこんな感じです。これを一つの gs ファイルとして保存します。

const TARGET_LABEL = PropertiesService.getScriptProperties().getProperty("TARGET_LABEL")
const MAIL_FILTER = `label:${TARGET_LABEL} is:unread` // 対象ラベルかつ未読
const GMAIL_LABEL_URL = 'https://mail.google.com/mail/u/0/#label'
const GMAIL_MESSAGE_ID_URL = 'https://mail.google.com/mail/u/0/#search/rfc822msgid:'

// その他、Scriptのプロパティに以下を設定済という前提 (クラシックエディタから設定)
// TARGET_LABEL  検索対象のラベル
// SLACK_BOT_TOKEN  Slackのbot token
// SLACK_NOTIFICATION_TO  Slackの通知先

function main() {
  // 対象スレッドを取得
  const targetThreads = GmailApp.search(MAIL_FILTER, 0, 10)  // 最新より10件

  if (!targetThreads.length) {
    return
  }

  // 対象スレッドから、対象メールのURLを取得
  const urls = createUrlsFromThreadsByLabel(targetThreads)  // ラベルを使う場合
  // const urls = createUrlsFromThreadsByMessageId(targetThreads)  // Message-IDを使う場合

  // デバッグ用途として、実行ログに出力しておく
  console.log(urls)

  // Slackで通知
  const {responseStatusCode, responseBody} = notify(urls)

  if (responseStatusCode === 200 && responseBody.ok) {
    // 全部終わってからメールを既読にする
    targetThreads.map(t => t.markRead())
  } else {
    throw new Error(responseBody.error)
  }
}

function createUrlsFromThreadsByLabel(threads) {
  return threads
  .map(thread => thread.getMessages())
  .flat()  // messagesの配列を平坦化して扱いやすくする
  .map(message => {
    const mailId = message.getId()
    return `${GMAIL_LABEL_URL}/${TARGET_LABEL}/${mailId}`  // ラベルを使ってURL化
  })
}

function createUrlsFromThreadsByMessageId(threads) {
  return threads
  .map(thread => thread.getMessages())
  .flat()  // messagesの配列を平坦化して扱いやすくする
  .map(message => {
    const messageId = message.getHeader('Message-ID')
    return `${GMAIL_MESSAGE_ID_URL}${messageId}`  // Message-IDを使ってURL化
  })
}

function notify(urls) {
  const message = `
  メールが届きましたので、確認してください。

  ${urls.join('\n')}
  `

  const response = callWebApi("chat.postMessage", {
    channel: PropertiesService.getScriptProperties().getProperty("SLACK_NOTIFICATION_TO"),
    text: message
  })

  const responseStatusCode = response.getResponseCode()
  const responseBody = JSON.parse(response.getContentText())

  return {responseStatusCode, responseBody}
}

// 移植
// https://qiita.com/seratch/items/2158cb0abed5b8e12809
function callWebApi(apiMethod, payload) {
  const token = PropertiesService.getScriptProperties().getProperty("SLACK_BOT_TOKEN")
  return UrlFetchApp.fetch(
    `https://www.slack.com/api/${apiMethod}`,
    {
      method: "post",
      contentType: "application/x-www-form-urlencoded",
      headers: { "Authorization": `Bearer ${token}` },
      payload: payload,
    }
  )
}

 

個別の解説

全体のソースコードだけだと後で見返したときにわからなくなるため、個別に調べたことなどを記載します。

 

対象スレッドを取得する

GASの GmailApp.search() を使い、対象スレッドを検索します。

const TARGET_LABEL = PropertiesService.getScriptProperties().getProperty("TARGET_LABEL")
const MAIL_FILTER = `label:${TARGET_LABEL} is:unread` // 対象ラベルかつ未読

const targetThreads = GmailApp.search(MAIL_FILTER, 0, 10)  // 最新より10件

 
なお、検索で使える演算子については以下に記載がありました。
https://support.google.com/mail/answer/7190?hl=ja

今回はラベルが付いていて、かつ、未読のものを対象とするため、 label:${TARGET_LABEL} is:unread としています。

また、 search で複数条件のANDを取りたいため、条件をスペースで区切っています。
https://developers.google.com/apps-script/reference/gmail/gmail-app#searchquery

 

対象スレッドから各メールのURLを取得する

今回は

  • GmailMessage.getId() の戻り値を使うパターン
    • createUrlsFromThreadsByLabel()
  • GmailMessage.getHeader('Message-ID') の戻り値を使うパターン
    • createUrlsFromThreadsByMessageId()

の2つを、以下を参考にして実装しました。

 

GmailMessage.getId() の戻り値を使うパターン

メソッドチェーンを利用して取得しています。

const GMAIL_LABEL_URL = 'https://mail.google.com/mail/u/0/#label'

function createUrlsFromThreadsByLabel(threads) {
  return threads
  .map(thread => thread.getMessages())
  .flat()  // messagesの配列を平坦化して扱いやすくする
  .map(message => {
    const mailId = message.getId()
    return `${GMAIL_LABEL_URL}/${TARGET_LABEL}/${mailId}`  // ラベルを使ってURL化
  })
}

GASの GmailApp.search() の戻り値は GmailThread[] であることから、 GmailThread.getMessages () を使ってメールを配列で取得します。
https://developers.google.com/apps-script/reference/gmail/gmail-thread#getMessages()

二次元配列になるため、 flat() で平坦化したあと、 GmailMessage.getId() でメールIDを取得しています。
https://developers.google.com/apps-script/reference/gmail/gmail-message#getheadername

メールIDが取得できたら、ラベルを使ったURL化を行っています。

 

GmailMessage.getHeader('Message-ID') の戻り値を使うパターン

こちらも、全体の流れは前掲のパターンと同じです。

const GMAIL_MESSAGE_ID_URL = 'https://mail.google.com/mail/u/0/#search/rfc822msgid:'

function createUrlsFromThreadsByMessageId(threads) {
  return threads
  .map(thread => thread.getMessages())
  .flat()  // messagesの配列を平坦化して扱いやすくする
  .map(message => {
    const messageId = message.getHeader('Message-ID')
    return `${GMAIL_MESSAGE_ID_URL}${messageId}`  // Message-IDを使ってURL化
  })
}

異なる点としては

となります。

 

Slackでの通知

以下の記事を参考に、必要な部分を移植・修正しました。
Google Apps Script (GAS) で Slack 連携を実装する前に知っておくとよい 5 つのこと - Qiita

 

GASによるメールの既読処理

GmailThread.markRead() を使い、対象のスレッドを既読にします。
https://developers.google.com/apps-script/reference/gmail/gmail-thread#markRead()

targetThreads.map(t => t.markRead())

 

定期実行について

今回はGASのトリガー機能にて 日付ベースのタイマー で毎日 午前9時〜10時 に定期実行するよう登録しました。

今回は急ぎではないためこの程度のトリガーとしましたが、必要に応じてトリガー設定を調整します。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/slack_notification_from_mail