DiscordにGmailの未読を流すbotをGASで作った

# 動機
団体で持っているGmailを誰も確認しないで放置する事案が発生したので
GoogleCalenderと同じようにDiscordに流すことにした

前記事

https://qiita.com/McbeEringi/items/8339006dd57ff06308ab

- 毎日指定時刻に未読メッセージを送信
- 以下は除外
    - 広告メール
    - 古い未読メール
- ウィジェットで本文もプレビュー
- 同一の送信者が一目でわかるように色付け
- メール本体に飛べるリンク

# コード全文
基本構造は前のcalenderと同じ
daily()を走らせると今後も実行される
```js
const
cfg={
  time:'07:00',// 通知の時刻
  webhooks:[// メッセージ送信先のwebhook 複数指定可
    'https://discord.com/api/webhooks/xxxxx/xxxxx'
  ],
  template:{// メッセージのテンプレート
    username:'Gmail-chan',
    avatar_url:'https://mcbeeringi.github.io/img/icon.png'
  }
},
crct=[...Array(256)].map((_,n)=>[...Array(8)].reduce(c=>(c&1)?0xedb88320^(c>>>1):c>>>1,n)),crc=(buf,crc=0)=>~buf.reduce((c,x)=>crct[(c^x)&0xff]^(c>>>8),~crc),// CRC32 大雑把に言うと乱数生成
widget=w=>({//discord embed形式
  color:crc(Utilities.newBlob(w.getFrom()).getBytes())&0xffffff,// 送信者情報からランダムな色を生成
  author:{name:w.getFrom()},// 送信者
  url:`https://mail.google.com/mail/u/example@gmail.com/#all/${w.getId()}`,
  title:w.getSubject(),// 件名
  description:w.getPlainBody()// 本文 改行は全部強制でCRLFになるっぽい
    .replace(/[\r\n]{3,}/g,'\n')// 空白行は詰める
    .replace(/[\s=\-\*]{3,}[\r\n]+/g,'')// 空白又は水平線のみの行削除
    .split('\n').slice(0,5).join('\n')+'……',// 5行
  timestamp:w.getDate().toISOString(),// 送信時刻
}),
send=(w={payload_json:{username:'ぬるぽ',avatar_url:'https://mcbeeringi.github.io/img/icon!.png',content:'ガッ'}})=>// webhook送信
  cfg.webhooks.forEach(x=>UrlFetchApp.fetch(x,{method:'post',payload:{...w,payload_json:Utilities.jsonStringify(w.payload_json)},muteHttpExceptions:false})),
main=w=>(// Gmail→Discord
  w=GmailApp.search(`is:unread in:inbox -category:promotions newer_than:7d`),// 広告ではなさそうな1週間以内の未読メールを含むスレッドを取得
  w=w.flatMap(x=>x.getMessages().filter(y=>y.isUnread())).reduce((a,x)=>(// 未読スレッドを未読メッセージに展開 念の為全て
      a.payload_json.embeds.push(widget(x)),// メッセージからウィジェットを作成してメッセージに追加
      a.payload_json.content+=`- ${x.getSubject()}\n`+x.getAttachments().map((y,i)=>(// タイトルと添付ファイル参照を本文に追加
        i=a.payload_json.attachments.length,
        a.payload_json.attachments.push({id:i,description:`"${x.getSubject()}" attachment file.`,filename:y.getName()}),
        a[`files[${i}]`]=y,
        ` - attachment://${y.getName()}\n`
      )).join(''),
    a
  ),{payload_json:{content:w.length?'':'未読メールはありません',embeds:[],attachments:[],...cfg.template}}),
  Logger.log(w),
  send(w)// 送信
),
daily=_=>(// 指定時刻送信
  ScriptApp.getProjectTriggers().forEach(x=>x.getHandlerFunction()=='daily'&&ScriptApp.deleteTrigger(x)),// daily予約削除
  ScriptApp.newTrigger('daily').timeBased().at(new Date(new Date(Date.now()+864e5).toDateString()+' '+cfg.time)).create(),// 翌日の指定時刻にdaily実行予約
  main()// メイン動作実行
);
```
# 解説?
## cfg
各種設定項目を収めたオブジェクト
## crct crc
CRC32のテーブルと生成関数
bytesを受け取りNumberを返す

同じ文字列から同じランダムな数字が作れれば良かった
zip生成の時に作ったCRC32があったので引っ張ってきた

https://qiita.com/McbeEringi/items/5acc1f940ff7504f7e16
## widget
GmailMessageを受け取りdiscord embed形式に変換する関数
本文から余計な区切り線等を削除したうえで5行程度にまとめる
crcを使って色付け
## send
オブジェクトを受け取り送信する
送信形式を指定しないのがみそ
## main
GmailのAPIを叩いてwidgetに渡してメッセージ本体を整形
出来上がったらsendに渡す


`GmailApp.search(くえり)`で目当てのスレッドを取得
クエリについて
- `is:unread` 未読のメッセージを含むスレッド
- `in:inbox` 受信ボックスのスレッド
- `category:promotions` 広告判定のメッセージを含むスレッド `-`を付けて否定形で使用
- `newer_than:7d` 七日以内のメッセージを含むスレッド

スレッドのままでは扱いづらいのでここから未読メッセージを取得した後に
ウィジェット生成と添付ファイルの取得を取得、一つのreduceに詰め込んでいる

## daily
毎日実行される関数
今回のトリガーを削除した後に次回の実行時刻を計算してトリガーに追加
ここにmainを入れて一緒に毎日動かす

# はまりポイント
## 広告メールの除外
広告メールには配信解除のリンクが張ってあることが多い
`-unsubscribe -配信停止`でなんとか乗り切っていたが突破するメールもあった
久々にのぞいたら自動判別してくれる`category:promotions`ができていてこれに差し替えた
## 連続した改行の削除
`.replace(/\n{3,}/g,'\n')`ではうまく動かなかった
`.replace(/[\r\n]{3,}/g,'\n')`が動いたので内部ではすべてCRLFなのかと思われる
## String → Uint8Array ?
gasにはUint8Arrayがなくて代わりにbytesがある
基本的に同じように扱える
そしてString→bytesは一度Blobを経由する必要がある模様
`Utilities.newBlob(txt).getBytes()`
## ファイルの送信ができない?
fetch()で送信形式を指定しないと送れる
裏で動いている自動判別がいい感じに動いている?
詳しくはdiscord公式を参照すると書いてある
よくわからんけど動くからヨシ!