January 31, 2021

GASを使ってGmailにきた予定をGoogle Calendarに自動で登録する

eyecatch

最近は在宅勤務なのでミーティングの時間となるべく被らないように 宅配便の配達時間指定をすることが多いのですが、せっかく時間指定しても忘れていてミーティングが被ることがあります。
他にも予約したお店とかの情報が勝手にGoogleカレンダーに追加されたら嬉しいなって思うことが多かったので、Gmailにきたメールをパースしてカレンダーに追加する方法を紹介したいと思います。

実はGmaliは一部のメールに関してはデフォルトでGoogleカレンダーに追加する機能が存在しています。
しかし、対応しているのは飛行機とか世界共通のものが多く日本のサービスは対応していないことが多いです。

flight

この機能を有効にするには、GmailではなくGoogleカレンダー側から設定する必要があります。

calendar settings

これだけだと特定のメールをGoogleカレンダーに登録できないため、 自分でなにかしらして上げる必要があります。

今回はGoogle App Script(GAS)を使ってGmailに届いたメールをパースしてGoogleカレンダーに追加するスクリプトを書きました。

このブログでは自分が良く使っているサービスを例に

  • ヤマト運輸
  • 日本郵便
  • ぐるなび

を例に上げています。
(佐川は最近サービスが変わって変更されたので後日追加予定)
GitHubに公開しているので、みんなのサービス毎のパーサーのプルリクをくれたら喜びます!

GitHub - AAkira/gmail-to-calendar

Contribute to AAkira/gmail-to-calendar development by creating an account on GitHub.

ポイント

メールを受信したら都度GASを実行したいところですが、GASの自動実行のトリガには時間かカレンダーしか設定できません。
そのため工夫をする必要があります。

今回はGmailのスター機能を使って、トリガはn時間毎に動かし、処理が終わったメールはスターをつけて処理済みのフラグとすることで重複処理を防ぎます。

Gmailの仕組み(スレッド)


const QUERY_YAMATO = 'subject(受け取り日時変更依頼受付完了のお知らせ)';

function main() {
  const mail = getMail(QUERY_YAMATO);
}

function getMail(query) {
  const threads = GmailApp.search(query, 0, 5); // queryに一致する0-5件目を取得
  return GmailApp.getMessagesForThreads(threads);
}

Gmailを取得するGASはこのようになっています。
queryの部分にはGmailの検索と同じワードを指定します。

このコードを理解するにはGmailのスレッドの概念を理解する必要があります。

gmail thread

Gmailには同じ件名や内容を自動的にスレッドにしてくれる機能があるので上の画像みたいに2通目のメールは4件のメールが1つのスレッドとしてまとめられます。
GASではその結果が2次元配列として返ってきます。

messages[0][0] => Material Gallery
messages[1][0] => GitHubの1通目
messages[1][1] => GitHubの2通目
messages[1][2] => GitHubの3通目
messages[1][3] => GitHubの4通目
messages[2][0] => Stripe
messages[3][0] => LAPRAS

実際のコード

仕組みがわかればあとは正規表現を使ってパースしていきます。

Gmail取得部分

便宜上main関数を最初の呼び出し関数として定義しています。(GASの仕組み上は不要)
パースする対象のメールが増えたらここに足していくと良いと思います。


const QUERY_YAMATO = 'subject:(受け取り日時変更依頼受付完了のお知らせ) ';
const QUERY_GNAVI = 'subject:([ぐるなび]予約が確定しました) ';
const QUERY_YUBIN = 'subject:(日本郵便】受付完了のお知らせ)'

function main() {
  pickUpMessage(QUERY_YAMATO, function (message) {
    parseYamato(message);
  });
  pickUpMessage(QUERY_GNAVI, function (message) {
     parseGnavi(message);
  });
  pickUpMessage(QUERY_YUBIN, function (message) {
    parseYubin(message);
  });
}

function pickUpMessage(query, callback) {
  const messages = getMail(query);

  for (var i in messages) {
    for (var j in messages[i]) {
      const message = messages[i][j];
      // starは処理済みとする
      if (message.isStarred()) break;

      callback(message);

      message.star()
    }
  }
}

function getMail(query) {
  var threads = GmailApp.search(query, 0, 5);
  return GmailApp.getMessagesForThreads(threads);
}

Googleカレンダー登録

CalendarApp.getDefaultCalendar() で同じGoogleアカウントのカレンダーを取得して、 createEventしてあげるだけです。
descriptionやlocationも登録できます。
予約した飲食店の場所を登録すると便利そうですね。

function createEvent(title, description, location, year, month, dayOfMonth,
  startTimeHour, startTimeMinutes, endTimeHour, endTimeMinutes) {

  const calendar = CalendarApp.getDefaultCalendar();
  const startTime = new Date(year, month - 1, dayOfMonth, startTimeHour, startTimeMinutes, 0);
  const endTime = new Date(year, month - 1, dayOfMonth, endTimeHour, endTimeMinutes, 0);
  const option = {
    description: description,
    location: location,
  }

  calendar.createEvent(title, startTime, endTime, option);
}

各サービスの登録

各サービスのパースは単純に正規表現を使います。

ヤマト運輸

