GAS×Gemini API実践|メール返信下書きを自動生成する方法

GASとGemini APIで新着メールを自動分類しパターン別に返信下書きを生成する実践ガイド AI

2026.03.25 更新:Gemini 2.5 Flashのシャットダウン予定(2026年6月17日)とGemini 3系モデルへの移行ガイダンスを追記

「案件継続の確認メール、了解の返信、稼働報告への受領連絡──」毎日同じようなメールを何通も書いていませんか。SE歴20年の筆者も、気づけば1日30分以上をメール返信に費やしていました。

Google Apps Script(GAS)とGemini APIを組み合わせれば、新着メールを自動で分類し、パターンに応じた返信の下書きをGmailに自動生成できます。本記事では、実際にスクリプトを動かして遭遇したハマりポイント(429エラー、権限承認、モデル別レート制限)も含め、コピペで実装できるコード全文を公開します。

毎日のメール返信、同じようなことを書いていませんか

マネージャーやリーダー職になると、受信するメールの種類はある程度パターン化されます。取引先からの案件継続確認、稼働報告、請求連絡。メンバからの日報。内容を確認して返信を書く作業自体は数分ですが、1日に何通も積み重なると無視できない時間になります。

この「パターン化されている」という特徴が、自動化のチャンスです。ルールベースの振り分けとAIによる文面生成を組み合わせれば、返信の下書きを自動で用意できます。あくまで下書きなので、送信前に内容を確認・編集できる安全設計です。

この仕組みで自動化できる5つのメールパターン

今回自動化するのは、筆者の実務で頻度が高い以下の5パターンです。

  • 取引先からの案件継続確認 → 継続・終了の2パターンを本文に併記し、自分で選んで編集
  • 取引先からの稼働報告 → 問題ない旨を伝え、次の手続き(請求書提出等)を促す
  • 取引先からの請求連絡 → 受領確認と謝辞
  • 日報 → 受領連絡とコメント記入用フォーマット
  • 進捗報告 → 受領連絡と内容に対するコメント

これら以外のメールも、Gemini APIが返信要否を判断し、必要なら自由に返信文を生成します。

全体のデータフローと処理の流れ

処理は5ステップで進みます。まず前回実行以降の新着メールを取得し、除外ルール(送信者・件名・受信タイプ)に該当するメールをスキップします。次にパターンシートのルールベースで分類し、一致すれば対応するプロンプトでGemini APIに返信文を生成させます。どのパターンにも一致しないメールはGeminiに自由に分類・返信要否判断を任せます。生成された返信は「全員に返信」の下書きとしてGmailに保存し、「AI下書き」ラベルを付けて通常の下書きと分離管理します。

メール返信自動化の全体フロー:新着メール→除外→パターン判定→Gemini分類→下書き保存

事前準備──Gemini APIキーの取得とGASの開き方

Gemini APIキーを無料で取得する

Gemini APIを使うために、Geminiの有料プラン(Gemini Advanced等)を契約する必要はありません。Geminiのチャットサービスと開発者向けAPIはまったく別物です。個人のGoogleアカウントでGoogle AI Studioにアクセスし、「Get API Key」ボタンを押すだけで、無料でAPIキーを取得できます。クレジットカードの登録も不要です。

Google AI StudioのGet API Keyボタンを押す操作

会社のGoogle Workspaceアカウントでは、管理者がAI Studioへのアクセスをブロックしている場合があります。「An unknown error occurred」と表示された場合は管理者設定が原因です。個人のGoogleアカウントに切り替えてアクセスしてください。

GASは「スプレッドシートから」作成する

GASのスクリプトを作成する方法は複数ありますが、本記事のスクリプトは必ずスプレッドシートから作成してください。スプレッドシートを開き、メニューの「拡張機能 → Apps Script」を選択します。

Googleドライブの「新規作成 → その他 → Google Apps Script」で作成すると「スタンドアロンスクリプト」になります。この場合、コード内のSpreadsheetApp.getActiveSpreadsheet()がどのスプレッドシートにも紐づかずnullを返すため、シート読み込みでエラーが発生します。筆者も最初にこの方法で作成してしまい、原因特定に時間を取られました。

