mcpField
M
mailwarden

mailwarden

ローカル
github.com/csitteコミュニケーション01 日前に更新

mailwarden について

mailwardenは、AIアシスタント向けにGmailのメール整理機能を提供するMCPサーバーです。Gmailの最新データを検索し、現在のラベルで結果を再確認します。メールボックス側のスヌーズや一括操作に対応し、メール送信ツールは意図的に備えていません。

主な機能

  1. 01最新ラベルで検索結果を再確認
  2. 02メールボックス側のスヌーズ
  3. 03アーカイブとラベルの一括操作
  4. 04配信停止の管理
  5. 05送信者認証結果の確認
  6. 06構造化されたツール出力

活用シーン

  1. 01未読やカテゴリ別メールの検索
  2. 02検索条件に合うメールの整理
  3. 03指定日にスレッドを受信トレイへ戻す
  4. 04送信者の認証情報の確認

MCP 対応

Gmailのメール整理機能をMCPツールとして提供し、出力スキーマを宣言して構造化データを返します。検索、スレッド確認、ラベル、アーカイブ、スヌーズ、配信停止に対応しますが、メール送信機能はありません。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"csitte-mailwarden": {
"command": "npx",
"args": [
"-y",
"mailwarden"
],
"env": {
"MAILWARDEN_DIR": "<MAILWARDEN_DIR>",
"MAILWARDEN_CREDENTIALS": "<MAILWARDEN_CREDENTIALS>",
"MAILWARDEN_AUTO_SWEEP": "<MAILWARDEN_AUTO_SWEEP>",
"MAILWARDEN_DOWNLOAD_DIR": "<MAILWARDEN_DOWNLOAD_DIR>"
}
}
}
}
起動コマンドnpx -y mailwarden
設定が必要な環境変数
MAILWARDEN_DIRConfig directory (default ~/.mailwarden), holds credentials.json and token.json
MAILWARDEN_CREDENTIALSシークレットPath to the Google OAuth credentials.json (Desktop app client)
MAILWARDEN_AUTO_SWEEPSet to 1 to sweep due snoozes at startup and hourly while running
MAILWARDEN_DOWNLOAD_DIRRestrict download_attachment and export_thread writes to this directory (recommended for HTTP hosting)

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
npmmailwarden0.25.0

ツール

26

掲載時のツール一覧 · 1 日前に更新

パラメーター型必須説明
querystringはい
pageTokenstringいいえ
maxResultsintegerいいえ

詳細

mailwarden

npm license Node Website Smithery

信頼性の高いネイティブな Gmail MCP サーバーです。AI アシスタント向けにメールボックスを包括的にトリアージでき、他の Gmail MCP サーバーにはない機能、メールボックス側のスヌーズを備えています。

主な特長

  • スヌーズ — Gmail MCP サーバーで唯一のメールボックス側スヌーズです。 スレッドを今すぐアーカイブし、指定日に受信トレイへ再表示できます。日付付きラベルと定期処理を基盤としているため、どのクライアントからでも機能し、Gmail 自体でも確認でき、再起動後も維持されます。(別のサーバーが「スヌーズ」を提供していても、それはローカルのリマインダーリストであり、メールが受信トレイから出たり、戻ったりすることはありません。)
  • 信頼できる検索。 スレッド検索がすべて経由する Gmail の threads.list は、is:unread に対して古いスレッド単位の既読状態を基に回答することがあります。実際のメールボックスで測定したところ、返されたスレッドの86%には未読メッセージがまったくありませんでした。別のメールボックスでは、ずれはまったくありませんでした。確認しない限り、どちらのメールボックスなのかは判断できないため、search はすべての検索結果をライブのラベルで再検証します。pageToken/nextPageToken によるページネーションに対応しています。(測定結果)
  • 送信者の表記ではなく、送信者認証を確認します。 get_thread は、受信サーバーが記録した SPF/DKIM/DMARC の結果を報告するため、「本当に銀行からのメールなのか?」を、ドメインの見た目ではなくメッセージ自身のヘッダーに基づいて確認できます。読み取るのは Gmail 自身のレポートのみです。つまり、mx.google.com という ID の下に記録されたレポートだけを対象とします(メッセージには偽造されたレポートが含まれている場合があります)。通過した DKIM 署名が From ドメインに属しているかを示し、誰も確認していない場合は unchecked と表示します。確認されていないことは、確認に通ったことを意味しないためです。
  • 大規模処理にも対応する一括操作。 bulk_modify は、API リクエストごとに最大 1000 件のメッセージを処理し、クエリに一致するすべてのメッセージをアーカイブまたはラベル付けします。全件成功か全件失敗かの二択ではなく、チャンクごとに部分的な成功を報告します。スヌーズの定期処理も同じバッチ処理を使用します。
  • 構造化出力。 すべてのツールが outputSchema を宣言し、フェンス付き JSON テキストとともに、検証済みの structuredContent を返します。クライアントが推測しながら解析する必要はありません。失敗も構造化されており、code と retryable フラグを備えています。そのため、クライアントは文章を読み解かなくても、「後でもう一度試す」べきか「再認証する」べきかを判断できます。
  • 攻撃対象領域を最小限に抑えています。 送信ツールはなく(プロンプトインジェクションを受けたメールによるデータ流出経路を排除)、読み取り専用モードは任意で有効にでき、テレメトリーはなく、既定ではポートを開かず、シンボリックリンクに対して安全なダウンロード制限を備え、出力へのインジェクションも防ぎます。また、送信できてしまうコード経路もありません。すべての Gmail リクエストは、侵害された、あるいは不注意な呼び出し元からの要求であっても、messages.send、すべての下書きエンドポイント、完全削除、転送設定を拒否する送信チェックポイントを通過します。意図的な例外が 1 つあります。 unsubscribe/bulk_unsubscribe(manage ティア)は、メッセージ自身のヘッダーに記載されたオプトアウトエンドポイントに接続します。mailwarden がアクセスする唯一の非 Google ホストです。read ティアのデプロイでは、外部へのリクエストは一切行いません。詳細はセキュリティとプライバシーおよび購読解除をご覧ください。
  • 実際のメールでも正しく動作します。 RFC 2047 ヘッダーをデコードします(=?UTF-8?B?…?= → 読みやすいテキスト)。本文はバイト列と宣言された文字セットに基づいてデコードします。Gmail がすでに UTF-8 に変換したテキストは、ヘッダーの内容にかかわらず UTF-8 として読み取り、それ以外は指定された文字セットで読み取ります(ISO-8859-1/Shift_JIS のメールでも文字化けしません)。429/5xx エラーには指数バックオフで再試行します。

理由

メールボックスを同期またはキャッシュするコネクターは、実際のメールボックスより遅れることがあります。また、Gmail 独自の検索インデックスも不正確な場合があります(下記参照)。mailwarden はライブの Gmail API に直接アクセスし(キャッシュされたスナップショットは使用しません)、インデックスが返した結果を再検証するため、表示されるのは実際に存在するものです。これは汎用的な Gmail 機能レイヤーです。独自のルールやロジックはサーバーではなく、AI クライアント側に実装してください。

search は生の API より一歩進んでいます。Gmail の threads.list インデックスは、既読状態を示す演算子に対して、その状態の古いコピーに基づいて回答することがあります。そのため、is:unread は数週間前に読み終えたスレッドを返すことがあり、あるメールボックスで測定したところ、返された結果の大半を占めていました。ヒットしたスレッドはすべて、いずれにしてもライブで取得されるため、search は曖昧さのない述語(is:unread/is:read/is:starred/in:inbox/category:…。否定形も含みます)を各スレッドの実際のラベルと照合し、インデックスの誤検出を除外します。

他の Gmail MCP サーバーとの比較

ほとんどの Gmail MCP サーバーは、読み取り/ラベル付け/送信という同じ機能を備えています。調べた限り、mailwarden だけが備える機能は 2 つ(メールボックス側でのスヌーズ、検索結果の再検証)あり、意図的に省いている機能が 1 つあります。これは欠落ではなく、セキュリティ機能です。送信者認証は、リリース当初は独自機能でしたが、現在はそうではありません。@agentcomms/gmail-mcp は 2026 年 9 月 18 日以降、Authentication-Results を読み取り、authserv-id が mx.google.com であるレポートのみを信頼しています。2026 年 10 月 13 日以降、下表には独立した列として掲載しています。Google 独自のサーバーも、見かけより機能が限定的です。下書きのみで、ゴミ箱、フィルター、登録解除には対応していません。

