GASでGmailの受信メールを
AIで自動ラベル分類する方法

受信トレイのメールをClaude APIに渡し、「見積依頼」「サポート」「営業」などのラベルを本文の内容から自動で付ける仕組みを、動くコードで解説します。 スレッド単位で処理し、確信度が低いメールだけ人が確認する運用にすることで、仕分けの手間を減らしつつ大事なメールの取りこぼしを防ぎます。

|対象: GAS / Gmail / Claude API

Table of Contents

なぜメールの仕分けをAIで自動化するのか

受信トレイには、見積依頼・サポートの問い合わせ・営業メールなど、性質の違うメールが混ざって届きます。 毎回開いて中身を読み、手でフォルダやラベルに振り分けるのは地味に時間がかかります。 急ぎの見積依頼が営業メールに埋もれて気付くのが遅れる、といった取りこぼしも起きがちです。

そこで、受信メールの本文をClaude APIに渡し、あらかじめ決めた候補ラベルのどれかに自動で分類します。 Gmailのラベルはスレッド単位で付くので、スレッドごとに1回だけAIを呼び、判定結果をラベルとして反映します。 確信度が低いものだけ「要確認」を付けておけば、人はそこだけ見ればよくなります。

本文で判断

件名だけでなく本文の内容からラベルを判定するので、精度が上がる

候補を固定

使うラベルをコードで固定し、AIが勝手に新しいラベルを作らないようにする

確信度で切り分け

自信のない判定には「要確認」を付け、人が見直す対象を絞る

二重処理を防止

処理済みスレッドに管理用ラベルを付け、検索から外して重複を防ぐ

APIキーを安全に保管する

使うのはClaude APIキーです。コードに直接書くと共有時に漏れる恐れがあるため、PropertiesService(スクリプトごとの設定値を安全に保管する仕組み)のスクリプトプロパティに保存します。 コードからはgetProperty()で読み込みます。

// スクリプトプロパティにAPIキーを保存(初回1回だけ実行)
function saveApiKey() {
  PropertiesService.getScriptProperties()
    .setProperty("CLAUDE_API_KEY", "sk-ant-xxxxxxxx");
}

function getApiKey() {
  const key = PropertiesService.getScriptProperties().getProperty("CLAUDE_API_KEY");
  if (!key) throw new Error("APIキーが未設定です。saveApiKey を実行してください。");
  return key;
}

Claude APIの基本的な呼び出し方は、GASからClaude APIを呼び出して問い合わせを自動分類する方法で詳しく解説しています。

候補ラベルと管理用ラベルを用意する

まず、どんなラベルに分類するかをLABELSに並べて固定します。候補を固定しておくのは、AIが毎回ばらばらの名前でラベルを作ってしまうのを防ぐためです。 あわせて、処理済みの目印にするAI分類済みと、自信のない判定に付ける要確認ラベルも決めておきます。

// 分類したいラベルの候補(自社の運用に合わせて書き換える)
const LABELS = ["見積依頼", "サポート", "営業", "請求", "その他"];

// 処理済みの目印に使う管理用ラベル
const DONE_LABEL = "AI分類済み";
// 確信度が低いメールに付ける確認用ラベル
const REVIEW_LABEL = "要確認";
// 確信度がこの値未満なら「要確認」を付ける(0〜100)
const CONFIDENCE_THRESHOLD = 60;

// ラベルが無ければ作り、あれば取得する
function getOrCreateLabel(name) {
  return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
}

getOrCreateLabel()は、指定した名前のラベルがあれば取得し、無ければcreateLabel()で新しく作る小さな関数です。ラベルを事前に手作業で作らなくても、実行時に自動でそろいます。 分類先を増やしたいときはLABELSに追記するだけで、プロンプトの選択肢も自動で切り替わります。

メール本文からラベルと確信度をJSONで受け取る

ここが今回の中心です。件名と本文をAIに渡し、ラベルと確信度を JSON(キーと値をまとめて扱う形式)で受け取ります。 プロンプトの選択肢はLABELSから自動で組み立てるので、候補を変えても書き換えは1か所で済みます。

// 1スレッドの本文から、ラベルと確信度をJSONで受け取る
function classifyMail(subject, body) {
  const choices = LABELS.map(function (l) { return '"' + l + '"'; }).join(", ");

  const instruction =
    "あなたは企業のメール仕分け担当です。\n" +
    "次のメールを、決められたラベルのどれか1つに分類してください。\n" +
    "件名: " + subject + "\n" +
    "本文: " + body + "\n" +
    "使えるラベル: [" + choices + "]\n" +
    "条件:\n" +
    "・必ず上の一覧にあるラベルの中から1つだけ選ぶ。\n" +
    "・判断に迷う・当てはまらない場合は「その他」にする。\n" +
    "・確信度は0〜100の整数で、確からしさを表す。\n" +
    "余計な説明は出力せず、JSONだけを返してください。\n" +
    '形式: {"label": "ラベル名", "confidence": 数値}';

  const payload = {
    model: "claude-sonnet-4-20250514",
    max_tokens: 200,
    messages: [{ role: "user", content: instruction }],
  };

  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": getApiKey(),
      "anthropic-version": "2023-06-01",
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });

  if (res.getResponseCode() !== 200) {
    throw new Error("API error: " + res.getContentText());
  }

  const responseText = JSON.parse(res.getContentText()).content[0].text;
  const result = JSON.parse(extractJson(responseText));

  // 一覧に無いラベルが返ってきたら「その他」に寄せる
  if (LABELS.indexOf(result.label) === -1) result.label = "その他";
  return result; // { label, confidence }
}

