0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gmail APIが返した件数を信じない——条件と補集合に割って総数を確定させる

0
Posted at

この記事の要点

Gmail API(および Gmail の検索画面)は 1 回のリクエストで返る件数に上限があります。上限とちょうど同じ件数が返ってきたら、それは実数ではなく打ち切りの可能性が高い。

ページトークンを辿るのが正攻法ですが、めくり忘れが起きます。代わりに、検索条件を 互いに素な 2 つ(=条件とその補集合) に割り、両方を上限未満に収めて足す方法を紹介します。数を確定させると同時に、「処理対象」と「保護対象」のリストが手に入るのが利点です。

Gmail の受信トレイ整理を題材にしますが、考え方はページネーションのある API 全般に使えます。

発端:100 件で止まっていた

「受信トレイにある 7 日以上前のメールを数える」という処理を書いて、返ってきたのがこれでした。

Found 100 messages matching 'in:inbox older_than:7d'
📄 PAGINATION: To get the next page, call ... with page_token='...'

100 は maxResults の既定値と一致しています。2 行目のトークンが「まだ続きがある」と言っているのに、1 行目の数字だけを見て「100 件くらいか」と受け取りかけました。エラーは出ず、値も返っているので、静かに間違えるタイプの取りこぼしです。

実際の総数は 188 件でした。

前提:使う検索演算子

Gmail API の q パラメータには、検索画面と同じ演算子がそのまま使えます。

in:inbox older_than:7d
  • older_than / newer_than の単位は d / m / y
  • 判定に使われるのは受信日。既読・未読は無関係

除外は演算子の前に - を付けます。

in:inbox older_than:7d -is:starred -is:important -has:attachment
除外条件 意味
-is:starred 自分でスターを付けたもの
-is:important Gmail が自動で「重要」判定したもの
-has:attachment 添付ファイルつき
-from:@example.co.jp 特定ドメイン発を除く
-subject:請求 件名に特定語を含むものを除く
-label:仕事 特定ラベルが付いたものを除く

条件が増えたら括弧でまとめられます。

in:inbox older_than:7d -(is:starred OR is:important OR has:attachment)

本題:補集合に割って数える

maxResults を 500 に上げて逃げることもできますが、上限が変わるだけで問題の構造は同じです。「返ってきた件数が上限に達していないこと」を、数が正しいことの根拠にするほうが確実でした。

そこで条件を 2 本に割ります。

① in:inbox older_than:7d -is:starred -is:important -has:attachment
② in:inbox older_than:7d  (is:starred OR is:important OR has:attachment)

①と②は積集合が空で、和が母集合(in:inbox older_than:7d)に一致します。結果は次のとおりでした。

クエリ 件数 上限(100)未満か
① 除外条件を通り抜けたもの 93 ✅
② 除外条件に当たったもの 95 ✅
合計 188 —

どちらも上限未満なので打ち切られておらず、188 が総数だと確定できます。

Node.js で書くとこの程度です。

const LIMIT = 100;

async function countExact(gmail, q) {
  const res = await gmail.users.messages.list({ userId: 'me', q, maxResults: LIMIT });
  const messages = res.data.messages ?? [];
  if (messages.length >= LIMIT || res.data.nextPageToken) {
    throw new Error(`打ち切りの可能性: q="${q}" (${messages.length}件 / 上限${LIMIT})`);
  }
  return messages;
}

const BASE = 'in:inbox older_than:7d';
const PROTECT = '(is:starred OR is:important OR has:attachment)';

const target    = await countExact(gmail, `${BASE} -is:starred -is:important -has:attachment`);
const protected_ = await countExact(gmail, `${BASE} ${PROTECT}`);

console.log(`総数 ${target.length + protected_.length} 件(対象 ${target.length} / 保護 ${protected_.length})`);

ポイントは、上限に達したら黙って先頭 100 件を返すのではなく例外にするところです。messages.length >= LIMIT と nextPageToken の両方を見ているのは、片方だけだと取りこぼす実装差があるためです。

上限に達したら割り方が粗いということなので、-from: や日付でさらに分割します。

副産物として分類が終わっている

数えるために作った ① と ② が、そのまま「処理してよいもの」と「触らないもの」のリストになります。カウント用のクエリと処理用のクエリを別々に書くと、両者がズレたときに気づけません。同じクエリを使い回せる形にしておくほうが安全でした。

処理は削除ではなくラベル+アーカイブ

Gmail のアーカイブは INBOX ラベルを外すだけの操作で、メール本体は残ります。API なら modify で INBOX を removeLabelIds に入れるだけです。

このとき、同時に作業用ラベルを付けておくと巻き戻せます。

label:"整理済み/2026-08"

ラベル名に /(入れ子)や空白を含む場合は引用符で囲みます。囲まないとスラッシュの前後が別トークンとして解釈されることがあります。入れ子を使わず 整理済み2026-08 のようなフラットな名前にすれば、引用符も不要です。

このラベルで検索 → 全選択 → 受信トレイに移動、で一括ロールバックできます。

画面から一括操作する場合の落とし穴

同じ作業を Gmail の UI からやるときは、ヘッダのチェックボックスを押しただけでは現在ページの分しか選択されません。「この検索条件に一致するスレッドをすべて選択」というリンクを追加で押す必要があります。

API の maxResults と同じ話が UI 側にもある、というだけのことでした。

最後に、機械では割れないものが残る

除外条件を通り抜けた 93 件は、実行前に件名と差出人を全部読みました。

たとえば「メンテナンスのお知らせ」は、実施日が過ぎたものとこれからのものが混在します。件名の形が同一なので、演算子では分けられません。しかも期日超過の基準は「今日」であって「受信日」ではないため、受信日の並び順とも一致しません。1 週間前に届いた「あと 5 日で締切」は既に過ぎていて、10 日前に届いた「来月のメンテ」はまだ生きています。

クエリで安全に落とせる範囲を最大化して、残りを目で見る。この線引きを最初に決めておくのが、一括処理をやるときの実務的な落としどころだと思います。

まとめ

  • 返却件数が上限と一致したら、実数ではなく打ち切りを疑う
  • ページを辿る代わりに、条件とその補集合に割って両方を上限未満に収める
  • 割った結果はそのまま「処理対象」と「保護対象」になる
  • 破壊的操作は避け、ラベル付け+アーカイブで巻き戻せる形にする
  • UI の一括選択にも同じ「見えている分だけ」問題がある

演算子の一覧は Gmail ヘルプ「Gmail で使用できる検索演算子」 にまとまっています。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?