機能mailwardenGoogle 公式taylorwilsdona-bonusaaronsbklodragentcommsgmail-tidy
メールボックス側のスヌーズ — 今すぐアーカイブし、指定日時またはプリセットで受信トレイに再表示✅———————
検索結果の再検証 — スレッドインデックスの誤検出を、現在のラベルと照合して除外✅———————
送信者認証 — SPF/DKIM/DMARCを、受信サーバーが記録したとおりに各メッセージで確認✅ Gmail独自のレポートのみ。値はトークン検証済み、DKIMアライメント対応—————✅ Gmail独自のレポートのみ。DKIMアライメント対応⚠️ 最初のヘッダーの失敗値を使用。発行元は未確認
検索条件に対する一括処理 — 検索で返されたすべてのスレッドに対して1回の操作を実行✅ リクエストあたり1000件、一部成功に対応—⚠️ 明示的なIDで一括処理。フィルターのapplyは検索条件に対して再ラベル付けするが、再確認なし—✅ 検索条件をプレビュー、最大10,000件、再確認なし⚠️ 明示的なIDで一括処理⚠️ 明示的なIDで一括処理、メッセージごとの正確な取り消し⚠️ 検索条件に対して実行、最大5,000件、プレビューも再確認もなし
配信停止 — 送信者ごとの概要とRFC 8058のワンクリック配信停止。送信スコープは不要✅—⚠️ ヘッダーを表示するが、操作なし————⚠️ リンクを表示するが、操作なし
受信トレイのトリアージ概要 — 待機中のメールを分類する1回の呼び出し✅ 送信者/ラベル/経過時間+ヘッダーシグナル——✅ ヒューリスティックによるフラグ付け+統計⚠️ 未読リストのみ。分類なし——⚠️ 送信者の統計+ニュースレターリスト。分類なし
サーバー側フィルター — アシスタントが介在しなくてもトリアージを続けるルール✅ 転送は一切不可—✅—✅ プレビュー対応。⚠️ 下書きのみのポリシーがない限り転送可能✅ 転送は一切不可—✅ 転送もゴミ箱への移動も不可
送信ツールなし — 設計によるもの — プロンプトインジェクションされたメールに情報流出経路を与えない✅ 作成機能なし⚠️ 下書きのみ❌ 送信可能❌ 送信可能❌ 送信可能。⚠️ オプトインで下書きのみのポリシーを設定可能❌ 送信可能❌ 送信可能。⚠️ メッセージごとの承認が必要。デフォルトではチャット内で実行⚠️ 下書きのみ
最小権限のツール階層 — 有効にしたツールからOAuthスコープを導出✅⚠️ スコープを分割⚠️ --permissionsでサービスごとにスコープを制限(累積または+による和集合)。階層で制限されるのはツールのみ—⚠️ アカウントごとに読み取り/読み書き可能。呼び出し時に適用⚠️ 逆方式:許可されたスコープでツールを制限⚠️ メールボックスごとの階層でスコープを選択。書き込みは呼び出し時に許可内容と照合—
保存時のトークン暗号化✅ デフォルトでAES-256-GCM、鍵はOSキーストアに保存。OSキーストアがない場合はパスフレーズを使用該当なし(ホスト型)⚠️ ファイルモードは0600。GCSではバケットCMEK———✅ デフォルトではOSキーチェーンを使用。任意で所有者のみがアクセスできるファイルに保存✅ デフォルトでAES-256-GCM、鍵はOSキーストアに保存
ベンダーのクラウド不要 — サーバーは自分で運用✅❌ Googleがホスト✅✅✅✅✅✅
構造化出力 — すべてのツールでoutputSchemaを宣言✅✅ 参照ドキュメントでツールごとに説明———⚠️ 2つのツール(download_email、download_all_attachments)に対応。今後追加予定✅ すべてのツール—
2026年10月13日時点のスナップショットです。7つの列の確認日のうち最も古い日付(Googleのリファレンスとa-bonusは、どちらもこの日に最終確認)であり、そのため表全体について主張できる唯一の日付です。npm run table-sourcesが変更を報告するたびに、docs/comparison-sources.jsonに記録されたリビジョンとの差分を読み、各リポジトリの列を最新の状態にしています。2026年10月25日には、そのうち4つが同時に更新されました。aaronsbは9c9f8033(v4.8.0)に更新され、それに伴って2つのセルも更新されました。2026年10月16日以降、クエリに一致するすべての対象にラベル変更を適用します。プレビューで対象件数を数え、IDをサーバー上に保持し、30分間有効な一度限りのトークンを返します(最大10,000件。一致する対象は再確認されません)。また、転送も含め、同じ種類のプレビューを通してフィルターの作成、更新、削除を行います。ただし、ドラフトのみのポリシーが設定されている場合を除きます。taylorwilsdonはfcbf5a2c(v2.2.0)に更新されました。--permissionsはgmail:readonly+sendのような複数レベルの組み合わせも受け付けるようになり、指定したレベルのみを許可します。この点は、最小権限のセルにも記載されています。klodrはe5c080c3(3.0.0)に更新されました。create_filterは、スコープにかかわらず転送アクションを拒否するようになり、フィルターのセルにもその旨が記載されています。agentcommsは261cae4f(0.28.0、宛先ごとの送信ルール付き)に更新されましたが、セルに変更はありません。a-bonusは2026年10月13日以降、更新されていません。Googleのツールリファレンスは2026年10月13日に最終確認されており、11個あるすべてのツールについて出力スキーマを記載しています。現在もデベロッパープレビューで、送信、ゴミ箱への移動、フィルター、スヌーズ、配信停止、クエリ駆動のツールはありません。—は、提供されていない、またはドキュメントに記載されていないことを表します。列には、読者が最も利用する可能性の高いサーバー、つまりGoogleのファーストパーティサーバーと、現在もメンテナンスされているコミュニティサーバーのうち最も規模の大きい3つに加えて、mailwarden自身の最小権限設計に最も近いklodrを掲載しています。agentcomms(@agentcomms/gmail-mcp、2026年10月13日に01cee751を確認)は、2つの理由から同日に追加されました。私たちが知る限り、Authentication-Resultsを読み取るほか唯一のサーバーであり、その読み取りをGmail独自のレポートに限定しています。これは、それまでのmailwardenよりも厳格で、mailwardenが現在同じ方式を採用している理由でもあります。また、初回リリースから1か月以内にnpmでの利用数がklodrを上回りました。一括処理はクエリ単位ではなくID単位ですが、変更のたびにメッセージごとの正確な取り消し情報を返します。mailwardenのbulk_modifyがundo: trueとして採用した設計です。メッセージごとの承認を経て送信します。デフォルトでは会話内の返答が「はい」となっていますが、同プロジェクトのREADMEには、その「はい」がエージェントによるものかどうかをソフトウェアでは判別できないと明記されており、代わりにターミナルでの確認(confirm)か、送信を一切行わない設定(never)を選べます。gmail-tidy(gmail-tidy-mcp、2026年10月25日に6df48c69を確認)は、その立場がこのプロジェクトに最も近いとして同日に追加されました。送信ツールもゴミ箱への移動もなく、どちらかがあるとビルドテストが失敗し、フィルターから転送もゴミ箱への移動もできません。ドラフトは作成するため、送信行ではこのプロジェクトではなくGoogleのサーバーに並びます。同プロジェクトのREADMEには、トークンを使えば送信できるとはっきり記載されています。ある行ではmailwardenより先行していましたが、mailwardenもその後、同じ設計を採用しました。デフォルトでトークンを暗号化し、鍵をOSのキーストアに保存します。一括処理はクエリを対象とし、上限は5,000件ですが、プレビューも再確認もありません。また、どのサーバーが書き込んだかを確認せず、最初のAuthentication-ResultsヘッダーからSPF/DKIM/DMARCの失敗を読み取ります。初回リリースから10日以内にnpmでの利用数がmailwardenと並びました。aaronsbは2026年9月8日に追加されました。これまで含まれていなかった理由は、はっきり述べる価値があります。選定は機能の重なりに基づいていたため、最小の列のサーバーを表に残したまま、その10倍もの規模を持つサーバーを表から除外していました。規模ではなく、送信行に値する存在です。mailwarden以外で、送信を完全に拒否するよう設定できる最初のサーバーでした。GWS_SAFETY_POLICY=draft-only-emailを設定すると、send、reply、replyAll、forwardがブロックされます。その後、agentcommsもnever送信ポリシーで同じ機能を加えました。この行を正確に読み解く鍵は、その約束がどこにあるかです。あちらは送信がデフォルトで、拒否するには運用者が環境変数を設定する必要があります。一方、こちらは送信用のツール自体が一度も実装されていません。同プロジェクトのno-deleteポリシーも、このプロジェクトの「ゴミ箱への移動のみ」というルールと同じ関係にあります。フィルターの行は同じ問題を反対側から問いかけており、3つ目のプロジェクトが、その問いが重要な理由を示しています。c0webster/hardened-google-workspace-mcpは、taylorwilsdonの列に対応するフォークで、この問題だけを理由に作られています。フィルターから転送できてしまうため、create_gmail_filterとdelete_gmail_filterを完全に削除しています。まさにこれがリスクであり、機能を削除して対処しています。mailwardenはその逆で、機能を残したままリスクを取り除いています。create_filterでは転送を指定できず、list_filtersでは既存のフィルターを表示して監査できるようにしています。このフォークは、こうした約束が通常どこで破綻するかも示しています。そのため、上の行ではスコープではなく、ツールの提供範囲とegress guardを扱っています。フォークはgmail.sendを削除する一方で、gmail.compose(「ドラフトの作成/編集のみ(送信は不可)」とコメントされています)とgmail.modifyを残しています。Googleのドキュメントでは、前者を「Manage drafts and send emails」、後者を「Read, compose, and send emails」と説明しています。つまり、この約束は余分なスコープ1つに依存するのではなく、2つのスコープに依存しており、gmail.modifyだけで十分に送信できます。これは、このプロジェクトが独自のmanage階層で保持するものと同じスコープであり、まさにそのために、この行ではツールの提供範囲を示しています。そのトークンで送信できます。送信できないのは、ツール一覧に送信ツールがないからにすぎません。最もインストールされているGmailサーバーが含まれていないのは、見落としではなく、まさにこの理由によります。GongRzhe/Gmail-MCP-Serverはアーカイブ済みで、最終コミットは2025年8月ですが、それでも2026年8月29日までの1か月間にnpmダウンロード数は112,163件に達しました。利用者の多さと最新性は別の問題であり、サーバーが現在何をするかの比較で答えられるのは後者だけです。送信機能はセキュリティ特性として記載しています。mailwardenに送信機能がないのは意図的です(Security & privacyを参照)。暗号化の行では、鍵を誰が保持するかを問います。mailwardenはデフォルトでランダムな鍵をOSのキーストアに保管し、gmail-tidyも同様です。キーストアを利用できない場合は、設定したパスフレーズを使います。パスフレーズも設定しない場合は平文ファイルに保存するため、セルでは両方を記載しています。taylorwilsdonは、ローカルではファイルのアクセス権に依存し、GCSでホストする場合はストレージバケット独自のCMEKに依存します。前者はファイルの盗難に、後者はディスクの盗難に対する保護です。最後の行で問うのは、サーバーがどこで実行されているかではなく、誰が運用するかです。ここではセルフホストが一般的で、表にあるコミュニティサーバーのうち、klodrとgmail-tidy(stdioのみ)以外はすべて何らかのリモートデプロイ方法を提供しています。mailwardenは--http、taylorwilsdonはOAuth 2.1対応のストリーム可能HTTP、a-bonusはCloud Runを介します。自分のホストで実行するのはクラウド上のコピーではありません。ベンダーのホストで実行するのは、クラウド上のコピーです。
優位性は、単一の項目ではなく、スヌーズとリアルタイムの再検証を組み合わせていることにあります。キャッシュされたスナップショットではなく、メールボックスの 現在の 状態に作用する、実際の受信トレイのワークフローレイヤーです。他のサーバーが追いついている点については、上記に正直に記載しています。保存時の暗号化(taylorwilsdon、デフォルトで有効かつ OS のキーストアに鍵を保存する agentcomms と gmail-tidy)、スコープに基づくツール制限(klodr は逆方向、taylorwilsdon はこちらと同じ方向で、訂正前にこの項目で述べていた以上の対応です。--read-only で OAuth フローを読み取り専用スコープマップに切り替えられるほか、2026 年 2 月 25 日以降、サービスごとの権限レベルにも対応しています。--permissions gmail:organize は、累積レベルである readonly、organize、drafts、send、full から要求された権限セットを構築し、ツールレジストリは、宣言されたスコープがその範囲外にあるツールをすべて無効にします。意図的にスコープ階層の拡張を省いているため、organize では実際に gmail.send が除外されます。この項目の主張と異なるのは、強さではなく方向です。レベルは有効にするツールから導出されるのではなく、サービスごとに選択されるため、レベルを指定しない限り --tool-tier core --tools gmail でも Gmail の全スコープが要求されます。また、どちらのモードにもリクエスト時のガードはありません。8 月の調査では tier の経路しか確認せず、ここでは 8 月 26 日以降、彼の対応を過小評価していました。2026 年 9 月 7 日、調査当日にリポジトリが置かれていたリビジョン 54b1c56 で、彼の auth/permissions.py、auth/scopes.py、core/tool_registry.py を再確認しました。tier に関する部分は 2026 年 8 月 26 日に確認し、同日、こちらの説明を鵜呑みにせず自身のクローンで検証した csitte.at によって訂正されました)、より充実したメッセージ単位のトリアージヒューリスティック(a-bonus)、そしてメールボックス全体を対象にした一括整理(スヌーズ機能もないホスト型の mcpemails.com)です。どのサーバーも行わないのは、クエリ に基づいて操作し、操作前にメールボックスの応答を確認することです。taylorwilsdon のフィルター apply、aaronsb のプレビュー付き modifyMatching、gmail-tidy の organize_emails は、いずれも messages.list を使って前半を実行するようになりましたが、後半を行うものはありません。
Workspace サーバーと併用する

