3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[GAS] Gmailの内容をAIで分析して、Googleカレンダーに予定を自動登録

Posted at

できること

  • AIがGmailに届いたメールを分析して、Googleカレンダーに予定登録

必要なもの

  • Googleアカウント (言わなくても持ってる)
  • ChatGPTのアカウント (APIを利用できるアカウントが必要)

イチオシポイント

  • 若干面倒だが、Firebaseを使って、一度処理したメールの識別子を保存することで、GAS側のメール取得回数の制限やChatGPTのAPI利用数を減らすことができる!
    • メールを別のラベル・フォルダを移す場合は過剰すぎる機能かもしれない。受信ボックスで管理して、移すのは自分でやりたいという人にはアリ
    • メールの識別子はメッセージのIDなので、スレッドの返信などにも対応可能
  • 最新14日の識別子の保存にすることでFirabaseの容量・ダウンロード量も削減
  • 上記の重複チェックにより、Googleカレンダーに重複して予定が入らない

作り方

  1. Apps Script (GAS)から新しいプロジェクトを作成

  2. OpenAIのページからAPIキーを生成
    2024_11_01_15_59_33_j3tr6kO3.png

  3. Firebaseでプロジェクト作成 & Realtime Database作成

  4. Firebaseで作成したプロジェクトの[プロジェクトの設定]→[サービスアカウント]→[以前の認証情報]から作成したデータベースのシークレットを取得

  5. 1で作成したGASの[プロジェクトの設定]→[スクリプトプロパティ]に以下の名前で2・4のトークンを設定

  • OPENAI_API_KEY : ChatGPTのAPIトークン
  • FIREBASE_SECRET : データベースアクセス用のトークン
  1. コード.gsに以下のコードをコピペ
// FirebaseのURLと認証情報を設定
var FIREBASE_URL = 'https://checker-30f77-default-rtdb.firebaseio.com';
var FIREBASE_SECRET = PropertiesService.getScriptProperties().getProperty('FIREBASE_SECRET');

// メールの一意IDをFirebaseに保存
function saveProcessedEmailId(emailId, messageId, receivedDate) {
  var url = FIREBASE_URL + '/processedEmails/' + emailId + '.json?auth=' + FIREBASE_SECRET;
  var options = {
    method: 'put',
    contentType: 'application/json',
    payload: JSON.stringify({ processed: true, messageId, receivedDate })
  };
  UrlFetchApp.fetch(url, options);
}

// 処理済みメールIDの一覧をFirebaseから取得
function getProcessedEmailIds() {
  var url = FIREBASE_URL + '/processedEmails.json?auth=' + FIREBASE_SECRET;
  var response = UrlFetchApp.fetch(url);
  var data = JSON.parse(response.getContentText());
  
  var processedIds = [];
  for (var email in data) {
    if (data[email].processed && data[email].messageId) {
      processedIds.push(data[email].messageId)
    }
  }
  return processedIds;
}

// 14日以上前のエントリを削除
function cleanOldEntries() {
  var url = FIREBASE_URL + '/processedEmails.json?auth=' + FIREBASE_SECRET;
  var response = UrlFetchApp.fetch(url);
  var data = JSON.parse(response.getContentText());

  // 14日前の日付を計算
  var cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - 14);
  // cutoffDate.setSeconds(cutoffDate.getSeconds() - 1); // test用

  for (var emailId in data) {
    var entry = data[emailId];
    var receivedDate = new Date(entry.receivedDate);

    // 受信日時が14日以上前なら削除
    if (receivedDate < cutoffDate) {
      var deleteUrl = FIREBASE_URL + '/processedEmails/' + emailId + '.json?auth=' + FIREBASE_SECRET;
      UrlFetchApp.fetch(deleteUrl, { method: 'delete' });
      Logger.log('削除されたメールID: ' + emailId);
    }
  }
}

// メイン関数
function checkEmailsAndCreateCalendarEvents() {
  var now = new Date();
  var hour = now.getHours();
  
  // 0時から6時までは処理をスキップ
  if (hour >= 0 && hour < 6) {
    Logger.log('夜間のため処理をスキップします。現在の時刻: ' + now);
    return;
  }

  try {
    // 処理済みのメールIDをFirebaseから取得
    var processedEmailIds = getProcessedEmailIds();
    var excludeQuery = processedEmailIds.map(id => `-rfc822msgid:${id}`).join(' ');
    
    // クエリで処理済みメールIDを除外して検索
    var threads = GmailApp.search(`in:inbox -is:starred ${excludeQuery}`);
    
    Logger.log(`取得したメール件数: ${threads.length}`)
    threads.forEach(function(thread) {
      var messages = thread.getMessages();
      messages.forEach(function(message) {
        // jsonのキー用のID
        const emailId = message.getId();
        // RFC822形式のMessage-IDを取得
        var messageId = message.getHeader('Message-ID').replace(/[<>]/g, "");
        var receiveDate = message.getDate();

        console.log(messageId, emailId)
        // メールが処理済みかどうかを確認
        if (processedEmailIds.includes(messageId)) {
          Logger.log('メールは既に処理済み: ' + messageId);
          return;
        }

        var body = message.getPlainBody();
        
        // OpenAI APIを使用してメール内容を解析
        var schedule = analyzeEmailWithOpenAI(body);
        saveProcessedEmailId(emailId, messageId, receiveDate);

        console.log(schedule)
        if (schedule.schedule_required) {
          // 既にカレンダーに登録されているか確認
          var isDuplicate = checkDuplicateEvent(schedule);
          
          if (!isDuplicate) {
            // Googleカレンダーにイベントを作成
            createCalendarEvent(schedule, message);
          }
        }
      });
    });
  } catch (error) {
    Logger.log('Error processing emails: ' + error);
  }
}

