GASでGmailの必要なメールだけ
取得する検索クエリ7選

Gmail自動化がうまくいかない原因の多くは、処理ロジックではなくGmailApp.search()に渡す検索クエリの精度にあります。実務で使う演算子と、二重処理を防ぐラベル設計、件数上限への対処までまとめます。

|対象: GAS / Gmail / 検索クエリ

Table of Contents

GmailApp.searchの基本とスレッドの考え方

結論から書くと、GmailApp.search()に渡す文字列は、Gmailの画面上部にある検索窓に入力するものとまったく同じです。 まずGmailの画面で検索して結果を目視で確認し、その文字列をコードに貼り付けるのが一番確実な進め方になります。

つまずきやすいのは戻り値です。search()が返すのはメール1通ずつではなく、スレッド(返信のやり取りをひとまとめにした会話)の配列です。 件名や本文を読むには、スレッドからメッセージ(1通のメール)を取り出す必要があります。

function searchBasic() {
  // 検索クエリはGmailの検索窓に入れる文字列と同じ書き方
  const query = 'from:info@example.com is:unread';

  // 返ってくるのは「スレッド(会話)」の配列
  const threads = GmailApp.search(query);
  Logger.log('ヒットしたスレッド数: ' + threads.length);

  threads.forEach((thread) => {
    // 1スレッドの中に往復した複数のメッセージが入っている
    const messages = thread.getMessages();
    messages.forEach((message) => {
      Logger.log([
        message.getDate(),
        message.getFrom(),
        message.getSubject(),
      ].join(' / '));
    });
  });
}

1スレッドに複数の返信が含まれるため、「10件ヒットした」はメール10通ではなく会話10件を意味します。 最新の1通だけを処理したい場合は、メッセージ配列の末尾を取るのが定番です。

実務で使う検索演算子7選

演算子(検索条件を指定するキーワード)は数多くありますが、業務自動化で実際に使うのはほぼ次の7つに絞られます。 条件を半角スペースでつなぐとAND条件になり、先頭にハイフンを付けると除外になります。

from: / to:

差出人・宛先で絞る。取引先や通知メールの自動処理で最初に使う条件

subject:

件名で絞る。日本語やスペースを含む語は引用符で囲む

has:attachment

添付ファイルがあるメールだけ。請求書・納品書の回収に必須

filename:

ファイル名や拡張子で絞る。filename:pdf のように書く

label: / -label:

ラベルの指定と除外。処理済みメールのスキップに使う

newer_than: / after:

期間で絞る。newer_than:7d は直近7日という意味

in:anywhere

アーカイブ済み・迷惑メールも含めて全体を検索する

// 1. 差出人で絞る
'from:billing@example.co.jp'

// 2. 件名に特定の語を含む(日本語やスペース入りは引用符で囲む)
'subject:"請求書"'

// 3. 添付ファイル付きだけ
'has:attachment'

// 4. 拡張子・ファイル名で絞る
'filename:pdf'

// 5. 特定ラベルのみ / 特定ラベルを除外(処理済みの除外に必須)
'label:受注 -label:処理済み'

// 6. 期間で絞る(newer_than は d=日 / m=月 / y=年)
'newer_than:7d'

// 7. アーカイブ済み・全フォルダを対象にする
'in:anywhere'

// 組み合わせは半角スペースでAND、OR は大文字で書く
'from:billing@example.co.jp subject:"請求書" has:attachment -label:処理済み newer_than:30d'

OR条件を使うときは、小文字の or ではなく大文字のORで書く必要があります。小文字のままだと、ただの検索語として扱われて意図しない結果になります。

日付を動的に組み立てて期間で絞る

定期トリガーで動かすスクリプトでは、検索範囲を「直近◯日」に限定するとムダな処理を減らせます。 単純な相対指定でよければnewer_than:7dで足ります。月初から月末までのように範囲を明示したいときは、after:before:を使い、日付を文字列に整形して埋め込みます。

function buildDateRangeQuery(daysAgo) {
  const tz = 'Asia/Tokyo';
  const now = new Date();
  const from = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);

  // after: / before: は「yyyy/MM/dd」形式の文字列を渡す
  const afterStr = Utilities.formatDate(from, tz, 'yyyy/MM/dd');
  const beforeStr = Utilities.formatDate(now, tz, 'yyyy/MM/dd');

  return `subject:"請求書" after:${afterStr} before:${beforeStr}`;
}