mailwarden は Gmail サーバーであり、Workspace スイートではありません。Calendar、Drive、Docs、Sheets を一か所から使いたい場合は、taylorwilsdon/google_workspace_mcp のような幅広いサーバーで、このサーバーでは対応できない領域をカバーできます。両者は相互排他的ではありません。両方を追加するのは妥当な構成です。その理由はツールの数ではなく、トークンにあります。メールを送信できるスイートサーバーは、接続先のすべてのメールボックスについて、メールを送信できる認証情報を保持します。代わりに Gmail を mailwarden に任せれば、構成内のメール機能には作成、返信、転送、送信のツールが一切ありません。この保証を支える仕組みは tier によって異なり、その違いは重要です。read では Google がトークン(gmail.readonly)によって制限し、送信エンドポイントはこのスコープを拒否します。一方、manage ではツールの公開範囲に依存します。Gmail は送信時に gmail.modify を受け付けるため、スコープだけでは保証になりません。どちらの場合も、送信先ガードがサーバー自体で messages.send とすべての下書き用エンドポイントを拒否するため、受信トレイ内の注入されたメッセージが利用できるツールも、到達できるエンドポイントもありません。

実際の構成では、スイートサーバーを必要なサービスに接続し、Gmail のツールを無効にして(--disabled-tools を使うか、Gmail のツールを含まない tier を選択します)、メール用に mailwarden を併用します。tier のルールも忘れないでください。各クライアント設定で書き込み tier を割り当てられるメールボックスは、最大 1 つです。

再検証が重要な理由 — 具体例

アシスタントに 「受信トレイをすでにスキップした未読のプロモーションメールをアーカイブして」 と頼むと、アシスタントは category:updates is:unread -in:inbox という明白なクエリを使います。Gmail のインデックスを信頼するサーバーは、すでに読んだスレッドまでアーカイブしてしまいます。触れるつもりのなかったメールが、一括操作で消え、簡単には元に戻せなくなります。 主張ではなく、実測です。 実際のメールボックス(約70,000件のメッセージ)で、category:updates is:unread を threads.list 経由で実行すると、131件のスレッドが返されましたが、そのうち未読メッセージを含んでいたのはわずか17件でした。つまり、87%が古い情報でした。同じメールボックスで同じクエリを同じ時間に、代わりに messages.list 経由で実行したところ、19件のメッセージが返され、古い情報はありませんでした。 つまり、これは「Gmail検索は信頼できない」という話ではありません。既読状態の スレッド ビューは遅れて更新されますが、メッセージ単位のビューは遅れず、search は threads.list を経由します。同じ日に同じ方法で測定した別のメールボックスでは、ずれはまったくありませんでした。

手順、3つすべてのクエリ、対照条件、そしてこの調査結果が 示していないこと(インデックスが述語を無視しているわけでも、珍しい演算子の組み合わせによる癖でもありません):Gmail のスレッドインデックスは、古い既読状態をもとに is:unread に応答することがある — 単独で読めるレポートで、すべての数値の根拠となる測定記録をたどれます。

これが要点です。サーバーには、どのようなメールボックスを扱っているのか分かりません。 ずれのない環境では再検証してもコストはかかりませんが、ずれがある環境では役に立ちます。上記の測定では、search が除外したスレッドはすべて実際に既読であり、実際の未読メールを一件も除外しませんでした。

コストがかからないわけではないのが、一括操作ツールです。 search はヒットをすべて取得するため、その際に再検証します。一方、bulk_modify(および create_filter の applyToExisting 一括処理)は数千件のメッセージを扱うため、ヒットごとに1回取得すると、コストの桁が変わります。どちらもインデックスが返す内容に対して処理を行います。bulk_modify は unverifiedPredicates を報告します。これは、クエリの条件のうちインデックスの判断に委ねたもの(+UNREAD、-INBOX など)です。空なら、疑うべきものはありません。空でなく、結果を既読状態まで正確にしたい場合は、まず search で対象を確定し、そのスレッド ID に対して操作してください。dryRun でもこの隔たりは解消されません。同じインデックスを再度読み取るため、対象件数は確認できますが、それが正しいかどうかは確認できません。previewToken が解消するのは別の隔たり、つまり対象を表示してから変更するまでの間です。これがなければ、書き込み時にクエリが再実行され、その時点で一致するものが変更されます。

フィルターの一括処理では、このフィールドは意図的に報告しません。 クエリはユーザーが指定したものではありません。create_filter は条件からクエリを構築し、query 条件は括弧で囲み、from/to/subject の各値は二重引用符で囲みます。このどちらも、述語の導出を無効にします。定義できるすべてのフィルターでリストが空になり、ここでの空リストは「疑うべきものがない」と受け取られる一方、実際には「判定できなかった」という意味です。そこで注意点は、例外なく当てはまる説明として文章で示します。一括処理は、インデックスが返した内容に対して行われるものとして扱ってください。 一括処理で実際に変更された内容を確認するには verify: true を使い、書き込み前に対象を正確に確定する必要がある場合は search を使ってください。

より低コストな折衷案を、ありのままに説明します。 bulk_modify では crossCheck: true も指定できます。これは書き込み前に Gmail に同じ内容を別の方法でもう一度問い合わせます。導出された各述語を、クエリ演算子ではなくラベルフィルター(labelIds)として再実行し、2つの方法で結果が異なるメッセージは変更せず、その旨を報告します。コストは述語ごとにリスト呼び出しが1回増えるだけで、一致するメッセージの数には左右されません。再検証ではヒットごとに1回取得します。得られる効果には限界があるため、その点を明記しておく価値があります。不一致は実際の証拠になりますが、一致は何の証拠にもなりません。どちらの方法も同じインデックスを読み取っており、インデックスは一貫して誤ることもあるためです。したがって、クロスチェックの有無にかかわらず、unverifiedPredicates は常に報告される内容を引き続き報告し、メールボックスそのものを再確認するのは search だけです。実際に2つの方法で結果が異なることがあるかは、まだ測定されていません。node scripts/probe-crosscheck.mjs を実行すると、読み取り専用で ID のみを使って、ご自身のメールボックスでその点を正確に測定できます。 mailwarden はいずれにしてもすべての検索結果をリアルタイムで取得するため、search は各スレッドの実際のラベルを使って、曖昧さのない条件(is:unread、is:read、in:inbox、category:…、および否定条件)を再確認し、ツールに渡す前にインデックスの誤検出を取り除きます。その後、一括操作は指定した対象だけに対して実行されます。これは、Gmail がインデックス化した内容に対して操作するのと、現時点でメールボックスに実際にある内容に対して操作することの違いです。だからこそ、スヌーズや一括処理は安心してアシスタントに任せられます。一括処理で再表示されるのは、実行時に実際のラベルを確認したうえで、スヌーズの期限が本当に来ているスレッドだけです。

ご自身でお試しください — Gmail アカウントは不要です。 リポジトリをクローンした状態で実行してください(デモはリポジトリ内だけで使う検証スクリプトであり、npm パッケージには含まれていません)。

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node scripts/demo-reverify.mjs

その隣には、node scripts/probe-reverify.mjs という別のスクリプトもあります。これは、偽のメールボックスではなく、ご自身のメールボックスで同じ動作を測定します。読み取り専用で、メタデータのみを扱い(件名、送信者、本文は取得しません)、件数とラベル名を表示します。上記の数値はこのスクリプトで取得したもので、メールボックスにずれがあるかどうかを確認することもできます。

このデモでは、意図的に古いインデックスを返す偽の Gmail API に対して実際の search() を実行します(Gmail と同様、is:unread クエリに対して既読のスレッドを返します)。そして、mailwarden がその誤検出を取り除くことを示します。結果をアサートするため、動作が回帰するとゼロ以外の終了コードで終了します。同じケースは test/gmail.test.ts の単体テスト(「ライブのラベル再確認によってインデックスの誤検出を取り除く」)でも固定されています。

すべてを再確認せずに、もう一度確認する

「前回確認してから届いたものは何か」という定期的な確認は、ライブサーバーには負荷の大きい処理です。これに答える明白な方法は、対象範囲全体を再び検索して比較することです。what_changed(read tier)は、代わりに Gmail 自身のイベントログを使って回答します。以前の呼び出しまたは get_profile が返した historyId を渡すと、新たに届いたもの、なくなったもの、追加または削除されたラベルと、次回保持する ID が返されます。

これはキャッシュではありません。この違いこそが設計の要点です。 呼び出し間で保持されるのは一つの数値だけで、それを保持するのは呼び出し元です。mailwarden は引き続きメールボックスについて何も保存せず、ミラーもインデックスも保持せず、すべての呼び出しで Gmail API に対してリアルタイムに処理します。これは、ここでの他のすべての機能と同じルールです。

利用する前に知っておくべき性質が二つあります。報告するのは状態ではなくイベントです。あるメッセージを未読にした後で既読にすると、両方のイベントに表示され、どちらも事実です。現在のメールボックスの状態を確認するには、search を使ってください。また、Gmail の履歴はおよそ 1 週間分しか保持されず、それを過ぎると ID は拒否されます。mailwarden はその拒否を空の結果ではなくエラーに変換します。「変更はありません」と「何が変更されたかをもう確認できません」では取るべき対応が正反対であり、安全に実行できるのはそのうち一方だけだからです。

送信者を判断する — authentication が答えること、答えないこと

get_thread の各メッセージには authentication オブジェクトが含まれます。これは、受信サーバーが記録した SPF、DKIM、DMARC の結果と、各チェックで実際に検証されたドメインを示します。

{
  "spf": "pass",   "mailedBy":   "forwarder.example",        // envelope sender SPF checked
  "dkim": "pass",  "signedBy":   "routing.example",           // domain whose key signed it
  "dmarc": "pass", "headerFrom": "authority.example",        // the From domain DMARC evaluated
  "dkimAligned": false,                                      // the signer is not the From domain
  "authservId": "mx.google.com",                             // who asserts all of the above
  "returnPath": "srs0=…=authority.example=…@forwarder.example"
}

まず dmarc を確認してください。 3 つのうち、合格したチェックを人間が目にする From アドレスと結び付けるのはこれだけです。spf: "pass" だけでは、エンベロープ送信者が送信を許可されていたことしか分かりません。これは、そっくりなドメインを使えば数分で実現できます。

3 つのドメインは一致する必要はなく、不一致があるからといって問題があるとは限りません。 上記のオブジェクトは、公共機関から送信され、メールルーティングサービス上のカスタムドメインを経由してメールボックスに届いた実際のメッセージです。各ドメインはすべて異なりますが、メールは本物です。転送によってエンベロープ送信者が書き換えられ(mailedBy は転送者になります)、転送者は独自の鍵で署名し(signedBy)、元の送信者の名前が残るのは headerFrom だけです。だからこそ、この場合に意味のあるチェックは SPF ではなく DMARC なのです。ドメインは結果の説明として捉え、それ自体を判定基準にしないでください。

合格していても、メールが本当にそのドメインから送られたとは限りません。 また、そのドメインが信頼に値するとも限りません。フィッシング詐欺師は、今朝登録したそっくりなドメインで、完璧な SPF、DKIM、DMARC を設定できます。認証から分かるのは誰が送信したかであり、その答えが「主張どおりの相手であり、まさにそれが問題です」ということもあります。