// ヤマト運輸
function parseYamato(message) {
  const strDate = message.getDate();
  const strMessage = message.getPlainBody();

  const datePrefix = "■お受け取りご希望日時 : ";
  const regexp = RegExp(datePrefix + '.*', 'gi');

  const result = strMessage.match(regexp);
  if (result == null) {
    console.log("This message doesn't have info.");
    return;
  }
  const parsedDate = result[0].replace(datePrefix, '');

  const year = new Date().getFullYear();
  const month = parsedDate.match(/[0-9]{2}\//gi)[0].replace('/', '');
  const dayOfMonth = parsedDate.match(/\/[0-9]{2}/gi)[0].replace('/', '');
  const matchedStartTimeHour = parsedDate.match(/[0-9]{2}時から/gi);
  const matchedEndTimeHour = parsedDate.match(/[0-9]{2}時まで/gi);

  var startTimeHour;
  var endTimeHour;
  if (matchedStartTimeHour == null || matchedEndTimeHour == null) {
    startTimeHour = '9';
    endTimeHour = '12';
  } else {
    startTimeHour = matchedStartTimeHour[0].replace('時から', '');
    endTimeHour = matchedEndTimeHour[0].replace('時まで', '');
  }

  createEvent("ヤマト配達", "mailDate: " + strDate + "\n" + getEmailLink(message),
   "", year, month, dayOfMonth, startTimeHour, 0, endTimeHour, 0);
}

日本郵便

// 日本郵便
function parseYubin(message) {
  const strDate = message.getDate();
  const strMessage = message.getPlainBody();

  const datePrefix = "【お届け予定日】";
  const dateSuffix = "【お届け希望時間帯】";
  const timeSuffix = "【ご希望配達先】";
  var regexp = RegExp(datePrefix + '[\\s\\S]*?' + dateSuffix, 'gi');

  const dateResult = strMessage.match(regexp);

  regexp = RegExp(dateSuffix + '[\\s\\S]*?' + timeSuffix, 'gi');

  const timeResult = strMessage.match(regexp);

  if (dateResult == null || timeResult == null) {
    console.log("This message doesn't have info.");
    return;
  }
  const parsedDate = dateResult[0].replace(datePrefix, '').replace(dateSuffix, '');
  const parsedTime = timeResult[0].replace(dateSuffix, '').replace(timeSuffix, '');

  const year = new Date().getFullYear();
  const month = parsedDate.match(/[0-9]*月/gi)[0].replace('月', '');
  const dayOfMonth = parsedDate.match(/[0-9]*日/gi)[0].replace('日', '');
  const matchedStartTimeHour = parsedTime.match(/[0-9]{2}~/gi);
  const matchedEndTimeHour = parsedTime.match(/[0-9]{2}時/gi);

  var startTimeHour;
  var endTimeHour;
  if (matchedStartTimeHour == null || matchedEndTimeHour == null) {
    startTimeHour = '9';
    endTimeHour = '12';
  } else {
    startTimeHour = matchedStartTimeHour[0].replace('~', '');
    endTimeHour = matchedEndTimeHour[0].replace('時', '');
  }

  createEvent("郵便配達", "mailDate: " + strDate + "\n" + getEmailLink(message),
   "", year, month, dayOfMonth, startTimeHour, 0, endTimeHour, 0);
}

ぐるなび

// ぐるなび
function parseGnavi(message) {
  const strDate = message.getDate();
  const strMessage = message.getPlainBody()

  const suffix = "のご予約が確定しました。";
  const regexp = RegExp('.*' + suffix, 'gi');

  const result = strMessage.match(regexp);
  if (result == null) {
    console.log("This message doesn't have info.");
    return;
  }

  const baseStr = result[0].replace(suffix, '');

  const year = baseStr.match(/[0-9]{4}年/gi)[0].replace('年', '');
  const month = baseStr.match(/[0-9]{2}月/gi)[0].replace('月', '');
  const dayOfMonth = baseStr.match(/[0-9]{2}日/gi)[0].replace('日', '');
  const startTimeHour = baseStr.match(/[0-9]{2}時/gi)[0].replace('時', '');
  const startTimeMinute = baseStr.match(/[0-9]{2}分/gi)[0].replace('分', '');
  const title = baseStr.match(/「.*」/gi)[0].replace('「', '').replace('」', '');


  // 住所
  const addressPrefix = '- - - - - - - - - -\n';
  const addressRegexp = RegExp(addressPrefix + '.*', 'gi');
  const address = strMessage.match(addressRegexp)[2].replace(addressPrefix, '');

  createEvent(title, "mailDate: " + strDate + "\n" + getEmailLink(message), address,
    year, month, dayOfMonth, startTimeHour, startTimeMinute, startTimeHour, startTimeMinute);
}

トリガ設定

コードを書き終わったら、左の時計アイコンからトリガの設定を行います。
「時間主導型」にして任意の時間を設定してください。
今回の用途だと特にリアルタイム性が求められるわけではないので4時間ぐらいで丁度いい気がします。

trriger settings

これで自動化が完了です!

注意

タイムゾーン

カレンダー登録をする場合タイムゾーンがズレていると時間がズレてしまうので修正します。

timezone settings

権限

Gmailとカレンダーの権限も追加しておきましょう。

authority settings

まとめ

なんとなく便利そうなサービスをとりあえずパースしてみました。
一度設定しておけばメールが来るたびに勝手にカレンダーに登録してくれます!!🐕
よく使うサービスのパーサーを自分で追加していけば、もっと便利になると思います!

人がやる必要無いことは、どんどん自動化していきましょう!

© AAkira 2023