function searchLastWeek() {
  const query = buildDateRangeQuery(7);
  Logger.log('query = ' + query);

  const threads = GmailApp.search(query);
  Logger.log('件数: ' + threads.length);
}

ポイントは、Utilities.formatDate()(日付を指定書式の文字列に変換するGASの関数)で必ずタイムゾーンに「Asia/Tokyo」を渡すことです。 省略するとスクリプトの設定によっては日付が1日ずれ、境界のメールを取りこぼします。 なおbefore:はその日を含まないため、当日分まで確実に拾いたい場合は翌日の日付を指定します。

ラベルで処理済みを除外し二重処理を防ぐ

定期実行するスクリプトで最も多いトラブルが、同じメールを何度も処理してしまう二重処理です。 対策はシンプルで、処理が終わったスレッドに「処理済み」ラベルを付け、検索クエリ側で-label:処理済みと除外します。次回の検索では最初から対象に入らなくなります。

const DONE_LABEL = '処理済み';

function processUnhandledMails() {
  // 処理済みラベルが付いたスレッドは検索の時点で除外する
  const query = `label:受注 -label:${DONE_LABEL} newer_than:30d`;
  const threads = GmailApp.search(query, 0, 50);
  if (threads.length === 0) return;

  const label = getOrCreateLabel_(DONE_LABEL);

  // 各スレッドのメッセージをまとめて取得(1件ずつより速い)
  const messagesByThread = GmailApp.getMessagesForThreads(threads);

  threads.forEach((thread, i) => {
    const messages = messagesByThread[i];
    const latest = messages[messages.length - 1];

    try {
      handleMessage_(latest);
      // 成功したときだけラベルを付ける(失敗分は次回また拾える)
      thread.addLabel(label);
    } catch (err) {
      console.error('処理に失敗: ' + latest.getSubject() + ' / ' + err);
    }
  });
}

function getOrCreateLabel_(name) {
  return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
}

function handleMessage_(message) {
  const sheet = SpreadsheetApp.getActive().getSheetByName('受注ログ');
  sheet.appendRow([
    message.getDate(),
    message.getFrom(),
    message.getSubject(),
    message.getPlainBody().slice(0, 500),
  ]);
}

設計上の要点は2つです。1つ目は、ラベルを付けるのは処理が成功した後にすること。 先にラベルを付けると、途中でエラーが出たメールが二度と拾われなくなります。 2つ目はGmailApp.getMessagesForThreads()を使うこと。スレッドごとにgetMessages()を呼ぶより通信回数が減り、件数が多いほど速度差が出ます。

500件の上限とページングの書き方

GmailApp.search()が1回で返せるのは最大500スレッドです。過去メールをまとめて棚卸しするようなケースでは、 第2引数に開始位置、第3引数に取得件数を渡してページングします。

function searchAllThreads(query) {
  const PAGE_SIZE = 100; // 1回の上限は500。安全側に100程度が扱いやすい
  const all = [];
  let start = 0;

  while (true) {
    // 第2引数=開始位置、第3引数=取得件数
    const threads = GmailApp.search(query, start, PAGE_SIZE);
    if (threads.length === 0) break;

    all.push(...threads);
    start += threads.length;

    // 取得数がページサイズ未満なら最終ページ
    if (threads.length < PAGE_SIZE) break;

    // 6分の実行時間制限に当たらないよう上限を決めておく
    if (all.length >= 500) {
      Logger.log('上限に達したため打ち切り。続きは次回実行で処理する');
      break;
    }
  }

  return all;
}

ただしGASには1回の実行あたり6分という時間制限があります。全件を1回で処理しようとせず、 1実行あたりの上限を決めて打ち切り、残りは次のトリガー実行に回すほうが安定します。 前節のラベル除外と組み合わせれば、実行のたびに未処理分だけが自然に前に詰まっていきます。

添付ファイル付きメールだけを処理する

請求書や納品書の回収は、検索クエリの精度がそのまま運用の手間に直結します。has:attachmentfilename:pdfを併用し、さらにコード側でMIMEタイプ(ファイル種別を表す文字列)を確認すると取り違えを防げます。

