Gmailで届いた会議、研究会のスケジュールを、メール転送するだけでGoogleカレンダーに登録する

# Gmailで届いた会議、研究会のスケジュールを、メール転送するだけでGoogleカレンダーに登録する
## 1. やることのポイント
::: note info
特定のサブアドレスに届いたメールをGASで処理
:::
::: note info
AIを用いた日時の判定
:::
::: note info
Googleカレンダーに、スケジュール日時、メールの件名、Gmailへのリンクを登録
:::
![Qiita_メールからカレンダーへの自動登録.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253484/ec445dae-66c7-5d4d-e0b8-1a4fd2204880.png)

※ この記事は、[「仕事の依頼メールをGoogle ToDoリストへ自動登録」](https://qiita.com/amori/items/48aa5e0f4944d59d468b)と同じようなことを、GooleTasks(Google ToDoリスト) ではなく、Googleカレンダーに対して行うものです。
## 2. 背景
* スケジュールを忘れがち
　研究会とか会議とかの通知はメールで届く。しかし、仕事に集中していると、それらの開始時刻はだいたい思い出せない。うっかり欠席してしまう。
* Googleカレンダーに登録するにしても通知のメールの内容を全部コピペできない
　Googleカレンダーに登録するとき、日時、場所など、いちいち入力するのは致し方ない（かも）。ただ、カレンダーには、メールの内容が全て登録できるわけではないことは致命的だ。メールに添付ファイルがある時点でアウト。だから、メールのままアクセスできるといい。ところが、Gmailでは、特定のメールを探そうとしてもうまくいかないことが多い。すぐには見つからないのはストレスだ。
* 対策案
　そこで、メールをAIに判定させて、自動的に情報を引き出して、カレンダーに登録することを考えた。特定のサブアドレスにメールを送ると、OpenAIの（ChatGPTの）API を使って文面から時刻を含む日程を抽出、それを使ってGoogleカレンダーに登録させる。これをGASにやらせる。GASは、1分ごとに起動する。登録内容には、メールへのリンクが仕込んであるので、カレンダーからすぐにメールにアクセスできる！
 　登録されたカレンダーは、Amazon Alexa に喋らせる。
   これで解決！（となるといいな）。
## 3. 準備
　これらは、[ToDoリストに登録するときに書いたこと](https://qiita.com/amori/items/48aa5e0f4944d59d468b)とほぼ同じ。
### 3-1. OpenAIのAPI
* 機密保持：OpenAI のAPIでは、機密は保たれることになっている
* 登録：OpenAIのAPIは、ChatGPTとは別に課金されるので、チャージしておく
* モデル：gpt-3.5-turbo を使う。
### 3-2. メールのサブアドレス
　今回は、hoge@example.com に対して、hoge+cal@example.com を使う。このアドレスに届いたメールだけを処理する。
### 3-3. Googleスプレッドシート
　あらかじめ、Googleスプレッドシートを作っておき、データが蓄積できるようにしておく。1行目まで入力しておく。IDを調べておく。
 ![Qiita_メールからカレンダーへの自動登録_Sheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253484/a18b74c8-2a37-74d3-c230-986891ff8f01.png)

## 4. プログラム
### 4-1. メールから情報を抽出してスプレッドシートに書き込む
　スタンドアロンのGASを毎分起動して、特定のサブアドレスに届いたメールから情報を抽出して、スプレッドシートに書き込む。時刻の読み取りはAIにやらせる。ポイントは、だいたい次の通り。
 * ChatGPT3 は現実世界の今日の日付を答えられないので、メールのヘッダから日付を抽出して与える。
 * Gmailへのリンクは、RFC822の Message ID を利用。送信者・同報で送られた人も同じURLで利用できる。
 * GASはスタンドアロンで、トリガーで毎分起動するように設定する。
 * [ToDoリストへ自動登録](https://qiita.com/amori/items/48aa5e0f4944d59d468b)でアイデアだけ書いた「転送されたメールなら、スレッドの一つ前のメールを処理する」を実現している。

``` javascript:メールを受信したら件名とスケジュールを書き出す
// サブアドレスに送られてきた仕事依頼のメールの以下の情報をスプレッドシートに書き出す。
//  ・件名
//  ・会合などの予定
//  ・メッセージIDに基づくGmailの検索URL
// Ver.01 : 2024-02-08

// 左側の 「プロジェクトの設定」（歯車マーク）> スクリプト プロパティ で、次の2つを設定、入力する
// 1) スプレッドシートのID : SID
// 2) OpenAI の APIキー  : ChatGPTAPI
// 3) あなたのメールアドレス: EMailAddress

// 関数本体

function processEmails() {
  var spreadsheetId = PropertiesService.getScriptProperties().getProperty('SID');          // スクリプトプロパティによるシートのID
  var subaddress    = PropertiesService.getScriptProperties().getProperty('EMailAddress'); // スクリプトプロパティによるメールアドレス
  subaddress        = subaddress.replace('@', '+cal@');                                    // メールのサブアドレス hoge+cal@foo.com を設定。

  var sheet = SpreadsheetApp.openById(spreadsheetId).getActiveSheet();

  const ProcessedLabel = 'CalProcessed'; // 処理済みとわかるためのラベル名。任意。
  var processedLabel = GmailApp.getUserLabelByName(ProcessedLabel);
  if (!processedLabel) {                 // ラベルが存在しない場合は作成
    processedLabel = GmailApp.createLabel(ProcessedLabel);
  }

  var query = 'to:' + subaddress + ' -in:draft -label:' + ProcessedLabel; // 未送信(下書き)、処理済ラベルがついたものは対象としない。
  var threads = GmailApp.search(query);

  for (var i = 0; i < threads.length; i++) {
    var messages = threads[i].getMessages();
    var messageIndex = messages.length - 1; // デフォルトではスレッドの最後のメッセージを使用

    // スレッド内の最後のメッセージが 'Fwd:' で始まるかチェック
    if (messages[messageIndex].getSubject().startsWith('Fwd:') && messages.length > 1) {
      messageIndex--; // 'Fwd:' で始まる場合、一つ前のメッセージを使用
    }

    var message = messages[messageIndex]; // 対象のメッセージを取得

    if (!message.isInTrash()) {
      // メールから情報取得
      var subject      = message.getSubject(); 
      var dateHeader   = message.getDate();
      var mailurl      = genMailURL(message);
      // AI で期限を取得
      var dateHour     = getDeadLineWithOpenAI(message.getPlainBody().slice(0,999), dateHeader);
      var dateHourStart= dateHour.slice(6,24);
      var dateHourEnd  = dateHour.slice(30,46)

      // スプレッドシートに追記（４列のデータ）
      sheet.appendRow([
          new Date(),    // 登録日時
          '',            // シートのコンテナバインドGASの処理状況欄。最初は空欄。
          dateHourStart, // 開始日時
          dateHourEnd,   // 終了日時
          subject,       // メールの件名
          mailurl]);     // メールのスレッドへのURL（検索）

      threads[i].addLabel(processedLabel); // 処理済ラベルを付ける
    }
  }
}

// OpenAI APIを使用してメールから期限・期日を抽出する関数
// AI は、「今日」を認識しないので、メールから取り出したメールの日付を活用する。

function getDeadLineWithOpenAI(emailBody, emailDate) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('ChatGPTAPI'); // スクリプトプロパティによるAPIキー
  const apiUrl = 'https://api.openai.com/v1/chat/completions'; //ChatGPTのAPIのエンドポイントを設定

  //ChatGPTに投げるメッセージを定義(ユーザーロールの投稿文のみ)
  const messages = [{'role': 'user', 'content': 'メールの内容を分析し、会合、研究会、会議などの開始時刻と終了時刻を、2つの19バイトの文字列を空白を挟んで連結して答えなさい。具体的には、"Start=yyyy-MM-dd HH:MM:SS End=yyyy-MM-dd HH:MM:SS" の形式で答えよ。答えは、1バイト文字だけ使い、39バイト分だけで答え、日本語を含まない。数字記号以外を含まない。理由などの記述も省く。終了時刻がわからない場合には、開始時刻の2時間後とする。\
  なお、メールの書かれた日時は、' + emailDate + 'である。\
  また、メールの内容は次のとおりである。：\
' + emailBody + ' \
'}];
  //OpenAIのAPIリクエストに必要なヘッダー情報を設定
  const headers = {
    'Authorization':'Bearer '+ apiKey,
    'Content-type': 'application/json',
    'X-Slack-No-Retry': 1
  };

  //ChatGPTモデルやトークン上限、プロンプトをオプションに設定
  const options = {
    'muteHttpExceptions' : true,
    'headers': headers, 
    'method': 'POST',
    'payload': JSON.stringify({
      'model': 'gpt-3.5-turbo',
      'max_tokens' : 2048,
      'temperature' : 0.0,
      'messages': messages})
  };
  //OpenAIのChatGPTにAPIリクエストを送り、結果を変数に格納
  const response = JSON.parse(UrlFetchApp.fetch(apiUrl, options).getContentText());
  Logger.log(response);

  //ChatGPTのAPIレスポンスをログ出力
  Logger.log(response.choices[0].message.content);
  return response.choices[0].message.content; // メールから取り出した期限・期日
}

// メールへのURL生成
//
function genMailURL(message) {
  const  urlHeader = 'https://mail.google.com/mail/#search/in%3Aanywhere+';
  return urlHeader + encodeURIComponent('rfc822msgid:'+message.getHeader('Message-ID'));
}
```
### 4-2. スプレッドシートからカレンダーに登録
　スプレッドシートのコンテナバインドのGASを書く。
* カレンダーIDは、予め取得しておいて、スクリプトプロマティに仕込んでおく。
* カレンダーIDは、特に新たなカレンダーを作っていなければ、Googleのメールアドレスになる（要確認）。
* 他人のカレンダーの場合には、きちんと書き込める許可があることを確認する。
```javascript
function addEventFromSheet() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var rows  = sheet.getDataRange().getValues(); // シートの全データを取得
  
  var calendarId = PropertiesService.getScriptProperties().getProperty('calID');
  var calendar   = CalendarApp.getCalendarById(calendarId);
  
  // 2行目からデータを読み込む（1行目はヘッダーと仮定）
  for (var i = 1; i < rows.length; i++) {
    var row = rows[i];
    
    var status = row[1];              // 状態をチェックする列
    if (status !== '') continue;      // 既に処理された行はスキップ
    
    var startTime   = new Date(row[2]); // 開始時刻
    var endTime     = new Date(row[3]); // 終了時刻
    var title       = row[4];           // 件名
    var description = row[5];           // 説明（Message-ID による URL）
    
    // カレンダーにイベントを追加
    var event = calendar.createEvent(title, startTime, endTime, {description: description});
    
    // 処理した行にステータスを更新（例：'登録済み'）
    sheet.getRange(i + 1, 2).setValue('登録済み');
    
    Logger.log('Event created: ' + title);
  }
}
```
## 5. 活用・応用
* メールを転送してすぐに登録されるわけではない。２つのGASは、それぞれ毎分起動するので、2分程度を見込む必要がある。
* 共用のカレンダーにしておくと、利便性が高まるかもしれない。（みんなが勝手に登録して、同じイベントが複数登録されてしまうこともありうる。）
* 前も書いたが、料金は安い。秘書さんに予定を管理してもらうことを考えると、利便性は高いと思う。
* 間違いをしそうなので、送り込む文面の作り込みとか、まだまだ工夫の余地はありそう。
* APIでAIが使えると、応用範囲が広い。他にもいろいろできそう。