動機
団体で持っているGmailを誰も確認しないで放置する事案が発生したので
GoogleCalenderと同じようにDiscordに流すことにした
前記事
- 毎日指定時刻に未読メッセージを送信
- 以下は除外
- 広告メール
- 古い未読メール
- ウィジェットで本文もプレビュー
- 同一の送信者が一目でわかるように色付け
- メール本体に飛べるリンク
コード全文
基本構造は前のcalenderと同じ
daily()を走らせると今後も実行される
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があったので引っ張ってきた
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公式を参照すると書いてある
よくわからんけど動くからヨシ!