🙆‍♀️

Gmailに届いた添付ファイルをGASでSlackに流す

2022/12/05に公開

これは株式会社SUPER STUDIO Advent Calendar 2022の3日目の記事です。
https://adventar.org/calendars/8255

ごきげんよう🌸
株式会社SUPER STUDIOでコーポレートエンジニアを担当している@bboobbaaです!

晩夏のとある日、QuickSightのユーザー作成・権限・課金に悩まされておりました。
そのときに、Amazon QuickSightのビジュアルを画像に変換するの記事を読んで、「Adminユーザーにメール送ってGASでSlackにファイル流せばええやん」と気付き実装しました。

GASとSlack APIを利用する初心者向けの記事となります。

まずはSlackのトークンの入手

Appを作成してTokenを入手します。
chat:writefile:writeの権限があればSlackの書き込みとアップロードができます。

const SLACK_TOKEN = 'xoxb-000000000000-000000000000-xxxxxxxxxxxx';

Gmailからレポートのスレッドの抽出

Class GmailAppより、スレッドの検索をします。
今回はQuickSightのレポートなので、Quicksight Reportで検索するとスレッドが抽出できます。

const threads = GmailApp.search('Quicksight Report', 0, 2);

第二引数の0は開始スレッドのインデックス、第三引数の2は返されるスレッドの最大数を設定しています。
スレッドの仕様についてはメールをスレッドごとにグループ化するマニュアルを参照ください。

Gmailのスレッドからメッセージの抽出

Class GmailAppより、スレッドからメッセージを取得します。

const messages = GmailApp.getMessagesForThreads(threads);

これでメッセージの連想配列となりました。

スレッドをループしてメッセージを取得

スレッドの中に複数メッセージが入っているので、ループを2回まわします。

// スレッドのループ
for (const i in messages) {
  // スレッド内のメッセージのループ
  for (const j in messages[i]) {
    console.log(messages[i][j]); 
  }
}

メッセージの詳細は各関数を使って取得することになります。
例えばタイトルを取得したい場合は、messages[i][j].getSubject()と書きます。

{ toString: [Function],
  getDate: [Function],
  getFrom: [Function],
  refresh: [Function],
  getHeader: [Function],
  getId: [Function],
  reply: [Function],
  isStarred: [Function],
  getSubject: [Function],
  getReplyTo: [Function],
  markRead: [Function],
  getAttachments: [Function],
  createDraftReply: [Function],
  getBody: [Function],
  getThread: [Function],
  getTo: [Function],
  forward: [Function],
  star: [Function],
  isDraft: [Function],
  moveToTrash: [Function],
  isInPriorityInbox: [Function],
  createDraftReplyAll: [Function],
  getPlainBody: [Function],
  getRawContent: [Function],
  isInTrash: [Function],
  unstar: [Function],
  markUnread: [Function],
  isUnread: [Function],
  isInInbox: [Function],
  isInChats: [Function],
  replyAll: [Function],
  getCc: [Function],
  getBcc: [Function] }

添付ファイルの取得

添付ファイル情報は、messages[i][j].getAttachments()から取得できますが、配列でデータを持っているためもう一度ループを回します。
QuickSightの場合メールの添付にロゴもくっついてくるので、ロゴの場合は処理をスキップします。

for (const i in messages) {
  for (const j in messages[i]) {
    for (const attachment of messages[i][j].getAttachments()) {
      if (attachment.getName() != 'logo.png') {
        console.log(attachment);
      }
    }
  }
}

Slackにファイルをアップロードする

files.uploadをつかってファイルをアップロードします。SlackはBearer認証です。

const payload = {
  file: attachment,
  channels: 'XXXXXXXXXXX',
}
const option = {
  method: 'post',
  payload: payload,
  headers: {'Authorization': `Bearer ${SLACK_TOKEN}`},
}
JSON.parse(UrlFetchApp.fetch('https://slack.com/api/files.upload', option).getContentText());

このままだと、ファイルごとにSlackに投稿されてしまう

見た目的によろしくないですね。というわけでチャンネル情報を渡しません。
アップロードだけして、ファイルのURLだけいただきましょう。

let text = '';
~~
const payload = {
  file: attachment,
}
~~
const upload =  JSON.parse(UrlFetchApp.fetch('https://slack.com/api/files.upload', option).getContentText());
text += `${upload.file.permalink}\n`;

Slackに情報を投稿する

先程のURLをまとめたテキスト情報をchat.postMessageを使ってSlackに投稿します。

let text = '';
~~
const payload = {
  channel: 'XXXXXXXXXXX',
  text: text,
}
const option = {
  method: 'post',
  contentType: 'application/json',
  payload: JSON.stringify(payload),
  headers: {'Authorization': `Bearer ${SLACK_TOKEN}`},
}
UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', option).getContentText();

あれれ〜おかしいぞ〜?実行するたびに毎回投稿される

投稿したら完了フラグ立てないと、実行したら毎回投稿してしまいます。
投稿したらstar()でメッセージにスターを付けて、以降はisStarred()trueが返ってくるものは処理しないように判定しましょう。

最終的なコードはこんなかんじ

const SLACK_TOKEN = 'xoxb-000000000000-000000000000-xxxxxxxxxxxx';

const threads = GmailApp.search('Quicksight Report', 0, 2);
const messages = GmailApp.getMessagesForThreads(threads);

let text = '';
for (const i in messages) {
  for (const j in messages[i]) {
    if (!messages[i][j].isStarred()) {
      for (const attachment of messages[i][j].getAttachments()) {
        if (attachment.getName() != 'logo.png') {
	  const payload = {
	    file: attachment,
	  }
	  const option = {
	    method: 'post',
	    payload: payload,
	    headers: {'Authorization': `Bearer ${SLACK_TOKEN}`},
	  }
	  const upload =  JSON.parse(UrlFetchApp.fetch('https://slack.com/api/files.upload', option).getContentText());
          text += `${upload.file.permalink}\n`;
        }
      }
      messages[i][j].star();
    }
  }
}

const payload = {
  channel: 'XXXXXXXXXXX',
  text: text,
}
const option = {
  method: 'post',
  contentType: 'application/json',
  payload: JSON.stringify(payload),
  headers: {'Authorization': `Bearer ${SLACK_TOKEN}`},
}
if (text) UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', option).getContentText();

GASのトリガーを設定して完了

よしなに関数組み立ててください!

Discussion