【GAS】Gmailの受信メールを検索して本文の内容からGoogleカレンダーへ登録する

Gmail宛に届いたメールから、日付や内容を取得してGoogleカレンダーにスケジュール登録するスクリプトです。

内容としましては、Gmailの未読の受信メールの中から条件に合うメールに絞り込み、正規表現で必要な情報を取得後、Googleカレンダーに予定を作成するという流れです。

正規表現で確実に内容を取得するため、フォーマットが決まっているお問い合わせフォームのようなものからのメールを想定しています。WordPressであればContact FormやBooking Packageを使うといいでしょう。

スポンサーリンク

プログラムの内容

まずメールが届くところから始まります。

これはWordPressのBooking Packageを使用し、件名は「予約フォームからの申し込み – 予約ID: *」となるように設定し、入力された氏名、日付、時刻が本文に記載されるようなフォーマットにしています。

そして、対象となるメールの条件は下記の通りになります。

  • 件名が「予約フォームからの申し込み」から始まる
  • 未読のメール

Gmailの検索バーで「予約フォームからの申し込み is:unread」と入れた時にヒットするメールというわけですね。

この状態でスクリプトが実行されると対象のメールを走査し、本文に記載された内容からGoogleカレンダーへ予定作成します。

本文に記載された日付と時刻に、予定の名前は氏名が入ります。

また、終了時刻は1時間後に設定し、説明文には予約IDが記載されます。

メールから取得し、Googleカレンダーへ登録された内容はスプレッドシートに履歴として入力されます。

スプレッドシートに記載されるのは以下の項目です。

  • 取得日 (スクリプトが実行され読み込まれた日)
  • 受信日
  • 予約ID
  • 氏名
  • 予約日
  • 予約時刻
  • Googleカレンダー予約ID

そして最終的に、対象となったメールは既読にされます。つまり、次の実行時にはヒットしなくなります。

プログラミング (GAS)

function myFunction() {
  const startTime = new Date();                                 // 処理開始日時、取得日
  const query = 'subject:予約フォームからの申し込み is:unread';   // 検索条件
  const start = 0;                                            // メールの走査開始番号
  const max = 100;                                            // メールの最大取得数
  const threads = GmailApp.search(query, start, max);         // 検索結果を取得
  const messagesForThreads = GmailApp.getMessagesForThreads(threads); // スレッドを取得
  const idReg = new RegExp('ID: \\d{1,}');                        // ID取得用の正規表現
  const idReplace = 'ID: ';                                       // IDの置換対象
  const dateReg = new RegExp('日付: \\d{4}/\\d{2}/\\d{2}');     // 日付取得用の正規表現
  const dateReplace = '日付: ';                                    // 日付の置換対象
  const timeReg = new RegExp('時刻: \\d{2}:\\d{2}');               // 時刻取得用の正規表現
  const timeReplace = '時刻: ';                                    // 時刻の置換対象
  const nameReg = new RegExp('氏名: .{1,}');                       // 氏名取得用の正規表現
  const nameReplace = '氏名: ';                                    // 氏名の置換対象
  const ss = SpreadsheetApp.getActiveSpreadsheet();               // スプレッドシート
  const sheet = SpreadsheetApp.getActiveSheet();                  // 対象のシート
  const myCalendar = CalendarApp.getCalendarById('*@gmail.com');  // Googleカレンダー
  const keta4Reg = new RegExp('\\d{4}');                          // 西暦取得用の正規表現
  const keta2Reg = new RegExp('\\d{2}', 'g');                     // 月・日・時・分取得用の正規表現

  // 検索にヒットしたメールの数だけ繰り返す
  for(messages of messagesForThreads){
    let msg = messages[0];          // メッセージを取得
    let subject = msg.getSubject(); // 件名
    let body = msg.getPlainBody();  // 本文
    let sentDate = msg.getDate();   // 受信日
    
    // 本文から必要なテキストを取得し、不要なテキストは空白に置換
    let id = body.match(idReg);               // 予約ID
    id = id[0].replace(idReplace, '');
    let date = body.match(dateReg);           // 日付
    date = date[0].replace(dateReplace, '');
    let time = body.match(timeReg);           // 時刻
    time = time[0].replace(timeReplace, '');
    let name = body.match(nameReg);           // 氏名
    name = name[0].replace(nameReplace, '');
    
    // シートの最終行を取得し情報を入力
    let row = sheet.getLastRow() + 1;
    sheet.getRange(row, 1).setValue(today);     // 取得日
    sheet.getRange(row, 2).setValue(sentDate);  // 受信日
    sheet.getRange(row, 3).setValue(id);        // 予約ID
    sheet.getRange(row, 4).setValue(name);      // 氏名
    sheet.getRange(row, 5).setValue(date);      // 日付
    sheet.getRange(row, 6).setValue(time);      // 時刻

    // 日付と時刻からGoogleカレンダーに登録
    let setYear = date.match(keta4Reg);     // 西暦を取得
    setYear = Number(setYear[0]);           // 配列に格納されるため0番目を取得
    date = date.replace(setYear + '/', '');   // 西暦部分を空白に置換
    let monthDay = date.match(keta2Reg);    // 月・日を取得
    setMonth = Number(monthDay[0]) - 1;     // 月 (0~11で指定)
    setDay = Number(monthDay[1]);           // 日
    let hourMinute = time.match(keta2Reg);  // 時・分を取得
    setHour = Number(hourMinute[0]);        // 時
    setMinute = Number(hourMinute[1]);      // 分
    
    // Date型の変数に開始日時と終了日時を格納
    let startDate = new Date(setYear, setMonth, setDay, setHour, setMinute);
    let endDate = new Date(setYear, setMonth, setDay, setHour + 1, setMinute);  // 終了日時は1時間後に設定
    
    let eventId = myCalendar.createEvent(name, startDate, endDate).getId();   // イベントを作成しIDを取得
    let event = myCalendar.getEventById(eventId);                             // イベントIDからイベントを取得
    event.setDescription(id);                                                 // メモに予約IDを入力
    sheet.getRange(row, 7).setValue(eventId);                                 // シートにイベントIDを入力

    // メールを既読にする
    msg.markRead();

    // 処理時間が5分を超えたら処理中止
    if (isTimeout(startTime)) {
      return;
    }
  }
}