// レスポンスから { ... } の部分だけを取り出す(前後の余計な文字対策)
function extractJson(text) {
  const start = text.indexOf("{");
  const end = text.lastIndexOf("}");
  return text.slice(start, end + 1);
}

AIが候補にないラベル名を返すことがあるため、一覧に無ければその他に寄せています。返答に前後の説明文が混ざる場合に備えて、extractJson(){から}までを切り出してから解析し、崩れにくくしています。 モデル名やパラメータは、利用中のプランやAPIの最新仕様に合わせて調整してください。

スレッドの最新メールから本文を取り出す

分類にはスレッドの最新メールを使います。thread.getMessages()で古い順にメールの配列が取れるので、その末尾が最新のメールです。 本文はgetPlainBody()でHTMLタグを除いたテキストとして取り出します。

// スレッドの最新メールから、AIに渡す本文を取り出す
function getLatestBody(thread) {
  const messages = thread.getMessages();
  const last = messages[messages.length - 1]; // 最新のメール
  // 本文が長すぎるとトークンを消費するため先頭2000文字に絞る
  const body = last.getPlainBody().slice(0, 2000);
  return { subject: last.getSubject(), body: body };
}

長いメールをそのまま送るとトークン消費が増え、GASの実行時間制限にも近づきます。 分類の判断には冒頭で十分なことが多いので、本文は先頭2000文字に絞っています。 引用や署名で長くなるメールが多い場合は、この上限を調整してください。

未処理スレッドだけにラベルを付ける

受信トレイのうち、まだ処理していないスレッドだけをGmailApp.search()で取得します。検索条件に-label:AI分類済みを含めると、処理済みラベルが付いたスレッドを最初から除外できます。 判定したラベルをスレッドに付け、確信度が低ければ「要確認」も添え、最後に処理済みの目印を付けます。

// 未処理スレッドを検索し、ラベルを付ける
function labelInbox() {
  const doneLabel = getOrCreateLabel(DONE_LABEL);
  const reviewLabel = getOrCreateLabel(REVIEW_LABEL);
  // 受信トレイのうち、まだAI分類していないスレッドだけを取得
  const query = "in:inbox -label:" + DONE_LABEL;
  const threads = GmailApp.search(query, 0, 15); // 1回最大15スレッド

  threads.forEach(function (thread) {
    const mail = getLatestBody(thread);

    let result;
    try {
      result = classifyWithRetry(mail.subject, mail.body);
    } catch (e) {
      Logger.log("分類に失敗: " + e.message);
      return; // このスレッドは飛ばして次へ(完了印は付けない)
    }

    // 判定されたラベルをスレッドに付ける
    getOrCreateLabel(result.label).addToThread(thread);
    // 確信度が低ければ「要確認」も付ける
    if (Number(result.confidence) < CONFIDENCE_THRESHOLD) {
      reviewLabel.addToThread(thread);
    }
    // 処理済みの目印を付ける(次回の検索から外れる)
    doneLabel.addToThread(thread);

    Utilities.sleep(500); // 連続呼び出しの間隔を少し空ける
  });

  Logger.log(threads.length + "スレッドを分類しました。");
}

分類に失敗したスレッドには、あえて処理済みの目印を付けずに飛ばします。 こうしておくと、一時的なエラーで分類できなかったメールも、次回の実行でもう一度拾い直せます。 1回あたりの処理件数はsearch(query, 0, 15)の第3引数で15件に抑え、実行時間制限に収めています。

リトライで一時的なエラーに備える

APIは、混雑や通信の乱れで一時的に失敗することがあります。 1回の失敗で止めず、少し待ってから再試行するリトライを挟むと、まとめて処理するときに安定します。 最後まで失敗したメールはエラーを呼び出し元に返し、そのスレッドだけ飛ばして処理全体は止めないようにします。

// 一時的なAPIエラー時に最大3回まで再試行する
function classifyWithRetry(subject, body, maxRetry) {
  const limit = maxRetry || 3;
  for (let attempt = 1; attempt <= limit; attempt++) {
    try {
      return classifyMail(subject, body);
    } catch (e) {
      Logger.log("試行" + attempt + "回目で失敗: " + e.message);
      if (attempt === limit) throw e; // 最後まで失敗したら呼び出し元へ返す
      Utilities.sleep(1000 * attempt); // 待ち時間を伸ばして再試行
    }
  }
}

時間主導トリガーで自動実行する

Gmailには受信の瞬間に発火するトリガーがありません。そこで、時間主導トリガー(一定間隔で自動実行する仕組み)で数分おきに未処理スレッドを拾う形にします。 10分間隔にしておけば、届いたメールはおおむね10分以内にラベルが付きます。

