JavaScript
GoogleCalendar
GoogleAppsScript
gmail
自動化

メールで送られてくる予定表をGoogleカレンダーに自動登録する


問題

上司からこんな感じのメールが送られてくる。毎週。

スクリーンショット 2019-03-14 22.44.03.png

一週間分の予定表である。

Googleカレンダーを共有すれば手っ取り早いが、提案しても却下される。

仕方ないのでいつも手動でGoogleカレンダーに登録していた。

めんどい。


解決策

Google Apps Script (GAS)を使う。

GASとはなにか。


  • JavaScriptライクなスクリプト言語。

  • サーバーサイドで動く。

  • Googleアカウントさえあれば使える。

  • 全部ブラウザ上で設定できる。

  • Googleアカウントのデータ(GmailやGoogleカレンダー)に簡単にアクセスできる。

GASを使って、メールに書かれた予定をカレンダーに自動登録できたらいいな。というわけである。

似たようなことをやってる人はたぶん世界中にいっぱいいるだろう。

実際、Qiitaにも以下のような記事が投稿されていた。

参考:ヨガレッスンの予約完了メールから自動でカレンダー作成してみる

助かる。この記事を下敷きにしつつ、自前のスクリプトを組んだ。

違いがどこにあるかと言うと、

@nakaaza-jimpiさんの記事は

一つのメール ⇒ 一つの予定

であるのに対し、自分のやりたいことは

一つのメール ⇒ 複数の予定

である。

あまり大きな違いではないが、正規表現の練習にはちょうどよい。


Google Apps Scriptの作成

Googleドライブ ⇒ 新規 ⇒ その他 ⇒ Google Apps Script

スクリーンショット 2019-03-12 14.35.14.png

出来上がったものがこちら。

スクリーンショット 2019-03-12 14.35.47.png

(メールアドレス隠すのを忘れて投稿するところだった。危ない危ない)


プログラムの概説


ソースコードの構造(コールグラフ)

関数コールツリー.png

伝われ。


mail2calender:メインルーチン


mail2calender.gs

//Gmailに届いたメールを元に、一週間の予定をカレンダーに追加。

function mail2calender() {
var myMessages = getMessages('from:(xxx@xxx.com) subject:(next week schedule)');
for(var i=0; i < myMessages[0].length; i++){
var message = myMessages[0][i]
var sendTime = message.getDate();
var now = new Date();
if(sendTime > now.setHours(now.getHours() - 1)){
setWeekSchedule(message);
}
}
}

getMessagesで検索キーを元にGmailからメールを取得し、内容をsetWeekScheduleに投げる。

今のところ、差出人と件名でスケジュールの書かれたメールを特定している。


サブルーチンの解説


getMessages:Gmailからメッセージの取得


getMessages.gs

//検索キーを元にGmailのスレッドから全メッセージを取得。

function getMessages(keyword) {
var myThreads = GmailApp.search(keyword,0,1);
var myMessages = GmailApp.getMessagesForThreads(myThreads);
return myMessages;
}

この辺は@nakaaza-jimpiさんの書いたものまんま。

自分の場合は基本的に1スレッド1メールなので特に問題はない。


setWeekSchedule:一週間分のスケジュールを設定


setWeekSchedule.gs

//一週間のスケジュールを設定。