// OpenAI APIを使用してメール内容を解析
function analyzeEmailWithOpenAI(body) {
  var OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
  
  var role = 'あなたは優秀な秘書です。Gmailにて、主人が何か予約したり、あるいは予定を入れたりしたことによって届いたメールを確認して、Googleカレンダーに登録するという業務を担当しています。以下のメールは予定に含めないこととしています。\n・サブスクリプションや継続課金の更新案内\n・広告や予定ではない何かの案内\n・応募していない抽選に関するメール\n・システムメンテナンスの案内\n・セール情報\n・配送情報\n本メッセージ下部のメール本文を確認して、以下の要素をjson形式で出力してください。コードブロックにする必要はなく、JSON.parseできる文字列で返却してください。\n・Googleカレンダーに予定を登録するべきかどうか(TrueかFalse)\n・予定の簡単な概要(カレンダーのタイトルとなる)\n・予定の日時(スタート時間)\n・差出人名\n\n--出力例--\n{\n  "schedule_required":,\n  "schedule_content": ,\n  "schedule_time": ,\n  "sender_name":\n}\n: \n'; 
  var prompt = role + '\n' + body;
  
  var url = 'https://api.openai.com/v1/chat/completions';
  var requestOptions = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': 'Bearer ' + OPENAI_API_KEY
    },
    payload: JSON.stringify({
      "model": "gpt-4o-mini",
      "messages": [{"role": "user", "content": prompt}]
    }),
    muteHttpExceptions: true
  };
  
  var response = UrlFetchApp.fetch(url, requestOptions);
  
  if (response.getResponseCode() !== 200) {
    Logger.log('OpenAI API Error: ' + response.getContentText());
    return {};
  }
  
  var jsonResponse = JSON.parse(response.getContentText());
  var content = jsonResponse.choices[0].message.content;

  // 使用されたトークン数を取得
  var usage = jsonResponse.usage;
  if (usage) {
    Logger.log('Total tokens used: ' + usage.total_tokens);
  } else {
    Logger.log('No usage data available in the response');
  }

  
  try {
    var schedule = JSON.parse(content);
    return schedule;
  } catch (e) {
    Logger.log('JSON Parse Error: ' + e);
    Logger.log('Response Content: ' + content);
    return {};
  }
}

// 重複イベントをチェック
function checkDuplicateEvent(schedule) {
  var calendar = CalendarApp.getDefaultCalendar();
  var events = calendar.getEvents(new Date(schedule.schedule_time), new Date(new Date(schedule.schedule_time).getTime() + 60 * 60 * 1000));
  console.log(events)
  
  for (var i = 0; i < events.length; i++) {
    var event = events[i];
    if (event.getTitle() === schedule.task_content) {
      return true;
    }
  }
  
  return false;
}

// Googleカレンダーにイベントを作成
function createCalendarEvent(schedule, message) {
  var calendar = CalendarApp.getDefaultCalendar();
  var startDateTime = new Date(schedule.schedule_time);
  var endDateTime = new Date(startDateTime.getTime() + 60 * 60 * 1000); // 1時間後
  
  calendar.createEvent(schedule.schedule_content, startDateTime, endDateTime, {
    description: 'タスク自動登録: ' + schedule.sender_name
  });

  message.star();
}

トリガーの作成

このままだとGASは自動で実行されないので、トリガーを設定する必要がある。
左のサイドメニューからトリガーを選択して、右下のボタンから以下のように追加。
メールが多い人は、時間間隔を5~10分くらいにしてもいいかも!
エラー通知頻度もお任せします!
2024_11_01_16_23_40_k6bxHeTO.png

古いメール識別子の削除の関数も以下のように設定して、トリガーしてください。
毎日0~1時の間に実行されます、多分!
2024_11_01_16_26_24_owBeAgMF.png

プロンプトについて

ChatGPTに送る、指示文(プロンプト)は以下のようになっています。
除外条件などは皆さんのお好みに調整してください。(コード内の変数roleを参照)
下のJSONの出力を変更した場合はコードの変更も必要です。

あなたは優秀な秘書です。
Gmailにて、主人が何か予約したり、あるいは予定を入れたりしたことによって届いたメールを確認して、Googleカレンダーに登録するという業務を担当しています。
以下のメールは予定に含めないこととしています。
・サブスクリプションや継続課金の更新案内
・広告や予定ではない何かの案内
・応募していない抽選に関するメール
・システムメンテナンスの案内
・セール情報
・配送情報

本メッセージ下部のメール本文を確認して、以下の要素をjson形式で出力してください。
コードブロックにする必要はなく、JSON.parseできる文字列で返却してください。
・Googleカレンダーに予定を登録するべきかどうか(TrueかFalse)
・予定の簡単な概要(カレンダーのタイトルとなる)
・予定の日時(スタート時間)
・差出人名

--出力例--
{
 "schedule_required":,
 "schedule_content": ,
 "schedule_time": ,
 "sender_name":
}

注意

  • バリデーションの甘いところがあるかもしれません。
    エラーがあれば教えて下さい!直して更新しておきます。

Special thanks

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?