スプレッドシートの拡張機能メニューからApps Scriptを開く操作

スプレッドシートの4シート設計

スクリプトが参照するスプレッドシートには、4つのシートを作成します。設定値・除外ルール・返信パターン・実行ログをそれぞれ管理する構成です。

「設定」シート──前回実行日時とラベル名

A列に項目名、B列に値を入力します。1行目はヘッダーです。B1の「前回実行日時」は初回実行時に空欄のままにしておけば、スクリプトが自動的に24時間前を基準にします。実行後に現在日時が自動で書き込まれます。B2の「ラベル名」は自動生成した下書きに付けるGmailラベルの名前です。

設定シートに前回実行日時とラベル名を入力した状態

「除外」シート──返信不要メールのルール定義

返信が不要なメールを事前にフィルタリングします。条件タイプは「送信者」「件名」「受信タイプ」の3種類です。受信タイプは、自分がToのみ(to-only)、Toに複数名(to-multi)、Cc(cc)、Bcc(bcc)の4分類で、ccとbccを除外に設定しておくと大半のノイズを排除できます。

受信タイプの分類

to-only:自分だけがTo。直接の依頼で返信必須度が高い

to-multi:Toに自分と他者。複数名への依頼で、誰かが返信すればよい場合も

cc:自分はCcのみ。参考共有で基本返信不要

bcc:自分はBcc。一斉送信でほぼ返信不要

除外シートに送信者・件名・受信タイプの除外ルールを入力した状態

「パターン」シート──5パターンの判定条件とプロンプト

A列にパターン名、B列に条件タイプ(現在は「件名」のみ)、C列に条件値(キーワード)、D列に受信タイプ制限(カンマ区切りで複数指定可、空欄なら無制限)、E列にGeminiへのプロンプトを記載します。

たとえば「案件継続確認」パターンでは、件名に「継続確認」を含み、受信タイプがto-onlyまたはto-multiの場合に、継続・終了の2パターンを本文内に併記するプロンプトを実行します。プロンプトはスプレッドシート上で自由に編集できるため、スクリプトを触らずに返信の文面やトーンを調整できます。

パターンシートに5パターンの条件とプロンプトを入力した状態

コピペ用コード全文と初回セットアップ

コード全文

以下がスクリプト全文です。メイン関数processNewEmailsと、受信タイプ判定・除外判定・パターンマッチ・Gemini分類・API呼び出し・下書き作成の6つの補助関数で構成されています。

メイン関数 processNewEmails() の冒頭部分だけ抜粋します。