function saveInvoicePdfToDrive() {
  const folder = DriveApp.getFolderById('フォルダIDをここに');
  const query = 'subject:"請求書" has:attachment filename:pdf -label:保存済み newer_than:14d';

  const threads = GmailApp.search(query, 0, 30);
  const label = getOrCreateLabel_('保存済み');
  const messagesByThread = GmailApp.getMessagesForThreads(threads);

  threads.forEach((thread, i) => {
    messagesByThread[i].forEach((message) => {
      // 画像などのインライン添付は除外する
      const attachments = message.getAttachments({
        includeInlineImages: false,
        includeAttachments: true,
      });

      attachments
        .filter((file) => file.getContentType() === 'application/pdf')
        .forEach((file) => {
          const name = Utilities.formatDate(message.getDate(), 'Asia/Tokyo', 'yyyyMMdd')
            + '_' + file.getName();
          folder.createFile(file.copyBlob()).setName(name);
        });
    });
    thread.addLabel(label);
  });
}

getAttachments()は既定で署名画像などのインライン画像も拾ってしまうため、includeInlineImages: falseを明示しておくと余計なファイルが保存されません。

検索が空振りするときの確認手順

1. まずGmailの検索窓で同じ文字列を試す

コードを疑う前に画面で検証します。画面で0件ならクエリの問題、画面ではヒットするのにコードで0件なら実行アカウントの問題です。 共有アカウントのメールを個人アカウントのスクリプトから探そうとしていないか確認してください。

2. コロンの後ろの空白と引用符を疑う

subject: 請求書のようにコロンの後ろに空白が入ると、条件として認識されません。 スペースを含む語や日本語は引用符で囲みます。

3. 受信トレイ以外を見ているか確認する

フィルタで自動アーカイブされているメールは、条件によっては検索対象から外れます。in:anywhereを付けて範囲を広げると解決することがあります。

4. 送信直後のメールは待つ

Gmailの検索は検索インデックスを参照するため、届いた直後のメールが即座にヒットするとは限りません。 テスト時に「送ったのに見つからない」場合は、少し時間をおいて再実行してください。

5. ログにクエリ文字列そのものを出す

変数を埋め込んで組み立てたクエリは、意図した文字列になっていないことがよくあります。 実行前にLogger.log(query)で必ず中身を確認しましょう。

まとめ

Gmail自動化の品質は、検索クエリをどれだけ絞り込めるかでほぼ決まります。 from・subject・has:attachment・filename・label・newer_than・in:anywhere の7つを押さえ、 処理済みラベルによる除外と組み合わせれば、二重処理もムダな走査もない安定した仕組みになります。 件数が多い場合はページングと実行件数の上限を設け、6分の実行時間制限に当たらないよう分割して回すのが実務的です。

よくある質問

同じです。GmailApp.search()に渡す文字列は、Gmailの画面上部の検索窓に入力するものとまったく同じ演算子が使えます。そのため、まずGmailの画面で検索して意図した結果になることを確かめ、その文字列をそのままコードに貼り付けるのが最も確実です。

1回の呼び出しで取得できるのは最大500スレッドです。それ以上を扱う場合はGmailApp.search(query, start, max)の第2引数・第3引数で開始位置と件数を指定し、ページングしながら回します。ただしGASには6分の実行時間制限があるため、1回の実行で処理する件数を絞り、続きは次回のトリガー実行に回す設計が安全です。

Gmailは返信のやり取りをスレッド(会話)としてまとめて管理します。GmailApp.searchが返すのはスレッドの配列で、1つのスレッドの中に往復した複数のメッセージ(1通1通のメール)が入っています。本文や送信者を読むにはスレッドからメッセージを取り出す必要があり、GmailApp.getMessagesForThreads()を使うとまとめて高速に取得できます。

Utilities.formatDate()でDateオブジェクトを「yyyy/MM/dd」形式の文字列に変換し、テンプレートリテラルでクエリに埋め込みます。タイムゾーンには必ず「Asia/Tokyo」を指定してください。単純に「直近7日」でよければnewer_than:7dと書くほうが簡単です。

処理が終わったスレッドに「処理済み」ラベルを付け、検索クエリ側で -label:処理済み と除外するのが定番です。ラベルは処理の直後に付け、次回の検索では最初から対象外になるようにします。より厳密にしたい場合は、メッセージIDをスプレッドシートに記録して照合する方法と併用します。

まずGmailの検索窓に同じ文字列を入れて件数を確認してください。よくある原因は、演算子とコロンの後ろに余計な空白が入っている、日本語の件名を引用符で囲っていない、アーカイブ済みメールを対象にしていない(in:anywhereが必要)、送信直後で検索インデックスにまだ反映されていない、の4つです。

Gmail業務の自動化を
相談する。

受注メールの転記、請求書PDFの回収、問い合わせの自動振り分けなど、Gmailを起点にした業務自動化をGASでご支援します。

AI×GAS業務自動化を見る