dkimAligned は、署名から生じる疑問に答えます。 自分が所有するドメインの鍵なら誰でもメールに署名できるため、dkim: "pass" だけでは、誰かが署名したことしか分かりません。dkimAligned は、その署名者のドメインが From ドメインと同じか、その親ドメインまたは子ドメインかを示します。兄弟サブドメイン同士については何も示しません。news.bank.example と mail.bank.example は整合していますが、a.co.uk と b.co.uk は整合していません。パブリックサフィックスリストがなければ、この 2 つのケースは同じに見えるため、誤った値ではなくフィールド自体がありません。上記の転送例では値は false で、これは正しい結果です。署名したのは転送者だからです。DKIM が合格しなかった場合、このフィールドはありません。ドメインの場合と同様、false は説明であって判定ではありません。まず確認すべきチェックは引き続き dmarc です。

unchecked: true の意味。 メッセージには Gmail の Authentication-Results ヘッダーがありませんでした。誰も調べていないということです。失敗でも合格でもありません。このヘッダーはメッセージを受信するサーバーが書き込むため、外部から届いたことのないメール、たとえば自分が送信したメールには、ヘッダーがないのが当然です。

偽造されたレポート。 メッセージには Authentication-Results ヘッダーを含めることができます。攻撃者は送信するメールに好きな内容を書き込めます。読み取られるのは Gmail 独自のレポートだけです。つまり、authserv-id が mx.google.com である最上位のレポートです。これは RFC 8601 が信頼するよう指示している境界です。それ以外の ID のレポートは、どれほどもっともらしく見えても読み取られません。また、別の ID の偽造レポートが含まれていても、Gmail のレポートがなければメッセージは unchecked です。制約として、Gmail の受信メールサーバーを通らずにメールボックスに届いたメール(POP 経由で別のアカウントから取得したメールや、アプリによって挿入されたメールなど)には、Gmail のレポートがまったく含まれないことがあります。その場合、mx.google.com を名乗る偽造レポートは本物と見分けがつきません。Gmail が自ら受信したメールには、常に最上位に独自のレポートが付くため、そこで偽造レポートが読み取られることはありません。otherReports は、読み取られなかったすべてのレポートを数えます。(0.22.1 までは、ID にかかわらず最初のレポートが読み取られていました。Gmail がレポートを書き込んだ場合は正しく、書き込んでいない場合に限って誤っていました。)値はそのまま通さず、トークンとして検証するため、判定結果のように見えるフィールドに文章が入ることはありません。同じ方式の結果が食い違う場合(2 つ目の DKIM 署名が失敗した場合など)、その食い違いは隠されず alsoReported に表示されます。

ツール

ツール機能
searchGmail のクエリ構文を使ってスレッドの概要を検索します(最初のメッセージの差出人/件名/日付、最後のメッセージの lastMessageId/lastMessageDate/lastMessageFrom と、それが自分から送信された場合の lastMessageSent、ブール値の unread、名前・種類・サイズ別の attachments、ラベル、最後のメッセージの Gmail のスニペット)。既読状態/カテゴリの条件は、各検索結果の最新のラベルに照らして再確認されます。pageToken/nextPageToken によるページネーションに対応しています。各検索結果には signals が含まれます。newsletter(List-Id/List-Unsubscribe/Precedence bulk、junk または list)、automated(Auto-Submitted、自動返信/抑制ヘッダー、no-reply 形式の差出人)、calendar(text/calendar または .ics パート)、replyToMismatch(Reply-To が From と異なるドメイン。サブドメインは同じドメインとして扱います)、displayNameSpoof(From の表示名に、メールの送信元とは異なるドメインのアドレスまたは URL が含まれる場合。例:"[email protected]" <[email protected]>)、lookalikeDomain(ホモグラフドメインのように、From のドメインのラベルにラテン文字とキリル文字、ギリシャ文字、またはアルメニア文字が混在する場合)です。追加の呼び出しは不要で、最初のメッセージのヘッダー/MIME から取得します。クエリに in:spam/in:trash が指定されていない限り、迷惑メールとゴミ箱は除外されます。迷惑メール内を検索するを参照してください。
get_threadスレッド全体(ヘッダー、プレーンテキスト本文と HTML 本文、添付ファイルのメタデータ)を取得します。各メッセージには authentication も含まれます。受信サーバーが報告した SPF/DKIM/DMARC と、各チェックで検証されたドメイン(signedBy/mailedBy/headerFrom)、それを主張するサーバー(authservId)、returnPath、相互に矛盾する結果の alsoReported、未読の報告を示す otherReports、DKIM 署名者が From ドメインかどうかを示す dkimAligned、メッセージに Gmail からの報告が含まれていない場合の unchecked: true です。差出人を判断する — 認証で分かることと分からないことを参照してください。full: false はヘッダーとラベルのみを取得します。この場合、plaintextBody/htmlBody/attachments は報告せずに省略し、代わりに metadataOnly: true を設定します。何も調べていないのに空として報告することはありません。この指定は非推奨であり、bodies: "none"(同じクォータで、実際の添付ファイル一覧を取得)を使用してください。また、bodies との併用は invalid_input として拒否されます。長いスレッドや HTML の多いスレッドでは、代わりに回答を短くしてください。bodies: "text" はプレーンテキストのみを残します(HTML のみのメールはテキストに変換され、bodyFromHtml が設定されます)。bodies: "new" は、返信末尾の引用履歴(帰属行のある末尾の > ブロック、または Outlook の Original Message ブロック)を除いた text です。インライン返信、引用の下にある返信、転送メッセージ(件名が Fwd/FW/WG の Outlook 転送も含む)は全体を残します。Outlook のプレーンテキストヘッダーブロックの下にある返信は、そのブロックと一緒に残します。切り詰められたメッセージには quotedStripped が設定されます。bodies: "none" は本文を含めず、ヘッダー、認証情報、実際の添付ファイル一覧を残します。lastN は最新の N 件のメッセージのみを返し、afterMessageId はすでに読んだメッセージの後のメッセージのみを返します。何を残してもクォータは同じです。messageCount と omittedMessages には、回答にスレッドのどの程度が表示されているかが常に示されます。
read_attachment添付ファイルを保存せずに、その中のテキストを読み取ります。プレーンテキスト(txt、csv、xml、json、ics など)、gzip ファイル、または ZIP アーカイブ内のテキストエントリ(DMARC 集計レポート、エクスポート、ログ)に対応しています。形式は宣言された名前や種類ではなく、バイト列に基づいて判定します。UTF-8 ではないテキスト(Windows で作成された Excel の CSV など)は windows-1252 として読み取り、guessedCharset を設定します。バイナリ形式は拒否され、download_attachment を案内します。サイズはすべて使用前に上限が適用されます(エントリごとに展開後 5 MiB、アーカイブごとに 20 MiB、50 エントリ)。そのため、ZIP 爆弾は上限で停止します。1 回の回答で、全エントリを合わせて最大 maxChars 文字のテキストを返します(既定値 30,000、最大 100,000)。途中で切られたエントリには nextOffset が示され、offset で続きを取得できます。アーカイブでは list: true により、最初に名前、サイズ、テキストの長さを表示できます。entry(名前または *.csv のようなパターン)を指定すると、必要なものだけを読み取り、残りは notSelectedNames に記載します。バイナリ、暗号化済み、サイズ超過、入れ子のアーカイブのエントリは開かずに skipped として一覧表示します。ディスクには何も書き込まず、メール本文と同様に、テキストをコードフェンスで囲み、隠し文字を除去します。
list_labelsすべてのラベル(システムラベルとユーザーラベル)を一覧表示します。
get_profile接続されたアカウントのアドレス、メッセージ/スレッドの総数、メールボックスの現在の historyId を返します。操作の前に、どのメールボックスが接続されているかを確認してください。また、応答しているサーバーも確認できます。serverVersion(リリースビルドの場合はバージョンのみ、それ以外は 0.23.0+a5ff4a7 のようにコミットを含みます)、build、実行中のコードが読み込まれたディレクトリを示す serverPath、account が含まれます。ドキュメントにあるように見えるツールが見つからない場合、想定しているビルドが実際に動作しているかを確認できます。
what_changed保持している historyId 以降のメールボックスのイベント(到着、削除、ラベルの追加と削除)を、1 回の呼び出しで取得します。
triage_digest判断に役立つ、メールボックスの一部の構造化された概要を返します。差出人の上位一覧(各スレッドが持つシグナル付き)、ラベル別および経過期間別の分類、未読数と添付ファイル数、ニュースレター/自動送信メール/カレンダー招待/返信先の不一致に該当するスレッド数が含まれます。生のスレッド一覧の代わりに使用できます。
follow_ups誰が誰からの返事を待っているかを確認できます。最後のメッセージが自分から送信され、数日間返信のないスレッド(自分のアドレス宛てのメモは除外。他の宛先が To または Cc に 1 人でもいれば含む)と、最後のメッセージがニュースレターや no-reply 通知ではなく人から届いた受信トレイのスレッドを返します。各スレッドの最後のメッセージとその最新のラベルに基づいて判断するため、インデックス上では受信トレイに分類されていても、すでにアーカイブされたスレッドは除外されます。
list_unsubscribeスレッドが提示するオプトアウト方法(List-Unsubscribe)を返します。提示されていない場合は、本文内のリンクを返します。どこにも接続しません。
list_subscriptionsメールボックスの一部を差出人別にグループ化して表示します。スレッド数/未読数、各差出人のメールが確認された期間、オプトアウト方法を返します。差出人ごとにヘッダーを 1 回取得し、どこにも接続しません。sendersFound は、topN による一覧の切り詰め前に見つかった差出人の数を示します。
create_labelユーザーラベルを作成します(冪等。Parent/Child 形式で階層化可能)。その ID を返します。任意の backgroundColor/textColor の組み合わせで、既存のラベルも含めて色を設定できます。
modify_labels名前または ID を指定してラベルを追加/削除します。add に不明な名前を指定すると自動作成されます(アーカイブ=INBOX を削除、既読=UNREAD を削除)。threadId、または最大 100 件の threadIds リストを受け取り、承認済みのリストに 1 回の呼び出しで変更を適用できます。その場合、回答ではスレッドごとに changed、failed、notAttempted を報告します。
bulk_modifyクエリに一致するすべてのメッセージに対し、ラベル変更を一括適用します。API リクエストあたり 1000 件のメッセージを処理し、部分的な成功をチャンクごとに報告します(スレッド ID リストは最大 500 件で、submittedThreadCount は合計数を示します)。messages.batchModify は本文のない 204 を返し、不明な ID を黙って無視するため、件数は**submitted**として報告されます。verify: true を指定するとラベルを再取得し、観測された唯一の結果である verified {applied, notApplied, unverifiable} を返します。未加工のインデックスに対して動作するため、確認できなかった条件は unverifiedPredicates に記載されます(以下を参照)。dryRun: true はクエリを解決し、一致したスレッドと作成するラベルを報告します。何も変更しません。一致するスレッドがある場合は previewToken も返します。サーバーはその正確なメッセージ集合を 30 分間保持します。同じクエリとラベル変更を指定してトークンを渡す書き込みは、その集合だけに適用されます。途中で新たに一致するようになったメールは変更されず、fromPreview.newMatches に計上されます。クエリを書き換えた場合や別の変更を指定した場合は、最初の書き込みの前に拒否されます。トークンは 1 回のみ使用できます。undo: true は書き込み前に、影響を受ける各スレッドのラベルを最新状態で読み取り、メッセージごとの正確な逆操作である undo を返します(最大 500 件。undo: true の場合、計画を作成できなければ何も書き込まれません)。
undo_bulk_modifybulk_modify が返した undo 計画を再適用します。各メッセージについて、失われたラベルだけを正確に戻し、追加されたラベルだけを正確に削除するため、すでに対象の状態だったメールはそのまま維持されます。これは add と remove を入れ替えるだけでは実現できません。計画にラベル ID が含まれるため、ID のみを受け付けます。一括変更で作成されたラベルは、空のままメールボックスに残ります。
archive / mark_read / mark_unread便利なラッパーです。modify_labels と同様に、リストとして threadIds を指定できます。
trash / untrashゴミ箱へ移動/ゴミ箱から復元します。
download_attachment添付ファイルをローカルパスに保存します(上書きは行わず、名前が重複する場合は数値の接尾辞を付けます)。
export_threadスレッド全体を返す代わりに、ローカルファイルに書き出します。人が読む場合は Markdown 形式(ヘッダーと添付ファイル名をコードとして扱うため、プレビューで画像が読み込まれたり、差出人が記載したリンクが開かれたりすることはありません。差出人の認証情報も含みます。本文はメールのテキストで閉じられないフェンス内に記載します)、または get_thread と同じ形式で返す JSON を選択できます。パスの規則は添付ファイルと同じです。
unsubscribeメッセージ自身のヘッダーにあるエンドポイントを使って、ワンクリックでオプトアウトします(RFC 8058)。Google 以外のホストに接続する唯一のツールです(詳細)。
bulk_unsubscribe複数のスレッドに対して同様の処理を順番に行い、差出人ごとに最大 1 回のみリクエストします(サーバーが動作している間は呼び出しをまたいで記録されるため、再試行しても同じ差出人に二重に接続しません)。スレッドごとに部分的な成功を報告します。dryRun: true は同じヘッダー読み取りと重複排除を行い、各スレッドで呼び出す予定のエンドポイントを wouldCall に報告します。どこにも接続しません。
snoozeすぐにアーカイブし、指定した日付(YYYY-MM-DD)、日付と時刻(2026-06-20 9am)、またはプリセット(tomorrow、tomorrow 9am、weekend、next week、曜日名、in N days、in N hours)以降に再表示します。
unsnoozeスヌーズを取り消し、すぐに受信トレイに戻します。
list_snoozedスヌーズ中のすべてのスレッドと期限を一覧表示します。
sweep_snoozedスヌーズ期限が到来したスレッドを再表示します(オンデマンド、cron、またはデーモン経由で実行)。バッチ処理に対応し、部分的な失敗を報告します。dryRun: true は、何も再表示せずに「今すぐ期限が来ているものは何か」に回答します(dueLabels/dueThreads)。
list_filtersすべての Gmail フィルター(条件とラベル操作)を一覧表示します。既存のフィルターにある forward アドレスを監査用に表示します。
preview_filterフィルターを作成する前に確認します。現在、その条件に一致するメッセージ数(applyToExisting の一括処理で使用されるものと同じインデックスから取得する currentMatches)、条件が完全に同じ既存フィルター(sameCriteria)、from/to/subject/query の値を共有するフィルター(共有するフィールド名付きの overlapping)、作成される addLabels の名前、今後の一致をすべてゴミ箱に移動する操作かどうかを返します。読み取り専用です。検証方法は create_filter と同じです。
create_filterサーバー側の自動振り分けルールを作成します(条件 → ラベル操作のみ。転送は不可。以下を参照)。任意で applyToExisting を指定すると、メールボックス内ですでに一致しているメールにも処理を適用します。
delete_filterID を指定してフィルターを削除します。
すべてのツールは outputSchema を宣言し、同じ JSON をコードフェンス付きテキストとして返すのに加えて、構造化コンテンツ(検証済みで機械可読)を返します。クライアントが文章を解析する必要はありません。