function processNewEmails() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var settingsSheet = ss.getSheetByName("設定");
  var lastRunRaw = settingsSheet.getRange("B1").getValue();
  var labelName = settingsSheet.getRange("B2").getValue() || "AI下書き";
  // ... 続きはトグル内で全文を掲載
}
function processNewEmails() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var settingsSheet = ss.getSheetByName("設定");
  var lastRunRaw = settingsSheet.getRange("B1").getValue();
  var labelName = settingsSheet.getRange("B2").getValue() || "AI下書き";
  var myEmail = Session.getActiveUser().getEmail();
  var lastRun;

  if (lastRunRaw) {
    lastRun = new Date(lastRunRaw);
  } else {
    lastRun = new Date(Date.now() - 24 * 60 * 60 * 1000);
  }

  var excludeSheet = ss.getSheetByName("除外");
  var excludeData = excludeSheet.getDataRange().getValues();
  excludeData.shift();

  var patternSheet = ss.getSheetByName("パターン");
  var patternData = patternSheet.getDataRange().getValues();
  patternData.shift();

  var logSheet = ss.getSheetByName("ログ");
  if (!logSheet) {
    logSheet = ss.insertSheet("ログ");
    logSheet.appendRow(["実行日時", "件名", "送信者", "受信タイプ", "処理区分", "分類結果", "ドラフトURL"]);
  }

  var label = GmailApp.getUserLabelByName(labelName);
  if (!label) { label = GmailApp.createLabel(labelName); }

  var afterDate = Utilities.formatDate(lastRun, Session.getScriptTimeZone(), "yyyy/MM/dd");
  var threads = GmailApp.search("after:" + afterDate + " is:inbox");

  var processedCount = 0;
  var excludedCount = 0;
  var draftedCount = 0;
  var skippedCount = 0;
  var now = new Date();

  for (var i = 0; i < threads.length; i++) {
    var messages = threads[i].getMessages();
    var lastMsg = messages[messages.length - 1];
    if (lastMsg.getDate() <= lastRun) continue;

    var subject = lastMsg.getSubject();
    var from = lastMsg.getFrom();
    var body = lastMsg.getPlainBody();
    var to = lastMsg.getTo() || "";
    var cc = lastMsg.getCc() || "";
    var receiveType = getReceiveType(myEmail, to, cc, lastMsg);
    processedCount++;

    if (isExcluded(subject, from, receiveType, excludeData)) {
      logSheet.appendRow([now, subject, from, receiveType, "除外", "-", "-"]);
      excludedCount++;
      continue;
    }

    var matchedPattern = matchPattern(subject, receiveType, patternData);
    if (matchedPattern) {
      var prompt = matchedPattern.prompt + "\n\n---\n【件名】" + subject + "\n【送信者】" + from + "\n【本文】\n" + body;
      var replyText = callGemini(prompt);
      if (replyText) {
        createReplyDraft(lastMsg, replyText, label);
        logSheet.appendRow([now, subject, from, receiveType, "生成", matchedPattern.name, "下書き作成済"]);
        draftedCount++;
      }
      continue;
    }

    var geminiResult = classifyAndDraftWithGemini(subject, from, body, receiveType);
    if (geminiResult.needsReply && geminiResult.replyText) {
      createReplyDraft(lastMsg, geminiResult.replyText, label);
      logSheet.appendRow([now, subject, from, receiveType, "生成(Gemini分類)", geminiResult.category, "下書き作成済"]);
      draftedCount++;
    } else {
      logSheet.appendRow([now, subject, from, receiveType, "スキップ", geminiResult.category || "返信不要", "-"]);
      skippedCount++;
    }
  }

  settingsSheet.getRange("B1").setValue(now);
  Logger.log("完了: 処理=" + processedCount + " 生成=" + draftedCount + " 除外=" + excludedCount + " スキップ=" + skippedCount);
}

function getReceiveType(myEmail, to, cc, message) {
  var myEmailLower = myEmail.toLowerCase();
  var toLower = to.toLowerCase();
  var ccLower = cc.toLowerCase();

  if (ccLower.indexOf(myEmailLower) !== -1 && toLower.indexOf(myEmailLower) === -1) return "cc";
  if (toLower.indexOf(myEmailLower) === -1 && ccLower.indexOf(myEmailLower) === -1) return "bcc";
  if (toLower.indexOf(myEmailLower) !== -1) {
    var toAddresses = toLower.split(",").map(function(addr) {
      return addr.trim().replace(/.*</, "").replace(/>.*/, "");
    }).filter(function(addr) { return addr.indexOf("@") !== -1; });
    return toAddresses.length === 1 ? "to-only" : "to-multi";
  }
  return "unknown";
}

function isExcluded(subject, from, receiveType, excludeData) {
  for (var i = 0; i < excludeData.length; i++) {
    var condType = String(excludeData[i][1]).trim();
    var condValue = String(excludeData[i][2]).trim();
    if (!condType || !condValue) continue;
    switch (condType) {
      case "送信者": if (from.toLowerCase().indexOf(condValue.toLowerCase()) !== -1) return true; break;
      case "件名": if (subject.indexOf(condValue) !== -1) return true; break;
      case "受信タイプ": if (receiveType === condValue) return true; break;
    }
  }
  return false;
}

