[GoogleAppsScript] タイムズカーシェアの予約をGmailからGoogleカレンダーに自動インポートする

# はじめに
Timesカーシェアを使い始めたのですが，予約日時を自動でカレンダーに反映させたいと思いました．
GoogleAppsScriptでできるのは知っていたのですが，ネットにサンプルのコードが落ちていませんでした．
なので他のサービスを対象として公開されている方のものを参考に，自作しました．

230712追記；ヤマト運輸のメールが新しくなっているようだったので、更新しました。（```QUERY_YAMATO_2```)

# 参考

- [DMM英会話の予定をGoogleカレンダーに自動登録する方法](https://designoma.com/dmm-eikaiwa-google-calendar/)
    - かつてこのコードでDMM英会話を登録していたことがあります．
    - コード実行のスケジューリングやサイトへのたどり着き方などがわかりやすく紹介されています．
- [GASを使ってGmailにきた予定をGoogle Calendarに自動で登録する](https://aakira.app/blog/2021/01/gas-gmail-to-calendar/)
    - こちらのサイトの[コード](https://github.com/AAkira/gmail-to-calendar)がもとになっています．
    - ヤマト運輸・ぐるなび・郵便局・ホットペッパービューティーのものは便利なのでそのまま残しています．
- [gmailからカレンダーに予定を自動生成、削除する](https://zenn.dev/yoshi65/articles/gas_mail_calendar)
    - こちらは上記のコードの勉強用に参照しました．
- [正規表現 メタ文字一覧](https://www.megasoft.co.jp/mifes/seiki/meta.html)
    - 正規表現をつかったことがなかったので，ここを参照しました．


# コード

上記のAAbrain様のコードにタイムズカーシェアの部分を追記し，動作確認を済ませました．
以下のコードを自分のGoogleAppsScriptのプロジェクト内のコード.gsにそのまま上書きしてください．
デフォルトの`myFunction()`から動くようにしてあります．

```コード.js
const QUERY_YAMATO = 'subject:(受け取り日時変更依頼受付完了のお知らせ) ';
const QUERY_YAMATO_2 = 'subject:(お届け方法変更の依頼受付完了のお知らせ) ';
const QUERY_GNAVI = 'subject:(［ぐるなび］予約が確定しました) ';
const QUERY_YUBIN = 'subject:(【日本郵便】受付完了のお知らせ)';
const QUERY_HOTPEPPER_BEAUTY = 'subject:ご予約が確定いたしました from:reserve@beauty.hotpepper.jp';
const QUERY_TIMESCAR = 'subject:【Times CAR】予約登録完了 from:inquiry@share.timescar.jp';

function main() {
  pickUpMessage(QUERY_YAMATO, function (message) {
    parseYamato(message);
  });
  pickUpMessage(QUERY_YAMATO_2, function (message) {
    parseYamato2(message);
  });
  pickUpMessage(QUERY_GNAVI, function (message) {
    parseGnavi(message);
  });
  pickUpMessage(QUERY_YUBIN, function (message) {
    parseYubin(message);
  });
  pickUpMessage(QUERY_HOTPEPPER_BEAUTY, function (message) {
    parseHotpepperBeauty(message);
  });
  pickUpMessage(QUERY_TIMESCAR, function (message) {
    parseTimesCar(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);
}

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,
  }

  console.log("start time: " + startTime);
  console.log("end time: " + endTime);
  calendar.createEvent(title, startTime, endTime, option);
}


function createEvent_overnight(title, description, location, year, month, dayOfMonth,
  startTimeHour, startTimeMinutes, endyear, endmonth, enddayOfMonth, endTimeHour, endTimeMinutes) {

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

  console.log("start time: " + startTime);
  console.log("end time: " + endTime);
  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,
    "", 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,
    "", 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, address,
    year, month, dayOfMonth, startTimeHour, startTimeMinute, startTimeHour, startTimeMinute);
}

// Hotpepper beauty
function parseHotpepperBeauty(message) {
  const strMessage = message.getPlainBody();

  const salonPrefix = "■サロン名";
  const regexpSalon = RegExp(salonPrefix + '[\\s\\S]*?' + '.+', 'gi');
  const salonName = strMessage.match(regexpSalon)[0]
    .replace(salonPrefix, '')
    .trim();

  const descriptionPrefix = "■メニュー";
  const couponPrefix = "■ご利用クーポン";
  const regexpDescription = RegExp(descriptionPrefix + '[\\s\\S]*?' + '(.+|[\\s\\S]*?)' + couponPrefix, 'gi');
  const description = strMessage.match(regexpDescription)[0]
    .replace(descriptionPrefix, '')
    .replace(couponPrefix, '')
    .trim();

  const locationPrefix = "※上の地図ページが開かない場合こちらの地図をご参照ください";
  const regexpLocation = RegExp(locationPrefix + '[\\s\\S]*?' + '.+', 'gi');
  const location = strMessage.match(regexpLocation)[0]
    .replace(locationPrefix, '')
    .trim();

  const datePrefix = "■来店日時";
  const dateRegex = "[0-9]{4}年(0[1-9]|1[0-2])月(0[1-9]|[12][0-9]|3[01])日（.）([01][0-9]|2[0-3]):[0-5][0-9]";
  const regexpDate = RegExp(datePrefix + '[\\s\\S]*?' + dateRegex, 'gi');
  const date = strMessage.match(regexpDate)[0]
    .replace(datePrefix, '')
    .trim();

  const year = date.match(/[0-9]{4}年/gi)[0].replace('年', '');
  const month = date.match(/(0[1-9]|1[0-2])月/gi)[0].replace('月', '');
  const dayOfMonth = date.match(/(0[1-9]|[12][0-9]|3[01])日/gi)[0].replace('日', '');
  const startTimeHour = date.match(/([01][0-9]|2[0-3]):/gi)[0].replace(':', '');
  const startTimeMinute = date.match(/:[0-5][0-9]/gi)[0].replace(':', '');

  createEvent(salonName, description,
    location, year, month, dayOfMonth, startTimeHour, startTimeMinute, parseInt(startTimeHour) + 1, startTimeMinute);
}

// ------------------- 追加部分 ---------------------------------
// TimesCarシェア
function parseTimesCar(message) {
  const strMessage = message.getPlainBody();

  const descriptionPrefix = "■車両";
  const planPrefix = "■料金プラン";
  const regexpDescription = RegExp(descriptionPrefix + '[\\s\\S]*?' + '(.+|[\\s\\S]*?)' + planPrefix, 'gi');
  const description = strMessage.match(regexpDescription)[0]
    .replace(descriptionPrefix, '')
    .replace(planPrefix, '')
    .trim();

  const locationPrefix = "■ステーション";
  const regexpLocation = RegExp(locationPrefix + '[\\s\\S]*?' + '.+', 'gi');
  const location = strMessage.match(regexpLocation)[0]
    .replace(locationPrefix, '')
    .trim();

  const datePrefix = "■利用開始日時";
  const dateRegex = "[0-9]{4}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01]).([01][0-9]|2[0-3]):[0-5][0-9]";
  const regexpDate = RegExp(datePrefix + '[\\s\\S]*?' + dateRegex, 'gi');
  const date = strMessage.match(regexpDate)[0]
    .replace(datePrefix, '')
    .trim();

  const year = date.match(/[0-9]{4}\//gi)[0].replace('\/', '');
  const month = date.match(/\/(0[1-9]|1[0-2])\//gi)[0].replace('\/', '').replace('\/', '');
  const dayOfMonth = date.match(/\/[0-3][0-9]\s/gi)[0].replace('\/', '').replace('\s', '');
  const startTimeHour = date.match(/([0-2][0-9]):/gi)[0].replace(':', '');
  const startTimeMinute = date.match(/:[0-5][0-9]/gi)[0].replace(':', '');

  const returndatePrefix = "■返却予定日時";
  const returndateRegex = "[0-9]{4}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01]).([01][0-9]|2[0-3]):[0-5][0-9]";
  const returnregexpDate = RegExp(returndatePrefix + '[\\s\\S]*?' + returndateRegex, 'gi');
  const returndate = strMessage.match(returnregexpDate)[0]
    .replace(returndatePrefix, '')
    .trim();

  const returnyear = returndate.match(/[0-9]{4}\//gi)[0].replace('\/', '');
  const returnmonth = returndate.match(/\/(0[1-9]|1[0-2])\//gi)[0].replace('\/', '').replace('\/', '');
  const returndayOfMonth = returndate.match(/\/[0-3][0-9]\s/gi)[0].replace('\/', '').replace('\s', '');
  const returnstartTimeHour = returndate.match(/([0-2][0-9]):/gi)[0].replace(':', '');
  const returnstartTimeMinute = returndate.match(/:[0-5][0-9]/gi)[0].replace(':', '');

  createEvent_overnight("TimesCar予約", description,
    location, year, month, dayOfMonth, startTimeHour, startTimeMinute, returnyear, returnmonth, returndayOfMonth, returnstartTimeHour, returnstartTimeMinute);

// ヤマト運輸2
function parseYamato2(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]*\//gi)[0].replace('/', '');
  const dayOfMonth = parsedDate.match(/\/[0-9]*/gi)[0].replace('/', '');
  const matchedStartTimeHour = parsedDate.match(/[0-9]*時～/gi);
  const matchedEndTimeHour = parsedDate.match(/～[0-9]*時/gi);

  var startTimeHour;
  var endTimeHour;
  if (matchedStartTimeHour == null || matchedEndTimeHour == null) {
    startTimeHour = '9';
    endTimeHour = '12';
  } else {
    startTimeHour = matchedStartTimeHour[0].replace('時～', '');
    endTimeHour = matchedEndTimeHour[0].replace('時', '').replace('～', '');
  }
  // console.log(year);
  // console.log(month);
  // console.log(dayOfMonth);
  // console.log(startTimeHour);
  // console.log(endTimeHour);

  const sendnumprefix = "送り状番号";
  var regexp_num = RegExp(sendnumprefix + '.*', 'gi');
  const result_num = strMessage.match(regexp_num);
  if (result_num == null) {
    console.log("This message doesn't have sending number.");
    return;
  }
  const description = result_num[0];

  createEvent_overnight("ヤマト配達", description,
    "", year, month, dayOfMonth, startTimeHour, 0, year, month, dayOfMonth, endTimeHour, 0);
    
}

// ------------------- 追加部分 ---------------------------------
}

function myFunction() {
  main()
}

```

# 読み取り対象のメール

ーーーー　様

この度はTimes CARをご利用いただき、ありがとうございます。
以下の内容で予約を受付けましたので、ご確認ください。

┏━━■予約内容■━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

　※予約内容を変更したい方はこちら
　　https://share.timescar.jp/view/reserve/list.jsp

　■予約番号
　　xxxxxxxxx
　■ステーション
　　"ここにステーション名"
　　"ここにステーションURL"
　■車両
　　"ここに車両名＆ナンバー"
　■料金プラン
　　"ここに料金プラン"
　■利用開始日時
　　YYYY/MM/DD HH:MM
　※ご利用の際は、必ず会員カードをお持ちください
　■返却予定日時
　　YYYY/MM/DD HH:MM

以下省略・・・


-----------------------------------------------------------