失敗も構造化されています。人が読む文に加え、isError と、code(not_authorized、needs_reauth、insufficient_scope、forbidden_operation、not_found、rate_limited、upstream_unavailable、network_error、invalid_input、declined_by_user、internal_error)および retryable フラグを含むコードフェンス付き JSON 本文が返されます。そのため、「待って再試行する」か「mailwarden --auth を再実行する」かを、次のリリースで言い換えられる可能性のある文言から推測するのではなく、クライアント自身が判断できます。(エラー時に structuredContent がないのは、成功を記述するツールの outputSchema に対して検証されるためです。)rate_limited エラーには retryAfterSeconds も含まれます。この場合、待つこと自体が解決策だからです。insufficient_scope エラーでは、可能なスコープではなく不足している点を明示します。トークンが読み込まれた時点で、どのスコープが不足しているか、それが何を対象とするか、保存済みの認可が代わりに何を許可するかを読み取り、サーバーは起動時に同じ内容を stderr に出力します。これは拒否を防ぐのではなく、拒否の理由を説明します。呼び出しは引き続き Google に送られ、拒否するのは Google です。暗号化されたトークンでは特に重要です。登録時のティア判定では復号できないため、利用可能な機能がすべて表示され、そのままではタスクの途中で理由の説明もない 403 が表面化することになります。

クォータ:自動的に解消される唯一のエラー

Gmail では、各呼び出しがユーザーごとの 1 分あたり 6,000 ユニットの予算から差し引かれます。ユニット数は一律ではありません。スレッドの取得が最もコストのかかる処理で、どの形式で取得しても同じです。

ツール1 回の呼び出しあたりのユニット数
searchリストの各ページにつき 10。さらに取得する候補スレッドごとに 40。デフォルトのサイズでは約 1,000、クエリに既読状態やカテゴリの条件がある場合は最大で約 4,000
get_thread(全形式およびメタデータ形式)40
trash、download_attachment、read_attachment20
export_thread40
archive、mark_read、untrashスレッドごとに 10
modify_labels、snooze10 に加え、名前で検索するラベルごとに 1、作成が必要なラベルごとに 5
bulk_modify一致するメッセージ 500 件ごとに 5、変更するメッセージ 1,000 件ごとに 50。verify: true を指定するとスレッドごとに 40 が追加され、crossCheck は条件ごと、一致するメッセージ 500 件ごとに 5、undo: true はスレッドごとに 40 が追加されます。previewToken を指定した書き込みでは、fromPreview を報告するためにもう一度リストを取得します
undo_bulk_modifyステップごとに 50 に加え、verify: true の場合はスレッドごとに 40
follow_upsプロフィールに 1、リストの各ページに 10、スキャンするスレッドごとに 40。デフォルトでは最大 50 スレッド、約 2,000
list_labels、get_profile1

mark_unread のコストは mark_read と同じです。

この仕組みを利用する前に知っておきたいことが 2 つあります。並列エージェントは同じ予算を共有します。単一ユーザーとして 1 つのサーバープロセスを通じて Gmail にアクセスするため、4 つのエージェントが同時にスレッドを読み込むと、予算の消費速度は 4 倍になります。また、6 スレッド以上を処理する場合、bulk_modify はループ処理より低コストです。一致するメッセージが 500 件までなら 55 ユニットで、archive を 5.5 回呼び出す場合と同じコストです。コストはスレッド数ではなく、メッセージ 1,000 件ごとに増加します。verify: true を指定すると状況は逆になり、ラベルを読み返すコストはスレッドごとに 40 となり、置き換えたループ処理より高くなります。

上限を超えると、mailwarden は rate_limited と retryAfterSeconds: 60 を返して呼び出しを失敗させます。このエラーは一時的なもので、待てば実際に解消されます。予算は 1 分ごとに集計されるためです。mailwarden は報告する前に、約 20 秒の待機を 1 回だけ自動で行います。それ以上はツール呼び出しを保留せず、判断を呼び出し元に戻します。この動作は、1 回の呼び出しで多数のスレッドを取得するツールにも当てはまります。search はクォータによって途中で打ち切られたリストを返すのではなく、rate_limited を返して失敗します。一方、verify を指定した bulk_modify と list_subscriptions は取得を停止し、取得できなかった項目をそれぞれ unverifiable と unknown として報告します(bulk_modify では変更はすでに適用されているため、失敗させるとその事実が隠れてしまいます)。Gmail はこのエラーを 429 だけでなく 403 としても返すため、ステータスだけで判定するクライアントは権限の問題と誤認します。code で判定してください。

スヌーズの仕組み(Gmail API にスヌーズ機能はないため、独自に実装しています)

snooze は INBOX を削除し、日付付きラベル MCP/Snoozed/<key> を適用します。キーは YYYY-MM-DD(終日で期限)または YYYY-MM-DDTHHMM(そのローカル時刻に期限)のいずれかです。until 引数には、明示的な日付、日付と時刻(2026-06-20 9am、…T17:00)、またはサーバー側で解決されるプリセットを指定できます。プリセットは today、tomorrow、weekend(次の土曜日)、next week(次の月曜日)、曜日名(monday~sunday、次に来る曜日)、in N days、in N hours です。日付のプリセットには末尾に時刻を付けることもできます(tomorrow 9am、monday 8:30)。そのため、呼び出し元が時刻を計算する必要はありません。sweep_snoozed は期限が到来したラベルを見つけ、対象のスレッドを未読の状態で受信トレイに戻します。時刻指定のスヌーズは、その時刻以降に最初に実行されるスイープで解除されるため、解除の遅延時間はスイープの間隔と同じです。スイープは次の方法で実行できます。

  • 必要に応じて実行(sweep_snoozed ツール)
  • cron 経由で実行:mailwarden --sweep
  • 自動実行:MAILWARDEN_AUTO_SWEEP=1 を設定(サーバー稼働中に毎時スイープ)
フィルター(永続的な自動振り分けルール)

create_filter は Gmail のサーバー側ルールを設定します。条件に一致するメールには指定したラベル操作が自動的に適用されるため、アシスタントが介在しなくてもメールボックスの振り分けが続きます。

  • 条件: from、to、subject、query(Gmail の検索構文全体)、negatedQuery、 hasAttachment、excludeChats、および size と sizeComparison(smaller/larger をセットで指定)。 少なくとも 1 つは必須です。
  • アクション(ラベルのみ): addLabels/removeLabels。名前または ID で指定します(addLabels に未知の名前を指定すると自動作成され、/ で階層化できます)。よく使われる例:受信トレイをスキップ → removeLabels: ["INBOX"]; 既読にする → removeLabels: ["UNREAD"];自動的にゴミ箱に移動 → addLabels: ["TRASH"]; スターを付ける → addLabels: ["STARRED"];迷惑メールにしない → removeLabels: ["SPAM"];ラベルを付けて保存 → addLabels: ["Receipts"]。
  • 既存のメール: フィルターは作成後に届いたメッセージにのみ適用されます。applyToExisting: true を渡すと、メールボックス内の既存メールにも同じアクションを一度適用します。mailwarden は条件から Gmail の検索クエリを作成し、一括変更を実行します(maxMessages まで。デフォルトは 1000。bulk_modify と同じ、未検証のインデックスに関する注意事項があり、この一度限りの処理では迷惑メールとゴミ箱は対象外です)。 これには少なくとも 1 つの 肯定的な 条件(from/to/subject/query/hasAttachment:true/size)が必要です。除外条件だけのルール(negatedQuery または hasAttachment:false)はメールボックスのほぼ全体に一致するため、applyToExisting では拒否されます。このようなフィルターはフラグを付けずに作成してください。 結果は applied に返されます(使用した query、matchedMessages/submittedMessages/submittedThreadCount の件数、対象が maxMessages に達した場合の capped、チャンクごとの failed、処理全体が失敗した場合の error 文字列)。applyToExisting を指定していない場合は null です。submittedMessages は API に渡された件数であり、変更された件数ではありません。applyToExisting とともに verify: true を渡すと、ラベルを読み戻して applied.verified {applied, notApplied, unverifiable} を取得できます。これは bulk_modify の verify と同じ確認処理です(影響を受けるスレッドごとに読み取りが 1 回追加されるため、デフォルトでは無効です)。フィルターが先に作成されるため、バックログ処理の一部または全部が失敗しても applied に 報告されるだけ で、例外にはなりません。ルール自体は有効なままです。
  • 転送には対応していません — セキュリティとプライバシーを参照してください。
  • gmail.settings.basic スコープが必要です。古いバージョンを認証した場合は、もう一度 --auth を実行してください。 読み取り専用モードでは利用できません。
