💿

GAS x Discordでメール通知

2024/01/31に公開

趣味でCDをたくさん借りていて最近TSUTAYA DISCASに加入しました。
発送処理されたら届くCDの名前と追跡番号が書いてある発送メールが、返却受付されたら返されたCDの名前が書いてある返却メールが来ます。
メールをそのまま見ていたのですが、Discordなどに通知が来るともっと便利に使えます。

なので、Google Apps Script(GAS)を使ってTSUTAYA DISCASから来たメールを必要なところだけ抜き出してDiscordにWebhookで通知するスクリプトを定期実行させることにしました。

画像のように通知されます。
discordの通知

Discord側準備

Discord側はIncoming Webhookの設定をするだけです。
通知したいテキストチャンネルの「チャンネルの編集」から連携サービス→ウェブフックで新しいウェブフックを作成してください。
「ウェブフックURLをコピー」をクリックすることでクリップボードにURLがコピーされますのでGASの登録の際に使用します。
discordのwebhook設定画面

GAS側準備

ソースコード

使用しているGASのコードは以下です。
日本語も変数にできるので日本語変数を一部使用しています。threadやmessageなどGASの中で出てくる言葉やwebhookなどそもそも英語なものは英語で書いています。
別に理由はなくて使ってみたかっただけなのでもしこのコードを使用する際にはTSUTAYA DISCAS以外で使用したい場合には変数名を適切なものに変更してください。

function hook() {
  const threads = GmailApp.search('is:unread label:discas');  // TSUTAYA DISCASから来たメールにつけてるラベル かつ 未読であること
  const webhook = PropertiesService.getScriptProperties().getProperty("DISCORD_WEBHOOK_URL");

  // 本文から情報を抽出するときに使う変数
  const 発送 = "発送"  // メールのタイトルに 発送 の文字が入る
  const 返却 = "返却"  // メールのタイトルに 返却 の文字が入る
  // 以下の4つはメールの本文にこの文章が今のところ固定で入っている
  const 発送始まり = "■通常プランで発送されたDVD/CD"
  const 発送終わり = "上記リンクをクリックしてもページが表示されない場合"
  const 返却始まり = "■今回返却されたDVD/CD"
  const 返却終わり = "■今回レンタルした"

  if (threads.length == 0) {
    Logger.log('新規メッセージなし');
    return
  }

  threads.forEach(function (thread) {
    const messages = thread.getMessages();

    const payloads = messages.map(function (message) {
      message.markRead();  // メールを既読に設定することで次回の実行時に通知対象から外れる

      const from = message.getFrom();
      const subject = message.getSubject();
      const plainBody = message.getPlainBody();
      let 本文始まり;
      let 本文終わり;

      if (subject.includes(発送)) {
        本文始まり = plainBody.indexOf(発送始まり);
        本文終わり = plainBody.indexOf(発送終わり);
      } else if (subject.includes(返却)) {
        本文始まり = plainBody.indexOf(返却始まり);
        本文終わり = plainBody.indexOf(返却終わり);
      } else {
        本文始まり = 0;
        本文終わり = 2048; // 1024で切ったらメールが半端なところで切れたので2048にした。これ以上大きいメールは来ないと思うし来ても見ない。でかいメールはGmailで見る。
      }

      const payload = {
        content: subject,
        embeds: [{
          title: subject,
          author: {
            name: from,
          },
          description: plainBody.substr(本文始まり, 本文終わり - 本文始まり),
        }],
      }
      return {
        url: webhook,
        contentType: 'application/json',
        payload: JSON.stringify(payload),
      }
    })

    Logger.log(payloads);
    UrlFetchApp.fetchAll(payloads);
  })
}

いくつかのGASっぽいところを解説します。

const threads = GmailApp.search('is:unread label:discas');  // TSUTAYA DISCASから来たメールにつけてるラベル かつ 未読であること
const webhook = PropertiesService.getScriptProperties().getProperty("DISCORD_WEBHOOK_URL");

GmailApp.search(検索条件)[1]はGmailのWebUIの上にある検索フォームと同じです。

すべてのスレッドのサイズが大きすぎてシステムで処理できない場合、この呼び出しは失敗します。

とあるのですが、大きすぎるサイズが良くわからないのでとりあえずこのままにします。

PropertiesService.getScriptProperties().getProperty(プロパティ名)[2]はGASでスクリプトプロパティ(環境変数)を呼び出すときに使います。
スクリプトプロパティはApps Scriptのプロジェクトの設定→スクリプトプロパティから追加ができます。
スクリプトプロパティの追加画面

スレッドという言葉が出てきましたが、GmailApp.search(検索条件)で得られる結果は「メールスレッドの配列」となります。
メールスレッドって何なんだろうと思い調べていくと、WebUIの受信トレイや検索結果で出てくる一列のことをthreadと呼ぶようです。
確かに返信されたメールなどはそのthreadの中にメールが入って行くので一連のやり取りの単位としてthreadというものになるという理解です。
更にthreadの中にはmessagesとして複数のメールが入っており、messageを取り出すと1通のメールになる。という構造です。

threads.forEach(function (thread) {
  const messages = thread.getMessages();
  const payloads = messages.map(function (message) {
    ~~~
  }
}

省略していますが、threadsからthreadを1件取り出し、その中でthread.getMessages()[3]でメッセージの配列を取得します。
取得したメッセージを更に1件ずつ処理していくことで各メールに対して処理ができるということです。

今回のTSUTAYA DISCASからのメールは1スレッド1メッセージなのであまり複数あることを考えなくても良いですが、今後どうなるかわからないので従います。

実際にメールを処理している部分では

message.markRead();  // メールを既読に設定することで次回の実行時に通知対象から外れる
const from = message.getFrom();
const subject = message.getSubject();
const plainBody = message.getPlainBody();

from、subject、plainBodyでそれぞれメールの情報を取得します。
特出すべきはmessage.getPlainBody()[4]の部分でこれはHTML形式ではないメッセージの本文の内容を取得します。
HTML形式で取得できればいい場合はmessage.getBody()という関数を使用すると本文のHTMLコンテンツを取得できるようです。

トリガー設定

TSUTAYA DISCASは発送処理が1日3回行われ、それぞれ

  • 03:00~03:30
  • 11:00~12:00
  • 15:00~15:30

に発送処理が行われるとのことです。
返却確認のメールは公式では11:00以降に、実際には10:00ぐらいから返却確認メールが届き始めます。

このタイミングでメールを処理するようにすれば良いので、1時間に1回実行するように設定します。
イベントのソースを時間主導型にすることで、一定時間ごとに実行するように設定できます。
トリガー設定

作ってみて

今回始めてGASを使って見たのですが、ほぼJavaScriptなので書きやすくお手軽に動かせるので便利だなとは思いました。
また、GoogleのサービスにアクセスするためのAPIも用意されているので色々使い所はありそうです。

一方で標準ではオンラインエディタでコードを書いてそのままデプロイまでするという流れなのでGitHubなどのバージョン管理、普段使い慣れてるエディタでは書けない、テストもちょっと面倒なので複雑なコードを書くときには色々な拡張をする必要があるなという印象です。

脚注
  1. searchメソッド ↩︎

  2. プロパティサービスのリファレンス ↩︎

  3. getMessagesメソッド ↩︎

  4. getPlainBodyメソッド ↩︎

Discussion