できること
- AIがGmailに届いたメールを分析して、Googleカレンダーに予定登録
必要なもの
- Googleアカウント (言わなくても持ってる)
- ChatGPTのアカウント (APIを利用できるアカウントが必要)
イチオシポイント
- 若干面倒だが、Firebaseを使って、一度処理したメールの識別子を保存することで、GAS側のメール取得回数の制限やChatGPTのAPI利用数を減らすことができる!
- メールを別のラベル・フォルダを移す場合は過剰すぎる機能かもしれない。受信ボックスで管理して、移すのは自分でやりたいという人にはアリ
- メールの識別子はメッセージのIDなので、スレッドの返信などにも対応可能
- 最新14日の識別子の保存にすることでFirabaseの容量・ダウンロード量も削減
- 上記の重複チェックにより、Googleカレンダーに重複して予定が入らない
作り方
-
Apps Script (GAS)から新しいプロジェクトを作成
-
OpenAIのページからAPIキーを生成
-
Firebaseでプロジェクト作成 & Realtime Database作成
-
Firebaseで作成したプロジェクトの[プロジェクトの設定]→[サービスアカウント]→[以前の認証情報]から作成したデータベースのシークレットを取得
-
1で作成したGASの[プロジェクトの設定]→[スクリプトプロパティ]に以下の名前で2・4のトークンを設定
- OPENAI_API_KEY : ChatGPTのAPIトークン
- FIREBASE_SECRET : データベースアクセス用のトークン
- コード.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分くらいにしてもいいかも!
エラー通知頻度もお任せします!
古いメール識別子の削除の関数も以下のように設定して、トリガーしてください。
毎日0~1時の間に実行されます、多分!
プロンプトについて
ChatGPTに送る、指示文(プロンプト)は以下のようになっています。
除外条件などは皆さんのお好みに調整してください。(コード内の変数roleを参照)
下のJSONの出力を変更した場合はコードの変更も必要です。
あなたは優秀な秘書です。
Gmailにて、主人が何か予約したり、あるいは予定を入れたりしたことによって届いたメールを確認して、Googleカレンダーに登録するという業務を担当しています。
以下のメールは予定に含めないこととしています。
・サブスクリプションや継続課金の更新案内
・広告や予定ではない何かの案内
・応募していない抽選に関するメール
・システムメンテナンスの案内
・セール情報
・配送情報
本メッセージ下部のメール本文を確認して、以下の要素をjson形式で出力してください。
コードブロックにする必要はなく、JSON.parseできる文字列で返却してください。
・Googleカレンダーに予定を登録するべきかどうか(TrueかFalse)
・予定の簡単な概要(カレンダーのタイトルとなる)
・予定の日時(スタート時間)
・差出人名
--出力例--
{
"schedule_required":,
"schedule_content": ,
"schedule_time": ,
"sender_name":
}
注意
- バリデーションの甘いところがあるかもしれません。
エラーがあれば教えて下さい!直して更新しておきます。