function matchPattern(subject, receiveType, patternData) {
  for (var i = 0; i < patternData.length; i++) {
    var name = String(patternData[i][0]).trim();
    var condType = String(patternData[i][1]).trim();
    var condValue = String(patternData[i][2]).trim();
    var allowedTypes = String(patternData[i][3]).trim();
    var prompt = String(patternData[i][4]).trim();
    if (!name || !condType || !condValue || !prompt) continue;
    if (condType === "件名" && subject.indexOf(condValue) === -1) continue;
    if (allowedTypes) {
      var types = allowedTypes.split(",").map(function(t) { return t.trim(); });
      if (types.indexOf(receiveType) === -1) continue;
    }
    return { name: name, prompt: prompt };
  }
  return null;
}

function classifyAndDraftWithGemini(subject, from, body, receiveType) {
  var prompt = "あなたはIT企業のプロジェクトマネージャーのメールアシスタントです。\n"
    + "以下のメールを分析し、JSON形式で回答してください。\n\n"
    + "【判断基準】\n"
    + "- 自分宛て(to-only, to-multi)の業務メールで返信が期待されている場合は返信要\n"
    + "- 通知・報告のみで返信不要と判断できる場合は返信不要\n"
    + "- 迷った場合は返信要として短い受領確認を作成\n\n"
    + "【出力JSON形式】\n"
    + "{\"needsReply\": true/false, \"category\": \"分類名\", \"replyText\": \"返信文\"}\n\n"
    + "【注意】\n- 返信文はビジネスメールとして適切な敬語で作成\n- JSON以外のテキストは出力しないでください\n\n"
    + "---\n【件名】" + subject + "\n【送信者】" + from + "\n【受信タイプ】" + receiveType + "\n【本文】\n" + body;

  var responseText = callGemini(prompt);
  if (!responseText) return { needsReply: false, category: "APIエラー", replyText: "" };

  try {
    var jsonMatch = responseText.match(/\{[\s\S]*\}/);
    if (jsonMatch) {
      var result = JSON.parse(jsonMatch[0]);
      return {
        needsReply: result.needsReply || false,
        category: result.category || "不明",
        replyText: result.replyText || ""
      };
    }
  } catch (e) {
    Logger.log("JSON解析エラー: " + e.message);
  }
  return { needsReply: false, category: "解析エラー", replyText: "" };
}

function callGemini(prompt) {
  var apiKey = PropertiesService.getScriptProperties().getProperty("GEMINI_API_KEY");
  if (!apiKey) {
    Logger.log("エラー: GEMINI_API_KEYが設定されていません");
    return null;
  }

  var url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent";
  var payload = { contents: [{ parts: [{ text: prompt }] }] };
  var options = {
    method: "POST",
    contentType: "application/json",
    headers: { "x-goog-api-key": apiKey },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  var maxRetries = 3;
  for (var attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      var response = UrlFetchApp.fetch(url, options);
      var statusCode = response.getResponseCode();

      if (statusCode === 200) {
        var data = JSON.parse(response.getContentText());
        return data.candidates[0].content.parts[0].text;
      }

      if (statusCode === 429) {
        var retryMatch = response.getContentText().match(/"retryDelay":\s*"(\d+)s"/);
        var waitSec = retryMatch ? parseInt(retryMatch[1]) + 5 : 60;
        Logger.log("レート制限(試行" + attempt + "/" + maxRetries + "):" + waitSec + "秒待機");
        Utilities.sleep(waitSec * 1000);
        continue;
      }

      Logger.log("APIエラー: " + statusCode + " " + response.getContentText());
      return null;
    } catch (e) {
      Logger.log("API呼び出しエラー(試行" + attempt + "): " + e.message);
      if (attempt < maxRetries) Utilities.sleep(30 * 1000);
    }
  }

  Logger.log("リトライ上限到達。スキップします。");
  return null;
}

function createReplyDraft(message, replyText, label) {
  var draft = message.createDraftReplyAll(replyText);
  var thread = message.getThread();
  thread.addLabel(label);
  return draft;
}