function isTimeout(startTime) {
  let diff = parseInt((new Date() - startTime) / (1000 * 60));
  if (diff >= 5) {
    return true;
  }
  return false;
}

条件に合ったメールを検索して取得

まずはメールを取得する必要があります。

const query = 'subject:予約フォームからの申し込み is:unread';   // 検索条件
const start = 0;                                            // メールの走査開始番号
const max = 100;                                            // メールの最大取得数
const threads = GmailApp.search(query, start, max);         // 検索結果を取得
const messagesForThreads = GmailApp.getMessagesForThreads(threads); // スレッドを取得

GmailApp.getMessagesForThreads()でメールを取得し、その引数に条件を入れます。

その条件はthreadsという変数に格納してあり、GmailApp.search(検索条件, 開始番号, 最大取得数)という形で指定します。

検索条件について、今回はメールの件名で検索していますが、件名に限定しない場合は「subject:」を取り除けばOKです。「is:unread」は未読メールの絞り込みです。

開始番号は0スタートです。1にすると最初のメールが取得できません。最大取得数はどれだけ指定してもいいのですが、取得件数は1日20000件までで、スクリプトの実行時間は6分までという縛りがあるので注意が必要です。

取得したメールを1件ずつ処理

検索にヒットし、取得したメールを1件ずつ処理します。ループ処理です。

// 検索にヒットしたメールの数だけ繰り返す
for(messages of messagesForThreads){
  let msg = messages[0];          // メッセージを取得
  let subject = msg.getSubject(); // 件名
  let body = msg.getPlainBody();  // 本文
  let sentDate = msg.getDate();   // 受信日
  // 中略
  msg.markRead();  // メールを既読にする
}

まず変数msgにmessages[0]を格納していますが、0番目を指定しているのは、ここで取得しているメール1件分ではなくスレッドとしてひとまとまりになっているからです。

今回はスレッドがあろうがなかろうが、フォームから送られてきた最初のメールさえ取れればいいのでこのような形にしています。

あとは関数を使って件名、本文、受信日を取得していきます。ループの最後にはメールを既読にして次回の実行時に取得しないようにします。

正規表現

正規表現は要件に応じて柔軟に対応できるようにしなくてはいけません。僕は結構苦手です。