配信停止 — 唯一の外部送信リクエスト

list_unsubscribe(読み取りティア)は、送信者が提示している配信停止方法を、誰にも連絡せずに報告します。List-Unsubscribe ヘッダーを実際に含む最新のメッセージを読み取ります。ニュースレターに返信したメールはスレッドの末尾に追加され、配信停止情報を示さないため、それを基にすると「このリストには配信停止方法がない」と誤って判断してしまうからです。スレッドに配信停止ヘッダーがまったくない場合、list_unsubscribe はメッセージ本文を調べ、見つかった配信停止リンクを bodyCandidates として報告します。多くの送信者はフッターにのみリンクを記載しているため、「配信停止方法がない」と答えるのはヘッダーについては正しくても、メールについては誤りです。これらのリンクは 報告されるだけで、取得されることはありません。unsubscribe に渡すこともできません。また、このフラグは従来どおりヘッダーの情報を示すため、hasUnsubscribe は false のままです。検索にはスレッド全体の取得が 1 回必要で、そのケースでのみ実行されます。 list_subscriptions(読み取りティア)も、スライス全体を同様に送信者ごとにまとめて処理します。これにより、誰が メールを送り続けていて、そのうち誰が実際に配信停止できるのかを確認できます。スレッドごとではなく、送信者ごとにヘッダーを 1 回取得します。unsubscribe と bulk_unsubscribe(管理ティア)はその処理を実行します。mailwarden が Google 以外のホストと通信するのは ここだけ なので、ルールは厳格です。

  • URL パラメーターはありません。 エンドポイントはメッセージ自身のヘッダーから取得し、それ以外の場所から取得することはありません。 URL 引数があると、プロンプトインジェクションされたメールによって、このツールがデータ流出経路に変えられてしまいます (メールボックスの内容がクエリ文字列に入ります)。ヘッダーにはモデルが選んだデータを含められません。また、本文中のリンクは報告するだけです。これは送信者がテキストに記載した URL であり、これを取得すると、このルールが防ごうとしているリクエストが、ガードを通らない経路で再び可能になってしまうためです。別の Gmail サーバーである navbuildz/gmail-mcp-server は、ヘッダーがない場合にリダイレクトをたどってこれらのリンクを取得します。
  • 実行するのは RFC 8058 のワンクリック解除のみです。送信者が List-Unsubscribe-Post を通じてオプトインしている必要があります。 通常の https: リンクはブラウザーで人が操作することを想定したものであり、取得せずに返します。
  • mailto: による配信停止は実行しません。 メールの送信が必要ですが、mailwarden にはその機能がありません。報告されたアドレスを使って、ご自身で対応してください。
  • リクエストは固定され、レスポンスは破棄されます。 POST 本文は常に List-Unsubscribe=One-Click で、ほかの情報から生成されることはありません。レスポンス本文は読み取らずにキャンセルされます。モデルに返されるのはステータスコードと実際に呼び出された URL だけです。エンドポイントからの内容は一切返されないため、その内容が指示を返すことはありません。(301/302/303 リダイレクトは GET としてたどられます。つまり、本文は一切送信されません。)
  • 1 送信者につきリクエストは 1 回で、単一の予算内で順番に実行されます。 bulk_unsubscribe が受け取るのはスレッド ID です(クエリは受け取りません。クエリ駆動の一括処理では、誰も確認する前に該当する送信者ごとにリクエストを送ってしまうためです)。リクエストがすでに送信された送信者のスレッドは duplicateOf として報告され、2 回目のリクエストの対象にはなりません。同じリストの 2 つのスレッドは 1 回の配信停止を共有し、2 回実行してもアドレスの確認が 2 回行われるだけです。送信者が記録されるのは、リクエストが実際にエンドポイントに到達した場合だけです。そのため、拒否された場合や接続が切断された場合は、次のスレッドで改めて試行できます。また、スキップされたスレッドが別のエンドポイントを示している場合は、その理由も報告されます。1 人の送信者が複数のリストを運営していることがあるためです。この記録はサーバーの稼働中、呼び出しをまたいで保持され、unsubscribe も共有します。タイムアウトした呼び出しは安全に再試行でき、同じニュースレターに 2 回配信停止を要求しても、送信者に連絡するのは 1 回だけです。意図的に 2 回目の試行を行う場合は、unsubscribe に force: true を渡してください。たとえば、エンドポイントが 500 を返した後などです。記録はメモリ内だけに保持されます。永続化すると、サーバーが意図的に保持しないトークン以外のローカル状態がもう 1 種類増えるため、再起動すると記録は失われます。 1 回の呼び出しにつき、スレッドは最大 25 件、実行時間は最大 60 秒です。予算内で処理できなかったものは、何もせず取り消されたかのように扱われるのではなく、skippedOutOfTime として返されます。いずれも取り消しできないため、これら 3 つの制限を設けています。
  • SSRF ガード。 https のみ、デフォルトポートのみ、URL 内の認証情報は禁止です。また、リダイレクトを含む各ホップ(追跡は最大 3 回まで)は、グローバルに到達可能なアドレスだけに解決される必要があります。各アドレスをバイト列にパースして IANA 特別用途レジストリと照合するため、同じアドレスであれば表記が異なっても判定は同じです(::1 と 0:0:0:0:0:0:0:1 も同様です)。パースできないアドレスは拒否されます。DNS 解決とすべてのホップには、共通で 10 秒の予算が適用されます。DNS リバインディングは防げません(接続時に fetch が再度解決するため)。詳しくは SECURITY.md をご覧ください。この間隙を越えても、レスポンスを読み取らないブラインド POST だけが実行されます。

信頼する前に、ご自身のメールで動作を確認してください。 リポジトリをクローンし(npm パッケージには含まれず、リポジトリ内でのみ利用できます)、npm run build と mailwarden --auth を実行した後に、次のコマンドを実行します。

node scripts/probe-unsubscribe.mjs --vet          # category:promotions, 25 threads
node scripts/probe-unsubscribe.mjs "from:substack.com" --max 50 --vet

実際の各 List-Unsubscribe ヘッダーと、パーサーが解析した結果が並べて表示されます。また、--vet はエンドポイントに URL の検証とアドレスガードも実行するため、パーサーがヘッダーを正しく理解したかどうか、さらに配信停止がガードを通過できるかどうかの両方を確認できます。厳密に読み取り専用です。送信者にリクエストが送信されることはなく、メールボックスの内容も変更されません。 取り消せないこと:このリクエストにより、送信者にあなたのアドレスが有効だと伝わります。オプトアウトを無視する送信者には、どのクライアントも対処できません。そのような相手には、unsubscribe と create_filter または trash を併用してください。自動化できる選択肢がない場合は、エラーではなく、代替手段とともに unsubscribed:false として報告されます。read のみのデプロイでは、list_unsubscribe と list_subscriptions が利用でき、リクエスト自体は決して送信されません。

スパムを確認する

場所を指定しないクエリでは、スパムもゴミ箱も検索されません。 Gmail は in:spam / in:trash を指定していない検索では両方を除外するため、スパムフォルダーに入っているメールに対して from:someone を実行しても何も返らず、そのことは結果にも示されません。実際のメールボックスで測定したところ、同じ from: クエリは、デフォルトでは 0 件、スパムを含めると 1 件を返しました。

これは、メールが誤分類される理由を考えると重要です。スパムフィルターはメッセージ単体で判定します。あなたがついさっき何かに登録したこと、パスワードのリセットをリクエストしたこと、注文したことはわかりません。そのため、あなたが待っている確認メールこそ、まさにスパムに振り分けられやすいメールなのです。たった今自分が何をしたかは、あなたにはわかっています。フィルターにはわかりません。

そのため、届くはずのメールが見当たらないときは、場所を指定してもう一度検索してください。

search("in:spam newer_than:2d")          # what got filed as spam recently
search("in:spam from:example.com")       # the confirmation that never arrived

スレッドを受信トレイに戻すには modify_labels(SPAM を削除し、INBOX を追加)を使います。誤判定が繰り返される送信者には、迷惑メールにしないルールを設定して根本的に対処するのが最適です。removeLabels: ["SPAM"] を指定した create_filter を使ってください(フィルターを参照)。

このサーバーが意図的に行わないことが 2 つあります。1 つ目は、スパムフォルダーをスキャンして、そこに何を残すべきかを 判定する ことです。実際のスパムフォルダーを調べたところ、その 89% にはメーリングリストの仕組みがまったくありませんでした。そのため、「一括メールとは違って見える」という基準ではフォルダーのほぼすべてが該当してしまい、何もフィルタリングできません。2 つ目は、その判定に基づいて自動的に対処することです。スパムからメールを戻すかどうかは判断が必要です。そして、「たった今そこに登録した」という判断を明らかにする文脈は、メールボックスではなく会話の中にあります。

セキュリティとプライバシー