初回セットアップ手順

  1. Googleドライブで新しいスプレッドシートを作成する
  2. 「設定」「除外」「パターン」の3シートを作成し、前述の通り入力する
  3. メニュー「拡張機能 → Apps Script」でスクリプトエディタを開く
  4. 上記コード全文を貼り付けて保存(Ctrl+S)
  5. 左メニュー「プロジェクトの設定」→「スクリプトプロパティ」に GEMINI_API_KEY を追加し、取得したAPIキーを入力
  6. 関数「processNewEmails」を選択して▶実行ボタンを押す
  7. 初回は権限承認が求められるので許可する(次のセクションで詳しく解説)
GASスクリプトプロパティにGEMINI_API_KEYを設定する画面

「このアプリはGoogleで確認されていません」の突破方法

初回実行時、「このアプリはGoogleで確認されていません」という警告画面が表示されます。GmailやGoogleドライブへのアクセス権限を求めるスクリプトでは必ず表示されるもので、自分で書いたスクリプトであれば安全です。

この警告は「Googleがまだ審査していないスクリプト」に対して表示されるもので、スクリプト自体が危険という意味ではありません。自分で作成したスクリプトであれば問題なく進めて大丈夫です。

画面左下の「詳細」リンクをクリックすると、下部に「(プロジェクト名)(安全ではないページ)に移動」というリンクが表示されます。これをクリックすると権限の許可画面に進みます。「許可」を押せばスクリプトが実行されます。

「このアプリはGoogleで確認されていません」の警告画面と詳細リンク
権限承認で「詳細」→「安全でないページに移動」を選択する操作

無料枠の落とし穴──モデル別レート制限と回避策

スクリプトが動き始めた直後に、429エラー(RESOURCE_EXHAUSTED)に遭遇しました。Gemini APIの無料枠にはモデルごとにリクエスト制限があり、想像よりかなり厳しい数値です。

モデル別の無料枠比較

2026年3月時点の公式情報です。無料枠のレート制限は2025年12月に大幅削減されており、以前の数値とは大きく異なります。最新の上限はGemini APIレート制限ダッシュボードで自分のプロジェクトの実際の値を確認してください。

モデルRPM(1分あたり)RPD(1日あたり)TPM(トークン/分)
Gemini 2.5 Pro5100250,000
Gemini 2.5 Flash10250250,000
Gemini 2.5 Flash-Lite151,000250,000

Gemini 2.5 Flashは2026年6月17日にシャットダウンが予定されています。後継のGemini 3 Flash Preview(2025年12月公開)やGemini 3.1系モデルがAPI無料枠で利用可能です。シャットダウン前にcallGemini関数内のモデル名をgemini-3-flash-previewなどに変更してください。最新の非推奨スケジュールはGoogle公式の非推奨モデル一覧で確認できます。

Gemini APIレート制限画面でRPM・RPDの使用状況を確認

モデルはRPDが別カウント──使い分けで回避する

重要な発見がありました。レート制限はモデルごとに別カウントです。Gemini 2.5 FlashでRPDを使い切っても、コードのモデル名をgemini-2.5-flashに変更すれば、そのモデルのRPDがフルに使えます。変更箇所はcallGemini関数内のURL1箇所だけです。

実際に筆者はGemini 2.5 FlashのRPD 250を使い切った後、Flash-Lite(RPD 1,000)に切り替えて同日中にテストを完了しました。返信文の品質はFlash-Liteでも実用レベルでした。日常運用ではRPD 250のGemini 2.5 Flashか、処理数が多い場合はRPD 1,000のFlash-Liteがおすすめです。コードの変更箇所はcallGemini関数内のURL1箇所だけです。

運用Tipsと今後の拡張

ルールベースのパターンを増やしてAPI呼び出しを最小化

パターンシートのルールベースで処理されたメールはGemini APIを1回呼び出すだけですが、Gemini分類に回るとAPI 1回で「分類+返信生成」を処理します。ルールベースでカバーできるパターンを増やすほど、API消費を抑えて安定した運用ができます。

運用を続けていくと、ログシートの「生成(Gemini分類)」に記録されるメールに繰り返し同じ分類名が出てきます。それが新しいパターン候補です。パターンシートに追加すれば、次回以降はルールベースで処理されるようになります。

「AI下書き」ラベルで通常の下書きと分離管理

