仕事の依頼メールをAIを使ってGoogle TODOリストへ自動登録する

# 仕事の依頼メールをGoogle ToDoリストへ自動登録
## 1. やることのポイント
:::note info
特定のサブアドレスに届いたメールをGASで処理
:::
:::note info
AIを使った期限の読み取り
:::
:::note info
Google ToDoリストに、上記の期限、メールの件名、メールへのリンクを登録
:::
![Qiita_メールからToDoリストへの自動処理.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253484/97a8f953-8e3b-24fe-e45d-f48d1c75030b.png)

## 2. 背景（ぼやき）
* 現状認識
　私立大学の教員は、常に、１０ぐらいの仕事が同時に走っている。週に５〜６コマの授業が走っているだけでなく、ゼミ対応、卒研対応、経費精算、委員会の仕事、もし引き受けていれば外部の仕事、…。そのような状況で、期限を切られて仕事が新規に舞い込んでくるのは、結構なストレスだ。（読み直して「研究」が抜けていることに、あとから気づいた。）
  　しかも悪いことに、仕事の依頼はGmailで来る。もともとメールは、お手紙のようなコミュニケーション手段であったはずが、いつの間にか、情報収集手段、業務依頼の窓口になっている。コロナでそれに拍車がかかった。本来別の目的の連絡が全部、メールで届く。その上、最悪なことに、Gmailは「あいまい検索」になっていて、ちょっと間違って検索しても表示される＝余計なメールがいっぱい表示される仕様になっている。困った。