脅威モデル全体(信頼境界、脅威ごとの緩和策、明示的な対象外事項、脆弱性の報告方法)については、SECURITY.md をご覧ください。主な内容は次のとおりです。

  • テレメトリーはありません。 情報を外部に送信することはありません。分析情報、クラッシュレポート、トラッキングもありません。
  • デフォルトではポートを開きません。 stdio のみを使用します。オプションの --http リスナーは 127.0.0.1 にバインドされ(LAN にはバインドされません)、MAILWARDEN_TOKEN ベアラートークンがないと起動を拒否します。信頼できる隔離ネットワークで上書きする場合は、MAILWARDEN_ALLOW_NO_TOKEN=1 を設定してください。ループバックにバインドする場合は、Host ヘッダーも検証します(DNS リバインディング対策)。リモートでホストする場合は、MAILWARDEN_HOST を設定し、TLS の背後に配置してください。
  • 送信ツールは設計上ありません。 mailwarden はメールの作成、返信、転送を行えません。メール内のプロンプトインジェクション指示には、このサーバー経由でデータを持ち出す経路がありません。create_filter も同じルールに従います。メールへのラベル付け、アーカイブ、ゴミ箱への移動、スター付け、既読・未読の変更はできますが、データ持ち出し経路となる 転送 フィルターは作成しません。list_filters はアカウントにすでに存在する転送フィルターを引き続き表示するため、確認できます。このルールが成り立つのは、そのようなツールが存在せず、実行時にも登録できないためです。mailwarden が送信を拒否するのではなく、Google が送信を拒否する、より強力な方法については、以下の読み取り専用モードを参照してください。**この約束は限定的なものとして受け取ってください。**このサーバー経由の経路を閉じるものであり、アシスタントのほかの経路は対象外です。URL の取得、同期フォルダーへの書き込み、コードの実行が可能なクライアントには、インジェクションされたメッセージが必要とする機能がすべて備わっています。そして、このサーバーはそのメッセージをクライアントに提示します。非目標を参照してください。
  • 外部への接続先は 1 つだけで、モデルが URL を選ぶことはありません。 unsubscribe ツールだけが、Google 以外のホストに接続するコードパスです。接続先はメッセージの List-Unsubscribe ヘッダーから読み取られ、ツールの引数から指定されることはありません。リクエスト本文は固定され、レスポンス本文は破棄されるため、データチャネルにはなり得ません。https とデフォルトポートのみが許可され、リダイレクト先も再検証されます。また、プライベート、ループバック、リンクローカル、メタデータアドレスのいずれかに解決されるホップは拒否されます。登録解除を参照してください。
  • ツールの階層(段階的な開示+最小権限)。 MAILWARDEN_TOOLS には、指定した階層のみが表示されます。read(読み取りツール)、manage(メールボックスの変更、スヌーズ、ダウンロード)、filters(サーバー側のフィルター CRUD。ツールが gmail.settings.basic を必要とする唯一の階層)があります。デフォルトでは 3 つすべてが有効です。たとえば read,manage を指定すると、フィルター管理を除いた、メールのトリアージに必要な機能一式が使えます。--auth で要求される OAuth スコープは有効な階層から決まります。read のデプロイでは gmail.readonly のみを要求し、gmail.settings.basic は filters 階層が有効な場合にのみ要求されます。また、保存済みトークンに gmail.settings.basic が含まれていない場合(たとえば、階層を有効にする前に認証したトークン)、フィルターツールは自動的に非表示になります。付与するには --auth を再実行してください。スコープの記録がない古いトークンでは、従来どおりツールが表示され、スコープ不足のメッセージは実行時のフォールバックとして表示されます。
  • 読み取り専用モード。 MAILWARDEN_READONLY=1(MAILWARDEN_TOOLS=read の省略形)を設定すると、読み取りツール(search、get_thread、read_attachment、list_labels、list_snoozed、get_profile、what_changed、triage_digest、follow_ups、list_unsubscribe、list_subscriptions)のみが登録されます。メールボックスの変更やファイルへの書き込みができるツールは、クライアントに一切表示されません(より広い gmail.settings.basic スコープを必要とするフィルターツールも除外されます)。トリアージのみを行う共有環境や HTTP デプロイでの利用を推奨します。Google が送信不可を強制する唯一の階層でもあります。gmail.readonly トークンを使用するため、Gmail の送信エンドポイントはそのトークンを拒否します。manage には gmail.modify が必要で、Gmail はこのスコープでの送信を許可します。mailwarden は単に送信可能なツールを公開しません。つまり、read のデプロイでは、このバイナリが置き換えられても送信できません。manage のデプロイでは、呼び出せるものがないため送信できません。(送信ができない書き込みスコープに切り替える方法はありません。SECURITY.md、脅威 1 を参照してください。)
  • 送信先ガード。 「呼び出せるものはない」という説明は、ツール一覧についてだけのものではなくなりました。認証済みの Gmail リクエストはすべて 1 つのチェックポイントを通過し、mailwarden が使用するエンドポイントのみを許可し、それ以外を拒否します。messages.send、drafts.*、messages.import/insert、完全削除、およびフィルター以外のすべての settings エンドポイントは、拒否リストで先に確認されるため、後から許可リストを変更しても誤って再び許可されることはありません。これには、メソッドに media が渡されたときに Google のクライアントが使用する /upload/gmail/v1/... ルートも含まれます。ホストが書き換えられたリクエスト(GOOGLE_CLOUD_UNIVERSE_DOMAIN、rootUrl オプション)は、トークンがプロセスの外に出る前に拒否されます。Gmail のディスカバリードキュメントにあるすべてのメソッドが、このガードのテスト対象です。このガードが保護するのはこのサーバーであり、トークンではありません。盗まれた gmail.modify トークンは、別の場所から引き続き送信に使えます。
  • ダウンロードの範囲制限。 MAILWARDEN_DOWNLOAD_DIR を設定すると、添付ファイルやスレッドのエクスポートなどのファイル書き込みはそのディレクトリ内に制限されます(実パスに正規化され、シンボリックリンクも考慮されます)。既存ファイルが上書きされることはありません。設定しない場合、クライアントから指定されたパスを基準に解決する場所がないため、download_attachment と export_thread はこのプロセスが書き込める場所ならどこにでも書き込めます。クライアントがリモートにいる --http では特に重要です。そのため、範囲制限なしで --http を起動すると、さらされるリスクの内容を明記した警告が表示されます(警告にとどまり、起動は拒否されません。ベアラートークンの不足とは異なり、これは認証済みクライアントが必要なリスクであり、既存のデプロイは現在の動作に依存しているためです)。read 階層のデプロイではツールが登録されないため、警告は表示されません。
  • 信頼できないコンテンツの隔離。 すべてのツール結果は <untrusted-tool-output> マーカーで囲まれ、不可視文字や BiDi 制御文字が除去されます。そのため、クライアントは引用されたメールの内容と指示を区別できます。除去対象には Unicode のタグ文字と異体字セレクター補助記号(不可視 ASCII を利用した隠蔽)も含まれます。また、テキスト版だけでなく structuredContent にも適用されるため、機械可読形式のデータを読むクライアントにも、同じサニタイズ済みコンテンツが渡されます。
  • ライブ API、コピーは保持しません。 メールボックスのミラーや検索インデックスは、どこにも保存されません。ローカルに保存されるのは ~/.mailwarden/ 内の OAuth トークンのみです。
  • トークンはデフォルトで保存時に暗号化されます。 token.json にはリフレッシュトークンが含まれています。ディスク上では mode 0o600(Windows では何も行いません)によってのみ保護されます。そのため、mailwarden --auth は OS のキーストア(Windows Credential Manager、macOS Keychain、Linux の Secret Service(GNOME Keyring、KWallet))に保存したランダムな鍵を使用し、AES-256-GCM で暗号化して保存します。これにより、ファイルのコピー(バックアップ、同期フォルダー、別のマシンなど)だけでは利用できません。キーストアにアクセスできない場合(デスクトップセッションのない Linux ホスト、または事前ビルドされたバインディングがないプラットフォーム)は、MAILWARDEN_TOKEN_PASSPHRASE を設定すると、代わりにそのパスフレーズから鍵が導出されます(scrypt)。どちらも使用できない場合、トークンは従来どおり平文で保存され、そのことが --auth と --check に表示されます。パスフレーズが設定されている場合は、キーストアより優先されます。既存の平文トークンはそのまま使用でき、次回の --auth で暗号化されます。知らないうちに書き換えられることはありません。保護の範囲にご注意ください。これはファイルの盗難に対する保護であり、ユーザー権限で動作するマルウェアに対する保護ではありません。そのようなマルウェアは、mailwarden と同じようにキーストアに鍵を要求したり、環境変数からパスフレーズを読み取ったりできます。

クイックスタート

claude mcp add mailwarden -- npx -y mailwarden

インストールはこれだけです。npx が公開済みパッケージを取得して実行するため、リポジトリのクローンやビルドは不要です。必要なのは、Google OAuth 認証情報を一度用意することだけです(以下を参照)。

セットアップ

Google OAuth アプリを初めてセットアップする場合は、手順を追ったセットアップガイド に従ってください。Google Cloud Console での正確なクリック手順、「未確認のアプリ」画面の説明、トークンが 7 日後に使えなくなる原因となる落とし穴について説明しています。概要は次のとおりです。

  1. Google Cloud: プロジェクトを作成 → Gmail API を有効化 → OAuth 同意画面を設定して本番環境に公開(テスト ステータスでは、Google が 7 日後にリフレッシュトークンを期限切れにします)→ デスクトップアプリ タイプの OAuth クライアント ID を作成 → credentials.json としてダウンロードします。
  2. credentials.json を ~/.mailwarden/ に置きます(または MAILWARDEN_CREDENTIALS=/path/to/credentials.json を設定します)。
  3. 一度認証します。ブラウザーが開き、リフレッシュトークンが ~/.mailwarden/token.json に保存されます。
    npx -y mailwarden --auth
    
    要求されるスコープ:gmail.modify(読み取り+ラベル付け/アーカイブ/ゴミ箱への移動)と gmail.settings.basic (フィルター管理のみ)。フィルターが追加される前のバージョンで認証した場合は、追加されたスコープを許可するため、--auth をもう一度実行してください。Gmail 自体が送信を拒否するトークンを保持するには、MAILWARDEN_TOOLS=read で認証します。上記の読み取り専用モードを参照してください。
  4. 組み込みの doctor を使って、いつでもセットアップを確認できます。
    npx -y mailwarden --check
    
    credentials.json の有無、トークンが存在するか(および暗号化されているか)、許可されたスコープが有効な階層をカバーしているかを確認し、Gmail にライブ呼び出しを 1 回行ってトークンが まだ機能することを検証します。問題があれば具体的な修正方法を表示し、該当する場合はゼロ以外の終了コードを返します (CI/ヘルスチェックに便利です)。一般的な落とし穴を診断します。認証情報ファイルがない/間違っている、まだ一度も 認証していない、MAILWARDEN_TOKEN_PASSPHRASE が設定されていない、またはこのマシンのキーストアにキーがない暗号化トークン、スコープの不足、 あるいは 7 日後の「テスト」同意画面によるトークンの期限切れです。

接続

Claude Code(ローカル stdio):

claude mcp add mailwarden -- npx -y mailwarden

Claude Code プラグイン — 同じサーバーに 2 つのスキルを追加します。/mailwarden:setup は OAuth のセットアップを案内し、問題のある設定を診断します。/mailwarden:triage は、メールボックスの作業中にアシスタントが従うべき運用ルールを伝えます。特に、search は再検証を行いますが、bulk_modify は行いません。そのため、unverifiedPredicates が空でない場合は、対象に対して操作する前に search でその集合を解決する必要があります。このルールがユーザーを保護するのは、アシスタントが従う場合に限られます。また、ツールの説明は呼び出しごとに一度読まれますが、スキルは計画を立てる前に読まれます。リポジトリのルートがプラグイン(.claude-plugin/plugin.json)なので、クローンからは次のようにします。

claude --plugin-dir /path/to/mailwarden

Anthropic のコミュニティマーケットプレイスに申請済みです。掲載後は、/plugin marketplace add anthropics/claude-plugins-community に続けて /plugin install mailwarden@claude-community を実行すれば、クローンなしで同じように使えます。プラグインは公開済みの正確なバージョン(リリーススクリプトが server.json とともに更新する npx -y mailwarden@<version>)を起動します。ディレクトリでは特定のリビジョンが審査されるため、後から npm が提供する任意のバージョンを実行するランチャーは受け付けられません。プラグインではすべてのツールを利用できます。より限定された階層(MAILWARDEN_TOOLS=read)や 2 つ目のアカウントを使う場合は、代わりに必要な環境変数を指定して claude mcp add を使用してください(設定と複数アカウントを参照)。この方法ではサーバーのみがインストールされ、スキルは含まれません。npm パッケージにはサーバーだけが含まれ、スキルはこのリポジトリの skills/ フォルダーにあります。claude mcp add の設定でも triage を使うには、クローンから skills/triage を ~/.claude/skills/ にコピーしてください。