// 10分おきに自動でラベルを付ける時間主導トリガーを登録する
function setupTrigger() {
  // 二重登録を防ぐため、同名の既存トリガーを消してから作る
  ScriptApp.getProjectTriggers().forEach(function (t) {
    if (t.getHandlerFunction() === "labelInbox") {
      ScriptApp.deleteTrigger(t);
    }
  });
  ScriptApp.newTrigger("labelInbox")
    .timeBased()
    .everyMinutes(10)
    .create();
}

トリガーを何度も登録すると同じ関数が二重に動いてしまうため、setupTrigger()では既存の同名トリガーを消してから作り直しています。 トリガーの仕組みや実行時間制限の回避策は、GASの6分実行時間制限を回避する3つの実装パターンでも解説しています。

実務での注意点

1. 大事なメールはラベルだけで判断しない

AIの分類は便利ですが、完璧ではありません。金額や契約に関わるメールは、ラベルを目安にしつつ本文を必ず確認してください。 確信度が低いものには「要確認」が付くので、まずはそこを人が見直す運用にすると取りこぼしを防げます。

2. 個人情報を含む本文の扱いに注意する

メール本文には氏名や連絡先などの個人情報が含まれることがあります。外部APIに送る以上、社内のルールや取引先との取り決めに反しないかを事前に確認してください。 必要なら、送信する本文の文字数を絞る・特定の差出人だけを対象にするなど、範囲を限定するのが安全です。

3. まず少数のスレッドで試す

いきなり受信トレイ全体に適用せず、まずは対象を絞って分類の精度を確かめてください。 検索条件をin:inbox newer_than:1dのように直近だけに限定すると、様子を見ながら少しずつ範囲を広げられます。

4. 実行時間とコストの上限に注意する

GASには1回あたり6分の実行時間制限があります。スレッドごとにAPIを呼ぶため、件数が多いほど時間とトークン消費が増えます。 1回の処理件数に上限を設け、残りは次回のトリガー実行に回す形にすると安定します。

まとめ

メールの自動ラベル分類は、候補ラベルを固定し、スレッドの本文をClaude APIに渡してラベルと確信度をJSONで受け取り、処理済みスレッドを検索から外しながらラベルを付ければ、まとめて自動化できます。 確信度が低いメールには「要確認」を添え、時間主導トリガーで定期実行すれば、届いたメールがほぼ自動で仕分けられます。 仕分けの下ごしらえをAIに任せ、人は「要確認」と大事なメールの中身の確認に集中してください。

よくある質問

多くのメールは正しく分類できますが、鵜呑みは避けてください。この記事のコードは、AIが返す確信度が低いメールには「要確認」ラベルを別に付けて、人が後で見直せるようにしています。特に金額や契約に関わるメールは、ラベルだけで判断せず本文を必ず確認する運用にすると安全です。まずは自動でラベルを付け、判断が難しいものだけ人が拾う、という切り分けが現実的です。

この記事ではスレッド(同じ話題でやり取りが続くメールのまとまり)単位で分類します。Gmailのラベルはスレッドに付くため、スレッドごとに1回だけAIを呼ぶ方が、API呼び出し回数もトークン消費も抑えられます。判定には最新のメール本文を使い、付けたラベルはスレッド全体に反映します。1通ずつ処理すると同じスレッドに何度もラベルを付け直すことになり、無駄が増えます。

防いでいます。この記事では処理が終わったスレッドに「AI分類済み」という管理用ラベルを付け、次回以降はそのラベルが付いたスレッドを検索対象から外します。GmailApp.search() の検索条件に -label:AI分類済み を含めることで、未処理のスレッドだけを取得できます。これでトリガーを定期実行しても、同じメールを二重に処理しません。

はい。この記事では候補ラベルをコードの LABELS 配列にまとめて固定しています。ここを自社の運用に合わせて「見積依頼」「サポート」「請求」などに書き換えるだけで、プロンプトの選択肢も自動で切り替わります。候補を固定するのは、AIが毎回ばらばらの名前のラベルを作ってしまうのを防ぐためです。存在しないラベルが返ってきた場合は「その他」に寄せる作りにしています。

時間主導トリガーで数分おきに実行すれば、ほぼ自動で付けられます。Gmailには受信の瞬間に発火するトリガーはないため、5分〜15分間隔の時間主導トリガーで未処理スレッドを拾う形が実用的です。急ぎの通知が必要なら、特定ラベルが付いたときにSlackへ知らせる仕組みと組み合わせる方法もあります。

直接書くのは避けてください。スクリプトを共有した際に漏れる恐れがあります。Claude APIキーは PropertiesService(スクリプトごとの設定値を安全に保管する仕組み)のスクリプトプロパティに保存し、コードからは getProperty() で読み込む方法が安全です。

メール業務の
自動化を相談する。

ラベル分けのほかにも、要約・緊急度判定・通知など、AIとGASを組み合わせたメール業務の自動化をご相談いただけます。

AI×GAS自動化を見る