function setWeekSchedule(message) {
//プレーンテキストで取得して、行(日)に分割
var body = message.getPlainBody();
var textArray = body.split(/\r\n|\r|\n/);

//スラッシュとコロンを含んでいる行(予定のある日)だけスケジュールを設定する
for(var i=0; i < textArray.length; i++){
var line = textArray[i];
if(line.match(/\//) && line.match(/:/)){
setDaySchedule(line);
}
}
}


メールの本文を行ごとに分解してsetDayScheduleに渡す(一行には一日分の予定しか書かれないので)。

正規表現リテラルの中でスラッシュ/を文字として認識させるためエスケープ\/している。見にくい。

シェルスクリプトのsedと違って、スラッシュ以外はデリミタとして認められないらしい。残念。

参考:Javascript での正規表現の記述方法


setWeekSchedule:一日分のスケジュールを設定


setDaySchedule.gs

//一日のスケジュールを設定。

function setDaySchedule(line) {
//日付(例:"3/12")の取得。グループ化を使う。プログラミングの世界では月だけ0~11。
var monthdate = line.match(/([0-9]+)\/([0-9]+)(.+)/);
var month = monthdate[1] - 1;
var date = monthdate[2];

//予定の月が現在の月よりも小さいなら、それは来年の予定
var today = new Date();
var year = today.getFullYear();
if(month < today.getMonth()){
year++;
}

//一日内の各予定(例:"13:00 ミーティング")をカレンダーに登録。gはグローバルサーチ
var schedules = data[3].match( /[0-9]+:[0-9]+[^0-9]+/g );
for(var j=0; j < schedules.length; j++){
setSchedule(year, month, date, schedules[j]);
}
}


行の文頭から日付を取得している。monthを-1している理由は下記参照。

参考:Why does the month argument range from 0 to 11 in JavaScript's Date constructor?

また、年末には来年の予定のメールが送られてくるので、それに対処するif文を途中に設けてある。

最後に、その日の予定が書かれた文字列(行の残り)をsetScheduleに渡している。

一行に複数の予定が書かれている場合があるため、文字列を分割してforループ内で一つずつ渡す。

日付も一緒に渡しておく。


setSchedule:一つのスケジュールを設定


setSchedule.gs

//日付と一つの予定(例:"13:00 ミーティング")からスケジュールを作成。

function setSchedule(year, month, date, rawschedule) {
var schedule = rawschedule.match( /([0-9]+):([0-9]+) ([^0-9]+)/ );
var hours = schedule[1];
var minutes = schedule[2];
var scheduleDateTime = new Date(year, month, date, hours, minutes, 00);
var scheduleName = schedule[3].replace(/[,、]/g,"");
var scheduleData = [scheduleDateTime, scheduleName];
setCalendar(scheduleData);
}

ここまで来ると"13:00 ミーティング"のような文字列が渡されているはずなので、(時間):(分) (予定の名前)として認識し、日付と合わせてスケジュールのデータとしてsetCalendarに渡す。


setCalendar:Googleカレンダーへの登録


setCalendar.gs

//スケジュール情報を元にカレンダーイベントを作成

function setCalendar(scheduleData) {
var scheduleDateTime = scheduleData[0]; //終了時刻
var title = scheduleData[1];
var startTime = new Date(scheduleDateTime);
var endTime = new Date(scheduleDateTime.setHours(scheduleDateTime.getHours() + 1)); //終了時刻 = 開始時刻の1時間後
var myCalendar = CalendarApp.getDefaultCalendar();
myCalendar.createEvent(title, startTime, endTime)
}

スケジュールのデータを元に、Googleカレンダーを設定する。

@nakaaza-jimpiさんのとほぼ同じ。


Google Apps Scriptのトリガー(起動条件)の設定

出来上がったスクリプトの実行条件をどこで設定できるかというと、ここ。

スクリーンショット 2019-03-12 14.35.47-2.png

この赤い丸で囲った時計マーク。

(右隣の▶は実行ボタンなので、今すぐ実行したいときはこれを押す。)

この時計マークをクリックすると、

スクリーンショット 2019-03-12 14.44.51.png

こんな感じの画面が出る。起動する時間間隔を簡単に設定できる。

ちなみに初回実行時はいろんな警告が出る(Googleアカウントのデータにアクセスするため)。


結果

スクリーンショット 2019-03-14 22.42.38.png

上手く設定できた。


オチ

手打ちで送られてくるメールなので、ときどき件名がscheduleじゃなくてscheduelと誤字っていたりする。

スクリーンショット 2019-03-14 23.05.54.png

何と戦うんだ。

また、スケジュールを訂正するメールが後で送られてくるときもある。その場合の件名はかなり不規則。

この辺の課題も何かしらの工夫で解決できればよいが……

上司を説得する方が先決かもしれない。