スクリプトが生成した下書きには自動的に「AI下書き」ラベルが付きます。Gmailの左メニューでこのラベルをクリックすると、自動生成された下書きだけを一覧表示できます。通常の手書き下書きと混在しないため、まとめて確認・編集する運用が可能です。

Gmailの「AI下書き」ラベルに自動生成された下書きが並んでいる状態
ログシートに処理結果(生成・除外・スキップ)が記録された状態

▶ GASを使った業務自動化の全体像や、SendTo・Seleniumなど他の自動化テクニックに興味がある方はこちらも参考になります。
SE歴20年の業務自動化術|SendTo・Selenium・AIエージェントまで

よくある質問

Q
GASは無料で使えますか?
A

はい、Googleアカウントがあれば無料で使えます。Google Workspaceアカウントでも個人アカウントでも利用可能です。有料のWorkspaceプランのほうが実行回数の上限は高くなりますが、本記事のスクリプト程度であれば無料アカウントでも問題ありません。

Q
プログラミング経験がなくても大丈夫ですか?
A

本記事のコードはコピペで動作するように設計しています。スプレッドシートの「設定」「除外」シートに値を入力し、スクリプトを貼り付けて実行ボタンを押すだけです。エラーが出た場合のデバッグにはJavaScriptの基礎知識があると助かりますが、エラーメッセージをそのままChatGPTやGeminiに貼り付けて解決策を聞く方法もあります。ExcelVBAを使った業務効率化に興味がある方は「SE歴20年の時短術|Excel業務効率化15選」も参考になります。

Q
複数プロジェクトに対応できますか?
A

設定シートの行を増やし、スクリプトをループ処理に変更すれば対応可能です。ただし、プロジェクトごとに除外キーワードが異なる場合は、除外シートの構成も合わせて変更する必要があります。まずは1プロジェクトで運用を安定させてから拡張することをおすすめします。

Q
NotebookLMの個人向け有料プランでAPIは使えますか?
A

2026年3月時点では使えません。NotebookLM APIが提供されているのはEnterprise版のみです。個人向けプラン(Free / Plus / Pro / Ultra)ではソースの自動追加APIは利用できません。プランの確認方法や各プランの上限値は「NotebookLM活用術7選」のプラン比較表を参照してください。

Q
除外フィルタで取りこぼしが出たらどうすればいいですか?
A

ログシートで除外数を確認し、想定より少ない場合は除外シートにキーワードを追加してください。Gmailの検索演算子を活用して「from:特定アドレス」や「label:ラベル名」で対象を絞る方法も有効です。自動化ツールを組み合わせた業務効率化の考え方は「SE歴20年の業務自動化術」でも詳しく解説しています。

まとめ

GAS×Gemini APIを使い、新着メールの自動分類と返信下書きの自動生成を実装しました。ルールベースのパターン判定とGeminiによるAI分類のハイブリッド構成により、定型メールは高速に処理しつつ、想定外のメールにも柔軟に対応できます。

実際に動かしてみて、無料枠のRPD上限が想像より厳しいこと、モデルを切り替えればRPDが別カウントで回避できること、スクリプトの作成場所(スタンドアロン vs バインド)で挙動が変わることなど、ドキュメントだけでは分からないハマりポイントが複数ありました。本記事のコードはこれらの問題をすべて解決済みです。

▶ GASによるメール集約からNotebookLMでの思考整理まで、一連の業務自動化の流れはこちらでまとめています。
GAS×NotebookLM実践記|本業メール集約を自動化する術

▶ GAS以外にもSendToカスタマイズやSeleniumなど、SE歴20年の実務で使っている自動化テクニックの全体像はこちらです。
SE歴20年の業務自動化術|SendTo・Selenium・AIエージェントまで

2026.03.25 ─ Gemini 2.5 Flashシャットダウン予定情報・Gemini 3系移行ガイダンスを追記
2026.03.20 ─ Gemini 2.0 Flash非推奨化に伴うモデル変更・レート制限表更新
2026.02.22 ─ 初版公開

タイトルとURLをコピーしました