日付を取得する部分を例に説明します。

const dateReg = new RegExp('日付: \\d{4}/\\d{2}/\\d{2}'); 
const dateReplace = '日付: '; 
let date = body.match(dateReg);
date = date[0].replace(dateReplace, '');

1行目が正規表現の条件を記述している部分です。「日付: {半角数字4桁}/{半角数字2桁}/{半角数字2桁}」というルールです。

今回使用したBooking Packageは、2022年3月1日は「2022/03/01」と表記するルールなのでこれで取得できます。もし「2022/3/1」であれば「\\d{1,2}条件を変える必要があります。

ここで注意が必要なのは、「\d」と書く場合、「\\d」とエスケープさせないといけません。

1行目で書いた条件を、3行目のmatch関数の引数の中で「/」で囲んで書くという方法もありますが、match関数に変数を入れて行うためにこの方法をとっています。

このままだと「日付: 」も一緒に取得してしまうのであとから置換します。

Googleカレンダーへ登録

let setYear = date.match(keta4Reg);     // 西暦を取得
setYear = Number(setYear[0]);           // 配列に格納されるため0番目を取得
date = date.replace(setYear + '/', '');   // 西暦部分を空白に置換
let monthDay = date.match(keta2Reg);    // 月・日を取得
setMonth = Number(monthDay[0]) - 1;     // 月 (0~11で指定)
setDay = Number(monthDay[1]);           // 日
let hourMinute = time.match(keta2Reg);  // 時・分を取得
setHour = Number(hourMinute[0]);        // 時
setMinute = Number(hourMinute[1]);      // 分
    
// Date型の変数に開始日時と終了日時を格納
let startDate = new Date(setYear, setMonth, setDay, setHour, setMinute);
let endDate = new Date(setYear, setMonth, setDay, setHour + 1, setMinute);  // 終了日時は1時間後に設定
    
let eventId = myCalendar.createEvent(name, startDate, endDate).getId();   // イベントを作成しIDを取得
let event = myCalendar.getEventById(eventId);                             // イベントIDからイベントを取得
event.setDescription(id);                                                 // メモに予約IDを入力

日付は「yyyy/mm/dd」の形式で取得できていますが、これをDate関数でしっかり日付型にし直します。そのために正規表現を使って西暦・月・日と分解します。

分解したらDate関数に入れて開始日時と終了日時をつくります。終了日時は開始日時の1時間後に設定します。

もし30分後として、開始時間が10:30だった場合、ここは10:60とはならずにちゃんと11:00に時間が繰り上がってくれます。

あとはcreateEvent(タイトル, 開始日時, 終了日時)でGoogleカレンダーにイベントを作成し、setDescriptionでメモの項目に予約IDを入力します。

日本時間に変更

初期設定のままでは取得したものと全然違う時刻が入ってしまいます。

これはタイムゾーンが “America/New_York” に指定されているためで、これを “Asia/Tokyo” に変えてやる必要があります。

まずはプロジェクトの設定を開き、『「appsscript.json」マニフェスト ファイルをエディタで表示する』にチェックを入れます。

そしてエディタに戻ると「appsscript.json」が表示されているので、開いてtimeZoneの値を “Asia/Tokyo” に変更します。

{
  "timeZone": "Asia/Tokyo",  // 変更前: "America/New_York"
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

5分で一旦中止する

GASの実行時間は最長で6分であり、そこで強制的に中止されます。

中止されても再度実行すればいいだけですが、中途半端に終わってしまうと次回の実行時の不具合になりかねません。

というわけでひと通り処理が終わったところで、処理開始時間と現時点の時間を比較して5分が過ぎていれば処理を中止するようにしています。

function hoge() {
  // 処理時間が5分を超えたら処理中止
  if (isTimeout(startTime)) {
    return;
  }
}

function isTimeout(startTime) {
  let diff = parseInt((new Date() - startTime) / (1000 * 60));
  if (diff >= 5) {
    return true;
  }
  return false;
}

「startTime」は冒頭で「取得日」として取得しています。

コメント

コメントする前にお読みください

迷惑コメント防止のために初回のコメント投稿は承認制のため、投稿が反映されるまで少し時間がかかります。もちろん荒らしは承認しません。

教えて君やクレクレ君に対しては回答しませんのでご了承ください。