GASで監視メールをSlackに流す

こんにちは、Slack依存症のwakです。

監視メールが多くて困る

弊社ではゴルフ場の基幹システムを取り扱っています。 ゴルフ場内のネットワークは様々な制約がかけられていることが多々あり、外部へ許可された通信はHTTP(S)とメールだけ、ということも少なくありません。 必定、ジョブ監視結果などはメールに頼ることになります。 ですが弊社では現在【メールをなるべく減らそう】運動の真っ最中で、できることならば監視メールをチェックする業務はなくしたいところです。面倒ですからね。

f:id:nurenezumi:20150427194655p:plain

今回も本文と特に関係のないかわいい猫

GASとは?

そこで登場するのがGoogle AppsとGAS(Google Apps Script)です。 GASを一言で表現すると、 Gmail, Google Drive, Google Docsなど、 Googleの各種サービスをお手軽に呼び出せるスクリプト環境(サーバーサイドJavaScript です。

たとえば次のようなことが簡単に実現できます。

  • Gmailを検索する。検索には「from:example.com 監視 label:unread」のような、普段Gmailで使っている検索クエリが利用できる。
  • Gmailの本文や添付ファイルを取得する。
  • Google Driveに任意のテキストやバイナリストリームを保存する。
  • Google Driveに保存したファイルの共有設定を変更する。
  • 外部サービスを呼び出し、POSTでデータを送信する。
  • スクリプトを一定時間おきに実行する。

これを繋げれば、こんなことが実現できます。

  1. 監視メールを受信し、未読のものがないか調べる。
  2. 未読監視メールがあったら、本文と添付ファイルをGoogle Driveに保存して、他ユーザーと共有する。
  3. Slackに通知する。長すぎると見づらいので、先頭数行以降は省略する。(省略した部分や添付ファイルはGoogle DriveへのURLを書く)
  4. 処理したメールは既読にする。

やってみる

やってみましょう。

スプレッドシートを新規作成する

スクリプトGoogle Docs上の何らかのファイル(スプレッドシート、ドキュメントなど)に含める形で書いていきます。 ここでスプレッドシートを選んでおくと後で色々と便利なので、今回はスプレッドシートを新規作成します。

スクリプトエディタを開く

新規作成したスプレッドシートで、ツール→スクリプトエディタをクリックするとクリプト入力環境が開きます。最初に開くとテンプレートを選べますが、「空のプロジェクト」を選びましょう。ここにコードを書いてゆきます。

メールを検索する

メール検索はこの1行だけです。

var threads = GmailApp.search("from:example.com 監視 label:unread");

注意点は、メールはスレッド単位でしか検索ができないことです。このsearchメソッドはスレッドの配列を返すので、スレッドが含むメールを順に取得して処理する必要があります。引っかかったメールを全件1次元の配列に並べましょう。

var messages = Array.prototype.concat.apply([], threads.map(function(t) { return t.getMessages(); }));

スクリプトエディタの補完は効きませんが、配列も通常のJavaScriptと同様に扱えます。これをArray.forEachで回せばいいわけですね。

messages.forEach(function(message) { ... });

本文・添付ファイルをGoogle Driveに入れる

手順は次の通りです。

  1. 保存するバイナリストリームかテキストを取得(または作成)する。
  2. データをGoogle Driveに保存する。ファイルは必ずルートフォルダーに入る(固定)。
  3. 必要に応じて、作成したファイルを別のフォルダーに移動(実際はコピー・削除)する。
  4. 共有設定を変更して他ユーザーから見えるようにする。
  5. URLを取得する。(ブラウザで開くとプレビューまたはダウンロードができるようになる)

事前準備として、ファイルを入れる格納先のフォルダーを作成し、そのフォルダーIDを調べておきましょう。

var folders = DriveApp.getFoldersByName("folderName");
while(folders.hasNext()) {
  var folder = folders.next();
  Logger.log(folder.getId());
}

これさえできたら後は簡単です。まずファイルを作りましょう。メールの本文(=テキストデータ)を保存するならこうなります。

var text = message.getBody(); // メール本文
var file1 = DriveApp.createFile(filename, text, "text/html"); // 第3引数はmime

メールの添付ファイルを保存するならこうなります。

var blob = message.getAttachments()[0].copyBlob(); // 添付ファイルのバイナリデータ
var file1 = DriveApp.createFile(blob); // 保存(名前は指定できない)
file1.setName(filename); // ...ので、ここで名前設定

これでGoogle Driveへ保存ができました。残りの処理はシンプルです。

var destFolder = DriveApp.getFolderById(folderId); // 移動先フォルダを取得
var file2 = file1.makeCopy(destFolder); // まず移動先にコピー
DriveApp.removeFile(file1); // コピー元はもう必要ないので削除
file2.setSharing(DriveApp.Access.DOMAIN, DriveApp.Permission.VIEW); // 権限変更
var url = file2.getUrl(); // URL取得

Slackに通知する

ちょっと長いのですが、構造は簡単です。

UrlFetchApp.fetch("https://hooks.slack.com/services/__your_secret_token__", {
  payload : JSON.stringify({
    channel : channel, // 通知先チャンネル名
    text : "本文本文本文本文本文\n本文本文本文本文本文",
    icon_emoji: icon, // アイコン(":heart:など")
    username: userName, // 通知するbotに付ける名前
    attachments: [
      {
        fallback: "添付データその1",
        pretext : "プレテキスト1",
        color : "#FFFF00",
        fields : [{title: "タイトル1", value: "テキスト1"}, {title: "タイトル2", value: "テキスト2"}]
      },
      {
        fallback: "添付データその2",
        pretext : "プレテキスト2",
        color : "#FF00FF",
        fields : [{title: "タイトル3", value: "テキスト3"}, {title: "タイトル4", value: "テキスト4"}]
      }
    ]
  })
});

通知の結果はこうなります。

f:id:nurenezumi:20150427173336p:plain
  • 本文は改行可能(\nで改行)
  • 任意の色のバーを複数表示できる(エラーレベルによって変えると良いでしょう)

あたりがポイントですね。必要のない部分は要素ごと削ればもっと短くなります。

スプレッドシートで動作設定をする

送信元によって監視メールにはいくつかパターンがあると思います。 全文をそのままSlackに載せれば事足りるもの、本文のうち一部を取り出して通知したいもの、添付ファイルがあるもの等々、メールの検索クエリも処理内容も通知先チャンネルも異なるはずです。

これらはスプレッドシートに書くのが簡単です。

f:id:nurenezumi:20150427180426p:plain

スプレッドシートにこのように書いておいて、この内容をGASで読み取り、動作パターンを変えてゆきます。 このシートの内容は自分しか変更しないことを前提としているため「関数名」列の値をそのまま関数名として実行してしまっていますが、気になる場合はホワイトリスト方式でチェックしてください。

var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
for (var i = 2; i <= 100; i++) { // 2行目から順にサーチしていく
  var alertName = sheet.getRange(i, 1).getValue();
  var functionName = sheet.getRange(i, 2).getValue();
  var channel = sheet.getRange(i, 3).getValue();
  // ...
  
  try {
    var func = eval(functionName);
    func(alertName);
  } catch(ex) {
    Logger.log(ex);
  }
}

おわりに

今回はできることを列挙してみましたが、次回はコードを公開できる形でまとめてみようと思います。

GASはいまいちメジャーでないのですが、サービスとサービスとをつなげる「ノリ」として使えばとても効率的に使える道具だと感じました。面白い使い道の記事がもっと増えるといいですね。