Claude Desktop — claude_desktop_config.json に追加します。

{
  "mcpServers": {
    "mailwarden": { "command": "npx", "args": ["-y", "mailwarden"] }
  }
}

または、MCPB バンドル(0.10.0 以降のGitHub リリースに添付されている mailwarden-<version>.mcpb)を Desktop 拡張機能としてインストールできます — Settings → Extensions → Install extension… — 実行時に自己完結する同じサーバーです(npx は不要で、Claude Desktop が Node ランタイムを用意します)。ツール階層は設定として選択できます。バンドルは、パッケージ化した npm パッケージ(公開済みのものと同じファイル一式。npm run mcpb でビルドし、CI で検証、展開、起動確認済み)に加え、宣言された各プラットフォーム用の OS キーストアバインディングから構成され、ロックファイルによる固定とハッシュ検証が行われます。初回の npx -y mailwarden --auth は引き続き必要です(その際は Node が必要です)。バンドルは同じ ~/.mailwarden/ のトークンを読み込みます。

Smithery — ディレクトリに csitte/mailwarden として掲載されています。Smithery は 2026 年 10 月 31 日に CLI、ツールボックス、ゲートウェイ、アカウントを終了し、公開ディレクトリとしてのみ存続します (告知)。そのため、Smithery からインストールする方法はもうありません。上記の npx または .mcpb でインストールしてください。その日以降も掲載が続くかどうかは Smithery 次第です。

リモート(Streamable HTTP) — VPS または claude.ai のカスタムコネクターを使う場合:

# Loopback + token required by default. For real hosting, bind outward and keep the token:
MAILWARDEN_TOKEN=<secret> MAILWARDEN_HOST=0.0.0.0 npx -y mailwarden --http   # :8787/mcp

次に claude.ai で、Settings → Connectors → Add custom connector → https://your-host/mcp の URL を指定します。Claude Code では、claude mcp add --transport http mailwarden https://your-host/mcp を実行します。

複数アカウント

1 つの OAuth アプリ(1 つの credentials.json)で、複数の Gmail アカウントを認証できます。各アカウントは、MAILWARDEN_ACCOUNT で選択される別々のファイルに、それぞれのリフレッシュトークンを保持します。

mailwarden --auth --account work        # stores token.work.json
mailwarden --auth --account personal    # stores token.personal.json

各アカウントに独自の MAILWARDEN_ACCOUNT を設定してサーバーをアカウントごとに 1 回登録すれば、並行して実行できます。各インスタンスは、それぞれ独自のトークン、付与されたスコープ、ツール群を持ち、ツール呼び出しは、その呼び出しを含むエントリのアカウントに対してのみ実行されます。

{
  "mcpServers": {
    "gmail-work":     { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work" } },
    "gmail-personal": { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "personal" } }
  }
}

アカウント名では大文字と小文字が区別されません。ファイル名になるため、Work と work は Windows/macOS では同じファイルになります。mailwarden は小文字に変換するため(--account Work → token.work.json)、名前は常に 1 つのメールボックスにだけ対応します。

--auth が書き込むファイルは、--account / MAILWARDEN_ACCOUNT のみによって決まり、ブラウザーで選択したアカウントには左右されません。したがって、--account を指定せずに 2 つ目のメールボックスを認証すると、1 つ目のトークンファイルがそのまま対象になるため、--auth は最初に確認し、別のメールボックスのトークンを置き換えるのではなく、拒否します。--force を指定すると、意図的に上書きできます。この 2 つの設定は相互に置き換えられません。MAILWARDEN_ACCOUNT は、1 つの設定ディレクトリから複数のメールボックスを利用するための設定で(token.<name>.json を選択します)、MAILWARDEN_DIR はディレクトリ全体を移動する設定です。環境を完全に分けたい場合には便利ですが、1 つのディレクトリ内に 2 つ目のアカウントを作るものではありません。リポジトリのクローンから npm run auth を実行すると、どちらも渡されないため、常にデフォルトのアカウントが使われます。

mailwarden --check を実行すると、アクティブなアカウントと、見つかったその他のアカウントが表示されます。MAILWARDEN_ACCOUNT が設定されていない場合は、これまでどおりすべてにデフォルトの token.json が使われるため、完全な後方互換性があります。

2 つを超えるアカウント

2 つのエントリなら簡単です。それ以上になると、この設計の 2 つの特性が重要になってきます。

各インスタンスは独自のツールを持ちます。 ティアは read が 11 個、manage が 16 個、filters が 4 個に分かれているため、フル構成のインスタンスは 31 個のツールを公開し、4 つでは合計 124 個になります。必要に応じてツール群を検索するクライアントなら対応できますが、すべての定義をコンテキストに保持するクライアントでは対応できません。

1 つのクライアントに複数のアカウントを設定すると、モデルコンテキストを共有します。 アカウント境界によって、呼び出しは 1 つのメールボックスに限定されます。しかし、すべてのツール群が同じモデルの前にあるため、1 つのメールボックスから読み取ったテキストが別のメールボックスに対する呼び出しを促すことまでは防ぎません。これは境界の限界であり、欠陥ではありません。SECURITY.md の脅威 8 を参照してください。

両方の問題を解決する方法は 1 つです。書き込みツールを使えるメールボックスを 1 つだけにし、残りは read のままにします。

{
  "mcpServers": {
    "gmail-main":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "main" } },
    "gmail-work":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-club":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "club",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-archive":{ "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "archive","MAILWARDEN_TOOLS": "read" } }
  }
}

これにより、3 つの効果が同時に得られます。read エントリが要求するのは gmail.readonly だけとなり、送信しないことが mailwarden のツール群ではなく Google によって強制されます。4 つのインスタンスのツール数は合計 124 個ではなく 64 個になります。また、いずれかに挿入された指示が、別のメールボックスにアクセスするための書き込みツールを見つけることもありません。読み取り専用のメールボックスを整理する必要がある場合は、恒久的に権限を変更するのではなく、作業に必要な間だけそのエントリを manage に切り替えます。

クライアントやセッションを分ければ、共有コンテキストを完全になくせます。その代わり、2 つのメールボックスを同時に表示できなくなります。複数のメールボックスで本当に書き込みツールが必要な場合には有効ですが、そうでなければティアの分割のほうが手軽な境界です。

どのツールも複数のメールボックスをまたいで読み取りません。 search、triage_digest、list_subscriptions はそれぞれ、インスタンスに設定された 1 つのアカウントだけを対象とします。そのため、「4 つすべてにニュースレターを送っているのはどれですか」のような質問には、4 回呼び出して、その結果を呼び出し元がまとめます。この規模の構成では、何かを変更する最初の操作の前に get_profile を呼び出すことをおすすめします。これにより、実際に接続されているメールボックスがわかります。

ソースから

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node dist/index.js --auth

クローンには意図的に CLAUDE.md が含まれていません。このリポジトリの作業指示は内部文書であり、ここでは追跡されていないためです。ビルド、テスト、npm run smoke のいずれにも、このファイルは必要ありません。貢献者やコーディングエージェントが実際に把握しておく必要のあるルールは、CONTRIBUTING.md(変更できない設計ルール、ビルド/テストの手順)と、そうしたルールの根拠となる脅威モデルを記した SECURITY.md にあります。

設定(環境変数)

Var意味
MAILWARDEN_DIR設定ディレクトリ(デフォルトは ~/.mailwarden)
MAILWARDEN_CREDENTIALScredentials.json へのパス
MAILWARDEN_ACCOUNT名前付きアカウントを選択します(トークンは token.<name>.json。名前は小文字に変換されます)。未設定の場合はデフォルトの token.json を使用します。複数アカウントを参照してください
MAILWARDEN_TOKEN_PASSPHRASEパスフレーズから導出したキー(AES-256-GCM)で token.json を保存時に暗号化します。キーストアのキーより優先されます。設定後、--auth を再実行してください
MAILWARDEN_KEYSTOREauto(デフォルト):利用可能な OS キーストアのキーで新しいトークンを暗号化します。off:キーストアには書き込みません。この場合、新しいトークンはパスフレーズで暗号化して保存するか、平文で保存されます。書き込みにのみ適用されます。すでにキーストアのキーで保存されたトークンには、引き続きそのキーが必要です
MAILWARDEN_AUTO_SWEEP1 → 起動時と実行中の毎時、スヌーズの一括処理を行います(ラベルを書き込みます。manage/gmail.modify スコープが必要です。read のみの許可では一括処理できません)
MAILWARDEN_DOWNLOAD_DIRdownload_attachment と export_thread の保存先をこのディレクトリに制限します。未設定の場合、クライアントはこのプロセスが書き込み可能な任意のパスを選択します。--http では manage ティアが無効でない限り、起動時に警告が表示されます
MAILWARDEN_CONFIRMcreate_filter、unsubscribe、bulk_unsubscribe の実行前に、MCP エリシテーションダイアログを通じてクライアント側の利用者に確認します。回答がモデルを経由しないため、プロンプトインジェクションされたメールから回答を入力させることはできません。ask:クライアントがエリシテーションに対応している場合は確認し、対応していない場合は続行します。require:対応していない場合は拒否します。off(デフォルト):確認しません。無人実行では、誰も見ないダイアログによって処理が失敗するためです。「いいえ」の場合は declined_by_user エラーとなり、何も実行されません。ドライランでは確認を求めません
MAILWARDEN_READONLY1 → 読み取りツールのみを登録します(search/get_thread/read_attachment/list_labels/list_snoozed/get_profile/what_changed/triage_digest/follow_ups/list_unsubscribe/list_subscriptions)。MAILWARDEN_TOOLS=read の短縮形です
MAILWARDEN_TOOLS公開するツールのティアをカンマ区切りで指定します:read、manage、filters(デフォルト:すべて)。--auth で要求する OAuth スコープもこれに基づいて決まります。例:read,manage を指定すると、フィルターツールとその gmail.settings.basic スコープが除外されます
MAILWARDEN_DEBUG1 → 1 行のメッセージではなく、スタックトレースを含む完全なエラーを出力します(バグ報告用)
PORTHTTP ポート(デフォルトは 8787)
MAILWARDEN_HOSTHTTP バインドアドレス(デフォルトは 127.0.0.1。リモートホスティングでは、たとえば 0.0.0.0 を設定します)
MAILWARDEN_TOKENHTTP エンドポイント用のベアラートークン。上書きしない限り、--http では必須です
MAILWARDEN_ALLOW_NO_TOKEN1 → トークンなしでの --http を許可します(信頼できるネットワーク、または隔離されたネットワークのみ)
MAILWARDEN_ALLOWED_HOSTSループバックの Host 許可リストに追加する、許可対象の host:port 値をカンマ区切りで指定します

ステータス

動作しており、日々のメールボックス自動化に利用されています。中核となる Gmail ツールとスヌーズ機能は @googleapis/gmail を使用して実装され、vitest スイート(1709 テスト — npm run coverage)でカバーされています。現在のバージョンについては、上の npm バッジ、変更履歴、またはリリースを参照してください。PR を歓迎します。ビルド/テストの手順と変更できない設計ルールについては CONTRIBUTING.md をご覧ください。

ライセンス

MIT © C.Sitte Softwaretechnik

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。