* 対策案
AIとGASを組み合わせて、ToDoリストへの自動登録を試みる。ToDoには RFC822 のMessage-ID を使ったメールへのリンクを仕込む。これによって、期日の書き間違い、メールと何かを同時に開く面倒、メールを再び検索する面倒、他の人と「どのメールか」を伝える面倒を回避する。
※ Googleカレンダーに対して同様のことをしたのが「[Gmailで届いた会議、研究会のスケジュールを、メール転送するだけでGoogleカレンダーに登録する](https://qiita.com/amori/items/06aea8dc342ad4f2b734)」の記事です。

## 3. 準備
### 3-1. Open AIのAPI
* 機密保持
OpenAIのAPIを使った場合には、データは学習には利用されない。もちろん、情報漏洩のリスクがないわけではないが、メールの処理をするには相対的には良い。そこで、OpenAIのAPIをGASで使って、メールから期日を抽出することにする。
* 登録
次に OpenAIのAPIを使うことについては、各所に掲載されているので、それを参照する。たとえば[OpenAI APIとは？使い方を実際の手順を紹介しながら解説](https://www.value-domain.com/media/openai/)など。私はいつの間にか初期のサービス分が尽きていたようなので10ドル入金した。
* モデルの種類
Open AI は、いろいろなモデルを供出している。(cf.[OpenAI API で提供されている モデル まとめ](https://note.com/npaka/n/n5d0a76b149f1)) ここではgpt-3.5-turboを使う。
### 3-2. サブアドレス
* サブアドレスとは
hoge@example.com に対して、hoge+todo@example.com のようなアドレスのこと。RFC5233で定められている。+todo の部分が余分でも、同じユーザーに届く。Gmailでも当然対応している。届いたメールアドレスを元になにか処理するときに使える。
* テストしておく
これを予め、自分で自分のサブアドレスにメールを送って確かめておく。
### 3-3. GAS とGoogle ToDo リスト
これを使うために、あらかじめ[GASでTasks(ToDo)追加する](https://taibonn.com/gsuite/gas-auto-add-tasks/)を読んでおく。なお、「ワンマンカー」というのが和製英語であるように（？）、Google ToDoリストは日本語の表記で、英語では Google Tasks のようである。(cf. [Google ToDo リストの概要](https://support.google.com/tasks/answer/7675772?hl=ja&ref_topic=7675628&sjid=2187807453339210319-AP))
### 3-4. Googleスプレッドシートの作成
あらかじめ、スタンドアロンのGASの書き出し用のGoogleスプレッドシートを作っておく。2行目以下は空欄にしておく。
![Qiita_メールからToDoリストへの自動処理_シートのみ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253484/a497789c-38cb-07bd-5271-13db97c9c853.png)
## 4. プログラム
### 4-1. メールから情報抽出してスプレッドシートへ記載
スタンドアロンのGASを書いた。ポイントはいくつかある。
* ChatGPT3 は今日の日付を答えられない。「明日まで」を理解するためには基点がわかる必要がある。そこで、メールのヘッダから日付を取得して与える。
* スクリプトプロパティを使った。プログラムを公開するときに安心。移植性もちょっと良くなる。
* Gmailのスレッドの最後のメールを使っている。これについては後ほど活用・応用のところでコメントする。
* 初回は手動で1回実行する。実行許可が問われるので許可する。
* トリガーを時間主導で1分おきに動くようにする。
* Gmailへのリンクは、RFC822の Message ID を利用している。私だけでなく、送信者や、同報で送られた人も（Gmailを使っていれば）使えるURLなので利便性が高い。
 ```javascript:サブアドレスに送られてきた仕事依頼のメールの以下の情報をスプレッドシートに書き出す
// サブアドレスに送られてきた仕事依頼のメールの以下の情報をスプレッドシートに書き出す。
//  ・件名
//  ・仕事の期限
//  ・メッセージIDに基づくGmailの検索URL
// Ver.01 : 2024-02-02

// 左側の 「プロジェクトの設定」（歯車マーク）> スクリプト プロパティ で、次の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('@', '+todo@'); // メールのサブアドレス hoge+todo@foo.com を設定。

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

  const ProcessedLabel = 'ToDoProcessed'; // 処理済みとわかるためのラベル名。任意。
  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 message = messages[messages.length - 1]; // スレッド内の最後（最新）のメッセージを取得

    if (!message.isInTrash()) {
      // メールから取り出すのは3つ。件名、メッセージID、日付
      var subject      = message.getSubject(); 
      var messageId    = message.getHeader('Message-ID');
      var dateHeader   = message.getDate();
      // AI で期限を取得
      var dateDeadLine = getDeadLineWithOpenAI(message.getPlainBody().slice(0,999), dateHeader);

      // スプレッドシートに追記（４列のデータ）
      sheet.appendRow([
          new Date(),    // 登録日時
          '',            // シートのコンテナバインドGASの処理状況欄。最初は空欄。
          dateDeadLine,  // 期限・期日
          subject,       // メールの件名
          'https://mail.google.com/mail/#search/in%3Aanywhere+'+encodeURIComponent('rfc822msgid:'+messageId)]); // メールのスレッドへの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': 'メールの内容を分析し、仕事の期限（締め切り）の日時を、文章を用いず、yyyy-MM-dd 形式で答えよ。理由などの記述は省く。\
  ただし、メールの書かれた日時は、' + 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; // メールから取り出した期限・期日
}
```
### 4-2. スプレッドシートからToDoリストへ登録
上記のGASを実行した結果は、スプレッドシートに保管される。そのスプレッドシートにコンテナバインドのGASを追加する。

ポイントとしては、次のようなところがある。
*  ```taskListId```のところは```var taskListId = '@default';``` とすれば、IDを調べる必要はない。個人名のついたToDoリストに登録される。
* 手作業でシートにToDo を書き込んでも良いように、GASを２つに分けている。シートの3列目（期日）を入力し直して修正、2列めを空欄にすれば、再びToDoリストにエントリーできる。
* このスクリプトも１回手作業で実行し、権限を与えてから、トリガーで１分ごとに起動するようにする。
```javascript:スプレッドシートからToDoリストへ登録
// スプレッドシートから ToDoリスト に仕事を登録する。
// Ver.1 2024-02-02
// 
// ※ シートは、メールの +todo のサブアドレス宛のメールを受信して、1分ごとのトリガーで追加・更新されている。
// ※ このシートに手書きすることを考えて、上の作業を実行するGASとは別に、ここで1分ごとのトリガーで起動する。
// ※ 準備として、ToDoリストのIDを取得する。次のURLにアクセスして、Execute
//     https://developers.google.com/tasks/reference/rest/v1/tasklists/list?hl=ja
//     リストは複数作れる。あらかじめ、"ByMail"というリストを作ってある。IDさえ取得できれば、名前は何でも構わない。
//     (cf. https://taibonn.com/gsuite/gas-auto-add-tasks/ )
//     取得した ID は、「プロジェクトの設定」>「スクリプトプロパティ」に taskID_ByMail として保存した。
// ※ 左側の「サービス」をクリックして、「Google TASKS API」を選んで「追加」をクリックする。左側に「Tasks」という表示が現れる。
//    1回、手動で実行して、各種パーミッションを許可してから、トリガーで１分ごとに実行するようにする。

// 必要なスクリプトプロパティ
// 1) taskID_ByMail : Google の TODO List のID

function processSpreadsheetForTasks() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var data  = sheet.getDataRange().getValues();
  var urlGS = SpreadsheetApp.getActiveSpreadsheet().getUrl();

  // Google Tasks APIを扱うためのToDoリストIDを取得して指定。
  //
  // デフォルトのリストの場合
  // var taskListId = '@default';
  // ByMail というToDoリストのIDは、スクリプトプロパティから取得
  var taskListId = PropertiesService.getScriptProperties().getProperty('taskID_ByMail');

  for (var i = 1; i < data.length; i++) { // 1行目（ヘッダー行）をスキップ
    var row = data[i];    
    if (row[1] === '' && isValidDateTime(row[2])) {
      var dateDL = new Date(row[2]);
      var dateDLstring = Utilities.formatDate(dateDL, "JST", "yyyy-MM-dd")+ "T00:00:00.000Z";
      try {
        var task = {
          title: row[3],
          notes: '■ 詳細のメールはこちら：\n' + row[4] +'\n■ スプレッドシートはこちら: ' + urlGS,
          due: dateDLstring
        };

        // タスクを追加
        var createdTask = Tasks.Tasks.insert(task, taskListId);

        // スプレッドシートに「Order」をマーク
        sheet.getRange(i + 1, 2).setValue('Ordered');
      } catch(e) {
        // エラー処理
        console.error('Error adding task: ' + e.message);
      }
    }
  }
}

// 日時または日付として妥当かどうかを判定
function isValidDateTime(value) {
  var date = new Date(value);
  return !isNaN(date.getTime()); // 日時として解釈可能ならtrueを返す
}
```
## 5. 活用・応用
* 周りの人に「私に期限付きの仕事を振るときには、このメールアドレスに送ってね！♡」なんて言っても送ってくれいないだろう。そういうメールが来そうなメーリングリストへの登録を、サブアドレスにしておく可能性がある。期日をAIが判定できなければ、登録されない。
* 自分でメールを読んでいて、「あ、これ登録しなきゃ」とおもったら、自分のサブアドレスに転送するとよい。登録される。メールへのURLもスレッドへのURLになるので、転送メールと転送前の着信メールと、両方開ける。
* プログラムではスレッドを取得している。繰り返しになるが、Gmailでは、転送されたメールも同じスレッドになる。だから、件名に Fwd: がある場合には、転送メールと判断し、一つ前のメールを登録すると良いかもしれない。
* Googleカレンダーの方が他の人と共有できていいのかもしれない。
## 6. 感想
OpenAIのAPIは安い。まだ1ドルも使っていない。こんなに安ければ、活用したくなる。業務の効率化は、アイデア次第で色々できそうだ。しかし、数年すれば、こんなスクリプト書かなくても、標準サービスで自動でやってくれそうな気もするし、そういった方向への進化を期待している。

もっとスタイリッシュにやっている人がいそうな気もしますが、自分の備忘録を兼ねて。