最近Slackからの通知をトリガーに作業を行うことが多いため、「メールをトリガーに作業を行う」という習慣がなくなりつつあります。
ただ、Webサービスからのメールなど、メールでしか通知を受け取れないこともあります。特に、「不定期に連絡があるけれど、見落とすとちょっと大変なこと」がメールに含まれている場合は、うっかり見落とすと気まずいことになります。
対策として、Gmailのラベル機能を使って見落とさないように努力していますが、それでも残念ながらうっかりしてしまいます。
そこで、「GoogleAppsScriptを使って、Gmailで特定のラベルが付いたメールがあったら、Slackへ通知した上で既読にする」ものを作ってみました。
目次
環境
- Google Apps Script (以下、
GAS
と表記) - Slack
なお、Slackへ通知するためのBot userはすでに作成済とします。
そのBotには scope として chat:write
を付与し、 xoxb
始まりのトークンも取得済とします。
仕様
- 受診したメールには、Gmailのラベルが設定済
- メールの確認は急ぎでは無いので、1日数回というタイミングで良い
- メール本文はSlackには通知しない
- GmailのURLだけ通知してもらい、そのURLを踏んでメールを確認する
- 「メール本文を解読してSlackに連携する」事も考えたが色々手間なので、メールを直接見るような導線だけ用意する
- Slackへの通知を行ったら、メール自体は既読にする
- 再度Slack通知されてしまうことを防ぐため
Gmail上のメールを一意に識別する値について
Gmail上のメールを一意に識別する値として、GASでは以下の2つの値が取得できるようです。
GmailMessage.getId()
の戻り値- https://developers.google.com/apps-script/reference/gmail/gmail-message#getId()
Gets the ID of this message.
とのこと- 同一メールであっても、Gmailのユーザーごとに別の値になる
GmailMessage.getHeader('Message-ID')
の戻り値
今回の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つを、以下を参考にして実装しました。
- hyperlink - Obtain a link to a specific email in GMail - Stack Overflow
- Gmail で特定のメールをあとで探せるようにしておく - Drafts
- GmailをURLで共有する - Qiita
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化 }) }
異なる点としては
Message-ID
はメールヘッダに含まれるため、getHeader()
で取得する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