mcpField
M
mail-muncher

mail-muncher

ローカル
craigjmidwinter.github.ioコミュニケーション310 週間前に更新

mail-muncher について

mail-muncher は IMAP または Gmail のメールを選別し、自動処理やエージェント向けにディスクへ保存します。定期実行やポーリングに加え、同じアーカイブを MCP 経由で照会できます。

主な機能

  1. 01IMAP と Gmail から読み取り専用で取得
  2. 02順序付きルールと動的なファイルフィルター
  3. 03EML と Markdown で保存
  4. 04増分配信と重複排除
  5. 05定期実行、デーモン、MCP に対応

活用シーン

  1. 01応募先ドメインごとに選考メールを保存
  2. 02サポートボット向けに取引先メールを収集
  3. 03スレッドや日付で保存済みメールを検索

MCP 対応

stdio MCP サーバーとして動作し、ルールやメールの確認、検索、スレッドの閲覧、新着メールの同期を提供します。アクセスは設定済みの保存先に限定され、同期で行う変更はファイルの追加のみです。

設定

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

mcp.json · 共通設定
{
"mcpServers": {
"craigjmidwinter-mail-muncher": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/craigjmidwinter/mail-muncher:0.4.0"
]
}
}
}
起動コマンドdocker run -i --rm ghcr.io/craigjmidwinter/mail-muncher:0.4.0

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

パッケージ
ocighcr.io/craigjmidwinter/mail-muncher:0.4.0

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

封筒にかみつく mail-muncher のアーカイブ獣
mail-muncher

mail-muncher

CI Go Reference Go version Release License: MIT

プログラム専用の読み取り専用メールボックスを用意し、要求したメールだけに絞り込んで、ディスク上のファイルとして届けます。

mail-muncher はメールプロバイダーからメッセージを取得し、それぞれを順序付きルールに照らして評価して、一致したメッセージをディレクトリに書き込みます。バイト単位で忠実な .eml ファイルに加え、オプションで、ヘッダーを YAML フロントマター、本文をテキストとして表示し、添付ファイルを隣に抽出したマークダウン形式も生成します。ルールのフィルター入力には、別のプログラムが管理するプレーンテキストファイルを指定できます。mail-muncher は各サイクルの開始時にこのファイルを読み直します。もう一方のプログラムがそのファイルの1行を変更すると、次のサイクルで異なるメールが届きます。設定の編集も、再起動も、再デプロイも必要ありません。

Gmail、Fastmail、iCloud、Proton Bridge、職場のアカウント、自分のサーバーなど、あらゆる IMAP メールボックスから読み取れます。また、読み取り専用の OAuth スコープを使って Gmail API から読み取ることもできます。cron 用の単発実行、ポーリングデーモン、またはエージェントが直接クエリできる stdio MCP サーバーとして動作します。どのモードでも、実行内容を示す同じ機械可読マニフェストを出力し、メールボックスへの書き込みは一切行いません。

メールボックスへの2つの接続方法

何かをインストールする前に、どちらかを選んでください。どちらもサポートされており、以降のルール、形式、ファイル名、アーカイブの構成、MCP ツールは、どちらを選んでも同じです。

provider: imapprovider: gmail
セットアップ時間約2分Google Cloud Console で約10分
登録するものなしご自身の Google Cloud プロジェクトとデスクトップアプリの OAuth クライアント
認証情報プロバイダーの設定ページで発行するアプリパスワードOAuth トークン、スコープは gmail.readonly
認証情報が許可する操作の範囲メール全体の認証情報です。 アプリパスワードでメールの送信や削除ができます読み取り専用で、それ以外はできません
読み取り専用を強制する主体mail-muncher 自身のコードGoogle
有効期限なしテストモードの同意画面では7日ごと。毎週 mail-muncher auth を再実行する必要があります
シークレットの保存場所すでにパスワードマネージャーで管理している場所です。password_cmd を実行し、その標準出力をパスワードとして使います。意図的に password キーは用意していませんtoken.json。モードは 0600 で、mail-muncher auth によって書き込まれます
対応するメールボックスmailboxes: に列挙したフォルダー。デフォルトは [INBOX] ですGmail アカウント全体。ただし、指定しない限り迷惑メールとゴミ箱は除きます
対応サービスGmail、Fastmail、iCloud、Proton Bridge、職場のアカウント、セルフホスト環境Gmail のみ
追加手順なし。この方法では auth コマンドはありませんdocs/gmail-setup.md に沿って設定した後に mail-muncher auth

上記の約2分、約10分、7日という数字は、mail-muncher init と未設定時の実行案内にも表示されます。どちらを選ぶかを決める数字だからです。

どちらの方法も、本当に読み取り専用を保証します。ただし、その保証は同じではなく、ひとくくりにするのは誠実ではありません。

  • Gmail:Google が強制します。 要求するスコープは gmail.readonly だけです。返されるトークンでは、送信、削除、ラベル付け、変更はできません。mail-muncher が操作を控えるからではなく、Google がその呼び出しを拒否するためです。このプログラムにバグがあっても、メールボックスにアクセスすることはできません。
  • IMAP:mail-muncher が強制します。 IMAP では、読み取り専用の認証情報を要求できません。アプリパスワードはメール全体の認証情報であり、プロトコル上はフォルダーを削除することも可能です。そこで mail-muncher は削除しないようにしています。すべてのフォルダーを EXAMINE で開き、SELECT は使いません。すべての本文を BODY.PEEK[] で取得し、BODY[] は使わないため、メールが既読にされることもありません。また、プロバイダー内には STORE、APPEND、EXPUNGE を実行するコードパスがありません。サーバーがクライアント自身からクライアントを保護する義務はないため、両方の対策を講じています。これは強力で監査可能な保証ですが、メールプロバイダーの保証ではなく、このプログラム自身の保証です。

Gmail API を使う具体的な理由がなければ、まずは IMAP をお試しください。Gmail アカウントでも使え、クイックスタートで案内している方法です。

問題点

自動化されたプロセスがメールを必要とすることがあります。就職活動のトラッカーは、応募先企業からの返信を必要としています。サポートボットは、特定のベンダーのドメインから届くメッセージを必要としています。リサーチエージェントは、3つの出版社のニュースレターをすべて、実際に読めるテキスト形式で必要としています。

一般的な解決策には、どれも問題があります。プロセスに受信トレイの認証情報を渡すと、すべてのメールを読めるだけでなく、送信や削除もできてしまいます。メール API との連携を実装すると、メールを必要とするすべてのプロセスで OAuth フロー、同期カーソル、MIME 解析、重複排除の仕組みを保守することになります。または、フィルターを設定ファイルにハードコードすると、必要なものを変えるたびに設定ファイルを編集して再デプロイしなければなりません。

mail-muncher は、その負担を2つに分けます。認証情報、差分同期、解析、重複排除は mail-muncher が管理します。利用するプログラムが用意するのは、必要なものを記したテキストファイルと、結果を読み込むディレクトリだけです。監視するのではなく問い合わせたい場合は、同じディレクトリを操作する少数の MCP ツールも使えます。

エージェントのワークフロー

サポートされている形式は2つあり、組み合わせて使えます。エージェントが独自のループで動作するのか、依頼されるまで待機するのかに応じて選んでください。

  • ファイルドロップ — mail-muncher がスケジュールに従って動作し、ファイルを書き込みます。エージェントはディレクトリを読み取ります。互いに呼び出すことはありません。以下で説明する形式です。
  • ツール呼び出し — エージェントが MCP 経由で mail-muncher mcp と通信し、購読しているもの、届いたもの、スレッドの内容、今すぐ取得する方法などを直接質問します。形式 2:ツール呼び出しを参照してください。

どちらも同じアーカイブを読み取ります。また、両方を同時に実行するのは一般的です。デーモンがディレクトリにデータを格納し、その内容に関する質問に MCP サーバーが応答します。

形式 1:ファイルドロップ

このループは完全に分離されています。mail-muncher がエージェントを呼び出すことはなく、エージェントが mail-muncher を呼び出す必要もありません。ディスク上の2つのパスを共有します。

1. エージェントが必要なものを宣言します。 エージェントが所有するファイルに追記します。

mkdir -p ~/.local/share/agent
cat >> ~/.local/share/agent/domains.txt <<'EOF'
# domains this agent is currently interested in
acme.com
globex.io
EOF

2. mail-muncher がその宣言を購読します。 ファイルを指定した単一のルールを設定します。

rules:
  - name: agent-inbox
    match:
      from_domains_file: ~/.local/share/agent/domains.txt
    dest: ~/mail/agent-inbox
    formats: [eml, markdown]

3. サイクルごとにファイルを再読み込みします。 cron から実行するか、デーモンを起動したままにします。

mail-muncher run                 # one cycle — the cron entrypoint
mail-muncher daemon --interval 5m  # poll forever

4. 条件に一致したメールは dest にファイルとして保存され、エージェントが読み取ります。

~/mail/agent-inbox/
└── 2026/
    └── 07/
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.eml
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.md
        └── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/
            └── offer.pdf

.md はそのまま利用できる形式で出力されたものです。フロントマターを解析して本文をモデルに渡し、添付ファイルは隣接するディレクトリから開きます。

---
subject: 'Re: Your application for Senior Engineer'
from: Jane Doe <[email protected]>
from_address: [email protected]
from_addresses: [[email protected]]
to: [[email protected]]
to_addresses: [[email protected]]
date: 2026-07-28T09:15:00Z
message_id: <[email protected]>
thread_id: 18fe9c0d1a2b3c4d
thread_id_source: provider
in_reply_to: <[email protected]>
account: personal
rule: job-search
labels: [INBOX]
attachments: [offer.pdf]
---

Hi there,

Thanks for applying.

## Attachments

- [offer.pdf](1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/offer.pdf)

すべてのメッセージに thread_id が設定され、空になることはありません。そのため、ディレクトリを会話ごとにまとめるには、1つのフィールドで sort するだけです。参照チェーンを再構築する必要はありません。

5. 必要に応じて、ツリーをたどる代わりにマニフェストを使用します。 --json を指定すると、サイクルの機械可読な記録がアカウントごとに1つのオブジェクトとして標準出力に書き込まれ、すべてのログ行は標準エラー出力に送られます。

mail-muncher run --json 2>/dev/null | jq -r '.stored[].path'

完全な仕様については、docs/manifest.mdを参照してください。

自律的なループに安全に組み込める理由となる特性は3つあります。

  • 設計上、読み取り専用です。 mail-muncher はメールボックスに何も書き込みません。Gmail では、これは Google による gmail.readonly スコープの適用によるものです。IMAP では EXAMINE と BODY.PEEK[] を使用し、書き込み経路は一切ありません。いずれの場合も、出力を利用するものにどんなバグがあっても、メールの送信、削除、変更はできません。どちらの保証を得られるかについては、上記の比較を参照してください。
  • 冪等な配信です。 メッセージのファイル名には account + ":" + message id のダイジェストが埋め込まれるため、保存先のパスはメッセージの識別情報から一意に決まります。ファイルがすでに存在する場合は「以前のサイクルで保存済み」を意味し、出力先には何も書き込まれません。再実行、状態を失った後のリプレイ、サイクル中のクラッシュ、2つの cron 実行の重複が起きても、ツリーは収束し、二重処理は発生しません。
  • 決定論的なルーティングです。 ルールは順番に評価され、最初に一致したルールが適用されるため、各メッセージは必ず1つのルールによって書き込まれます。各利用者に専用のルールと dest を割り当てれば、それぞれが他のプロセスから書き込まれない専用のメールボックスを利用できます。

配信先はディスク上のファイルであり、ここでネットワーク上の接続を待ち受けるものはありません。仕様となるのはディレクトリで、マニフェストは変更内容を記録するためのオプションの機械可読な記録です。

形状 2:ツール呼び出し

mail-muncher mcp は、すでにアーカイブ済みのメールを対象とする stdio MCP サーバーです。エージェントが要求したときに動作し、定期実行はされません。

{
  "mcpServers": {
    "mail-muncher": {
      "command": "/usr/local/bin/mail-muncher",
      "args": ["mcp", "--config", "/Users/you/.config/mail-muncher/config.yml"]
    }
  }
}

ツールは 5 つです。

ツール回答する内容
list_rules何を収集していて、現在どの送信者を購読しているか。各 from_domains_file は呼び出しのたびに再読み込みされます。
list_messages何が届いたか。ルール、アカウント、スレッド、日付で絞り込めます。任意で会話ごとにグループ化できます。
search_messagesX に言及しているメッセージはどこにあるか。件名、送信者、宛先、ラベル、添付ファイル名、本文を対象に部分一致検索します。
read_message1 件のメッセージ全体(メタデータ、本文、添付ファイル名とサイズ)を取得し、任意でスレッド全体を順番に取得します。
sync新着メールを 1 回取得し、run --json が書き出すものと同じマニフェストを返します。

メールに対しては読み取り専用です。どのツールもメールの送信、削除、変更は行わず、唯一何かを変更する sync もファイルを追加するだけです。ファイルシステムへのアクセスは、設定済みのルールの dest ルートに制限されるため、設定、保存済みの認証情報、状態ディレクトリにはアクセスできず、名前も取得できません。

未設定の mcp サーバーも起動します。これは意図した動作です。 設定がない状態でクライアントが mail-muncher mcp を起動しても、サーバーは終了しません。ハンドシェイクを完了して同じ 5 つのツール名を登録し、すべての呼び出しに対してセットアップ手順をツールエラーとして返します。そのため、エージェントは「サーバーの起動に失敗しました」と伝える代わりに、手順を伝えられます。運用担当者向けに設定している場合、これは想定どおりの動作であり、バグとして報告する必要はありません。手順は起動時に stderr にも出力され、クライアントはサーバーログを tee で記録します。

詳しいリファレンス、クライアントの設定、すべての引数と戻り値のフィールドについては、 docs/mcp.md を参照してください。

list_rules は一連の流れを完結させるツールです。エージェントが自分のファイルにドメインを書き込み、次に list_rules を呼び出すと、自分の購読内容が反映されていることを確認できます。次のサイクルで照合されるのと同じ一覧です。

代替ツール

導入を決める前にお読みください。メールの取得、フィルタリング、配信をうまく行えるツールはいくつかあり、このツールより適している場合もあります。

ツール代わりにこちらを使う場合
getmail6成熟していて広くパッケージ化されているフェッチャーが必要な場合。IMAP と Gmail OAuth2 に対応し、Maildir/MDA に配信して、外部プログラムでフィルタリングできます。人間(または mutt、notmuch)が利用するなら、こちらのほうが優れたツールです。
fdmコンパクトで十分にテストされた設定により、ルールごとに Maildir の保存先を指定したい場合。このツールと同じ用途ですが、外部のフィルターソースはありません。Gmail へのアクセスにはアプリパスワードを使う IMAP が必要です。
lieerGmail のメールボックス全体を notmuch 用のローカル Maildir に双方向同期したい場合。フィルタリングした一部を取り出す用途ではありません。
gmail-archiveほぼまさにこれと同じ、Gmail のクエリから Maildir への差分取得を行うツールでした。今もメンテナンスされていれば、真っ先におすすめするところです。2018 年以降、メンテナンスされていません。
gmail-exporter差分同期ではなく、ラベルベースでスプレッドシート形式のエクスポートを一度だけ行いたい場合。
mbsync / offlineimapメールボックス全体を複製し、その後ローカルでフィルタリングする場合。

これらのどれも行わず、このツールが存在する理由となっているのは、別のプログラムが管理するファイルからフィルター条件を受け取り、サイクルごとに再読み込みすることと、メールクライアントで表示するためではなくプログラムが利用するための形式で出力することです。この両方が必要でなければ、上記のツールのほうが適しており、実績も豊富です。

インストール

最初の 2 つの方法では、Go ツールチェーンは必要ありません。

Homebrew
brew install craigjmidwinter/tap/mail-muncher

craigjmidwinter/homebrew-tap を tap し、ビルド済みバイナリをインストールします。brew upgrade mail-muncher で新しいリリースに更新できます。

バイナリをダウンロードする

各リリース には、amd64 と arm64 の macOS および Linux 用アーカイブに加えて、 checksums.txt とその署名が含まれています。

# Latest release, without the leading v. Set this by hand to pin a version.
VERSION=$(curl -fsSL https://api.github.com/repos/craigjmidwinter/mail-muncher/releases/latest \
  | sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p')

OS=$(uname -s | tr '[:upper:]' '[:lower:]')     # darwin | linux
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')

curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/mail-muncher_${VERSION}_${OS}_${ARCH}.tar.gz"
tar xzf "mail-muncher_${VERSION}_${OS}_${ARCH}.tar.gz" mail-muncher
sudo install -m 0755 mail-muncher /usr/local/bin/mail-muncher

その後、バイナリがまったく実行できない場合は —

bash: mail-muncher: cannot execute binary file: Exec format error

— アーキテクチャが異なるアーカイブをダウンロードしています。このメッセージは カーネルからのもので、mail-muncher について何かを示すものではありませんが、 どのようなメッセージなのか知っておくと役立ちます。uname -m の結果を、 ダウンロードしたファイル名に含まれる _amd64 / _arm64 と照らし合わせてください。 上の ARCH= 行で適切な値が決まるため、手動で名前を設定した場合にのみ この問題が発生します。

root 権限がありませんか? /usr/local/bin へのインストールには必要ですが、 ~/.local/bin へのインストールには不要です。sudo を外して、代わりにそこへ インストールしてください。mail-muncher にシステム全体の場所は必要ありません。

install -d ~/.local/bin
install -m 0755 mail-muncher ~/.local/bin/mail-muncher

その後 mail-muncher が「command not found」と表示される場合は、~/.local/bin が PATH に含まれていません。シェルのプロファイルに追加してください。

インストール先を変更せずに sudo だけを省くと、install 自体から Permission denied が返されます。macOS では一時ファイル名が mail-muncher ではないため、 初めて見ると戸惑うかもしれません。

install: /usr/local/bin/INS@LPh1Hz: Permission denied     # macOS
install: cannot create regular file '/usr/local/bin/mail-muncher': Permission denied   # GNU

どちらのメッセージも意味は同じです。上記の ~/.local/bin を使うか、sudo を戻してください。

macOS では、自分でダウンロードしたバイナリは Gatekeeper によって隔離されます。 xattr -d com.apple.quarantine /usr/local/bin/mail-muncher で隔離を解除するか、 上記の Homebrew インストールを使用してください。こちらでは隔離が自動的に解除されます。

ダウンロードしたファイルを検証する

このツールはメールを読み取ります。アーカイブがリリースのワークフローで ビルドされたものか確認してください。まず、チェックサムを確認します。

curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/checksums.txt"

# Linux
sha256sum --check --ignore-missing checksums.txt
# macOS
shasum -a 256 --check --ignore-missing checksums.txt

次に、checksums.txt の署名を確認します。リリースは cosign で鍵を使わずに 署名されています。取得する公開鍵も、保管する必要のある秘密鍵もありません。 署名証明書はリリースワークフロー自身の GitHub OIDC ID に対して発行され、 公開 Rekor 透明性ログに記録されています。そのため、確認しているのは 「このリポジトリの release.yml がタグからビルドしたものかどうか」です。

cosign はどのプラットフォームにもデフォルトではインストールされておらず、 一般的なディストリビューションのリポジトリにも含まれていません。そのため、ここで cosign: command not found と表示されても、「まだインストールされていない」という意味であり、 「検証に失敗した」という意味ではありません。まずインストールしてください。 brew install cosign、go install github.com/sigstore/cosign/v2/cmd/cosign@latest、 またはインストール ドキュメントからリリース バイナリを入手できます。

curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/checksums.txt.sig"
curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/checksums.txt.pem"

cosign verify-blob \
  --certificate checksums.txt.pem \
  --signature checksums.txt.sig \
  --certificate-identity-regexp '^https://github\.com/craigjmidwinter/mail-muncher/\.github/workflows/release\.yml@refs/tags/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

Verified OK と表示されたら、チェックサムファイルは本物です。続けて sha256sum を 実行すると、手元のアーカイブがそのファイルに対応していることを確認できます。cosign 3 では --certificate と --signature の非推奨通知が表示されますが、検証は引き続き実行されます。 また、これらの分離ファイルは cosign 2 でも理解できます。

go install

Go 1.25 以降がすでにインストールされている場合は、この方法が適切です。

go install github.com/craigjmidwinter/mail-muncher/cmd/mail-muncher@latest

go install でビルドした場合、--version の結果は dev になります。バージョンは リンク時に埋め込まれますが、go ツールはそれを行わないためです。リリースバイナリと make build では、実際のタグが表示されます。go install でビルドしたものから バグを報告する場合は、インストールしたコミットを記載してください。

ソースからビルドする
git clone https://github.com/craigjmidwinter/mail-muncher
cd mail-muncher
make build          # -> ./mail-muncher, version stamped from git describe

リリースに含まれる内容を確認したい場合は、make snapshot を実行すると、リリース用 アーカイブ一式をローカルでビルドできます(goreleaser が必要です)。

以下で参照する設定例は examples/ にあります。 imap.yml、minimal.yml、 job-search.yml です。これらは各リリースアーカイブにも 同梱されているため、バイナリをダウンロードした場合も利用できます。ただし、使い始めるのに これらは必要ありません。mail-muncher init を実行すると、設定を一から作成できます。

コンテナーイメージ
docker pull ghcr.io/craigjmidwinter/mail-muncher:latest

linux/amd64 と linux/arm64 に対応し、リリースアーカイブに含まれるものと同じバイナリーからビルドされています。イメージのデフォルトコマンドは mcp です。アーカイブを stdio 経由で提供するのがコンテナーに適したモードだからです。クライアントがコンテナーを起動して通信し、停止します。run と daemon も動作しますが、コマンドを上書きする必要があります。ホストでは、それらは cron の設定や launchd/systemd のユニットとして使うほうが適しています。

2 つのマウントが必要で、どちらも重要です。

# -e IMAP_PASSWORD forwards the variable, it does not invent it: export it
# first, from wherever you actually keep the secret.
export IMAP_PASSWORD="$(security find-generic-password -s mail-muncher -w)"

docker run -i --rm \
  -e IMAP_PASSWORD \
  -v ~/.config/mail-muncher:/home/muncher/.config/mail-muncher:ro \
  -v ~/.local/share/mail-muncher:/home/muncher/archive \
  ghcr.io/craigjmidwinter/mail-muncher:latest mcp

この export は、設定ファイルの password_cmd: printenv IMAP_PASSWORD と組み合わせます。ホストのパスワードマネージャーにコンテナー内からアクセスできない理由については、下の注記を参照してください。

config.yml 内のすべてのパスは、コンテナーから見えるパスでなければなりません。 dest: に ~/Mail/receipts を指定すると、自分のホームディレクトリーではなくコンテナーのホームディレクトリーを基準に解決されるため、メールはコンテナーの終了時に消えるレイヤーに保存されます。dest: にはマウントしたディレクトリー(上記のマウントの場合は /home/muncher/archive/receipts)を指定してください。そうしないと、何もない場所にアーカイブし、マニフェストには問題なく処理できたと表示されてしまいます。

password_cmd は /bin/sh のもとでコンテナー内で実行されます。つまり、ホストのパスワードマネージャーは使えません。pass show mail/fastmail は動作しません。コンテナー内で使えるシークレット情報を使ってください。

password_cmd: printenv IMAP_PASSWORD          # -e IMAP_PASSWORD
password_cmd: cat /run/secrets/imap-password  # docker secret or a mounted file

ここは、コンテナーを使う方法がホストにインストールするより実際に不便な唯一の点です。認証情報をパスワードマネージャーからコンテナーの環境に移すことになります。この引き換えが割に合わない場合は、バイナリーをインストールしてください。password_cmd はホストでの使用を想定して設計されており、これは妥協案であって、本来の意図ではありません。

このイメージは、MCP Registry の掲載内容を支えています。掲載情報は server.json にあり、その name はイメージに組み込まれた io.modelcontextprotocol.server.name ラベルと一致している必要があります。

掲載情報の公開は自動で行われます。リリースにタグを付けるとイメージがビルドされてプッシュされ、その後、2 つ目のジョブが git タグをもとに server.json 内の version とイメージタグを書き換え、保存済みのトークンではなくワークフロー自身の OIDC ID で認証してレジストリーに公開します。

そのため、server.json にコミットされている version は直近のリリースのもので、意図的に 1 つ前のタグのままになっています。 正式な情報源はタグであり、このファイルは CI が値を設定するテンプレートです。手動で更新しても何も変わりません。

Claude Code スキルとして

このリポジトリーには skills/ にスキルとプラグインのパッケージが含まれています。これをインストールすると、エージェントが mail-muncher をセットアップして操作できます。設定の作成、auth の実行、クライアントへの MCP サーバーの接続を行います。この方法で導入する場合は、以下のクイックスタートではなく、まずこちらをご覧ください。

このスキルでは provider: imap を優先し、mail-muncher init を実行します。そのため、Google Cloud Console に誘導するのではなく、この README と同じく 2 分で済む手順で導入できます。

Windows

Windows ビルドはなく、上記のどの方法も目立たない形でその問題を回避することはありません。Homebrew は Windows では動作しません。リリースアーカイブは darwin と linux のみで、上記のダウンロード用スニペットは uname を使う POSIX シェルスクリプトのため、PowerShell や cmd では実行できません。

何かを生成できるのは go install だけであり、ここで問題を明確に述べておく必要があります。Go はこのモジュールを問題なくクロスコンパイルできます。cgo はなく、テストファイル以外にプラットフォーム用のビルドタグもありません。そのため、起動する mail-muncher.exe と、問題なく設定を書き込む mail-muncher init が生成されます。そして最初の run で止まります。この README の最初に紹介している、約2分で済む IMAP プロバイダーの処理では、imap.password_cmd を /bin/sh -c に渡して実行します(internal/provider/imap/password.go)。しかし、標準的な Windows マシンには /bin/sh がありません。init は macOS や Linux 用のシークレットツールを Windows の設定に書き込まないよう配慮していますが、それでも書き込まれるコマンドは存在しないシェルに渡されるため、失敗が遅れて発生し、原因を誤認させます。

Gmail プロバイダーにはこのような依存関係はありません。OAuth フローは Windows で rundll32 を使うため、最初から最後まで動作する可能性があります。ただし、Windows ではテストされておらず、サポート対象外です。

Windows で動作する方法は、Docker Desktop 上のコンテナーイメージ、または WSL2 内でのいずれかのインストール方法です。WSL2 には Linux バイナリと /bin/sh の両方が存在します。

アップグレード
brew upgrade mail-muncher            # Homebrew
go install github.com/craigjmidwinter/mail-muncher/cmd/mail-muncher@latest

ダウンロードしたバイナリを使っている場合は、上記のダウンロード手順を繰り返してください。install の手順で既存のファイルが上書きされます。それ以外に変更は不要です。設定スキーマとディスク上のレイアウトは 0.x の間は安定しており、state_dir 内の同期カーソルは、どの新しいバージョンからも読み込めます。これが当てはまらなくなる変更は CHANGELOG.md に記録されますが、現時点ではそのような変更はありません。

インストールされたバージョンは mail-muncher --version で確認できます。make を使わずにビルドした場合は dev と表示されます。これは go install のバージョン情報設定に関する注意点であり、インストールが壊れているわけではありません。

アンインストール

バイナリを削除しても、それ以外はすべて残ります。以下は、最も機密性の高いデータから先に削除する順序です。これらの操作はすべて自分で行う必要があります。

# 1. Stop it, if you scheduled it.
launchctl unload ~/Library/LaunchAgents/com.craigjmidwinter.mail-muncher.plist
rm ~/Library/LaunchAgents/com.craigjmidwinter.mail-muncher.plist
rm -f ~/Library/Logs/mail-muncher.out.log ~/Library/Logs/mail-muncher.err.log
# or, if you used cron: crontab -e and delete the line.

# 2. The credential. This is the part nothing else will clean up.
security delete-generic-password -s mail-muncher      # macOS Keychain, IMAP
# Gmail instead: revoke the app at https://myaccount.google.com/permissions

# 3. Config, credentials and the OAuth token.
rm -rf ~/.config/mail-muncher

# 4. Sync cursors, both lockfiles, and the quarantine directory.
rm -rf ~/.local/state/mail-muncher

# 5. The binary.
brew uninstall mail-muncher          # Homebrew
rm -f /usr/local/bin/mail-muncher    # downloaded binary
rm -f "$(go env GOPATH)/bin/mail-muncher"   # go install

アーカイブしたメールは、意図的にこのリストに含めていません。 ルールで指定した dest の場所に保存されています。init の既定値を使った場合は ~/Mail/mail-muncher です。メールはツールを削除した後も残る通常のファイルであり、それこそがこの形式の目的です。保存先を確認したい場合は手順3の前に grep -n 'dest:' ~/.config/mail-muncher/config.yml を実行し、メールを削除したい場合は自分で削除してください。

Homebrew の cask には zap の指定がないため、brew uninstall で削除されるのはバイナリだけで、ホームディレクトリ内のものは何も削除されません。これは意図的なものです。このツールがすでに書き込んだメールはユーザーのものであり、アンインストーラーを使って初めてそれに気づくような状況は避けるべきだからです。

クイックスタート

所要時間は約5分で、ブラウザーもリポジトリのクローンも不要です。ここでは IMAP を使う方法を説明します。代わりに Gmail API を使う場合は、始める前に下記のクイックスタート:Gmailを読んでください。Google Cloud Console で約10分かかり、トークンは毎週期限切れになります。

0. インストールを確認します。 インストール直後、設定を作成する前に実行します。

mail-muncher run

これは間違いではなく、実際に役立つスモークテストです。終了コード1で終了し、確認した場所、次に実行するコマンド、各プロバイダーの所要時間を正確に表示します。

mail-muncher is not configured.
  missing config file: /Users/you/.config/mail-muncher/config.yml
  next command:        mail-muncher init
  then:                mail-muncher validate && mail-muncher run --dry-run

init asks which provider to use. Both are supported; the costs differ.
  provider: imap   ~2 min. Gmail, Fastmail, Proton Bridge, work accounts,
    self-hosted. Needs an app password, which is a broader credential than a
    read-only OAuth token; mail-muncher only ever issues BODY.PEEK.
  provider: gmail  ~10 min in the Google Cloud Console: gmail.readonly is a
    Google restricted scope, so mail-muncher ships no OAuth client and you
    register your own. Google enforces read-only, but on a Testing-mode
    consent screen the refresh token expires every 7 days, so
    "mail-muncher auth" must be re-run weekly. Read docs/gmail-setup.md.
Docs: https://craigjmidwinter.github.io/mail-muncher/

設定が必要なすべてのコマンドでこのメッセージが表示されるため、インストールが壊れている状態と、設定が済んでいない状態を混同することはありません。

1. アプリパスワードを取得します。 Gmail、Fastmail、iCloud、Proton、勤務先のアカウントなど、メールプロバイダーの設定ページから取得してください。このパスワードはこの用途だけに使うもので、ほかのものに影響を与えずに取り消せます。すでにシークレットを保管している場所に保存してください。

# macOS Keychain
security add-generic-password -s mail-muncher -a "$USER" -w

# or pass, 1Password, secret-tool, gpg — anything that prints it on stdout

mail-muncher はこのパスワードを保存しません。指定したコマンドを実行し、その標準出力からパスワードを読み取るため、シークレットはパスワードマネージャーに残ります。設定スキーマに password: キーがないのは意図的です。

2. 設定を書き込みます。

mail-muncher init --provider imap
Account name [personal]:
Write matched mail to [~/Mail/mail-muncher]:
IMAP host: imap.fastmail.com
IMAP username: [email protected]
Password command [security find-generic-password -s mail-muncher -w]:
Wrote /Users/you/.config/mail-muncher/config.yml

Next, for provider imap:
  1. Run that password_cmd in a shell and check it prints the password and
     nothing else, for example:
       security find-generic-password -s mail-muncher -w | cat -A
     Anything else on stdout - a prompt, a warning, a trailing blank line -
     becomes part of the password and the login fails. If it is not there
     yet, create an app password with your mail provider first and store it
     where password_cmd can read it.
  2. mail-muncher validate
  3. mail-muncher run --dry-run     then     mail-muncher run
Matched mail lands in ~/Mail/mail-muncher
Docs: https://craigjmidwinter.github.io/mail-muncher/

編集する手順はありません。 init は IMAP パスに必要な情報をすべて尋ね、最初から検証に合格する設定を書き込みます。パスワードコマンドには、お使いのプラットフォームのデフォルトがあらかじめ入力されています。macOS では Keychain、Linux では secret-tool、それ以外では pass が選ばれるため、そのまま Enter キーを押しても、仮の値ではなく有効な回答になります。

各質問にはフラグが用意されているため、先に回答することも、すべてをスクリプト化することもできます。

mail-muncher init --provider imap --yes \
  --host imap.fastmail.com --username [email protected]

--yes は、妥当なデフォルトがある項目にはすべてその値を使います。そのため、--provider、--host、--username は引き続き必須です。この3つには推測して設定する価値のあるデフォルトがないため、そのことを伝え、仮の値を書き込むことはありません。

error: --host and --username required with --yes --provider imap; host and
username have no honest default to take. Run `mail-muncher init --provider
imap` without --yes to be prompted instead

残りの項目には、--account NAME、--dest DIR、--password-cmd CMD を追加して回答します。既存の設定が --force なしで上書きされることはありません。

デフォルトのパスは ~/.config/mail-muncher/config.yml です。--config を指定すると、init を含め、すべてのコマンドでパスを上書きできます。

3. パスワードコマンドがパスワードだけを出力することを確認します。

security find-generic-password -s mail-muncher -w | cat -A

| cat -A を使うと、余計なプロンプトや警告、末尾の空行を確認できます。標準出力に余計なものがあると、それもパスワードの一部として扱われ、ログインに失敗します。初回実行時に認証できない原因として、これが最もよくあります。

参考までに、init が書き込んだ内容を示します。全体にコメントが付いています。より詳細な記述例は examples/imap.yml をご覧ください。

accounts:
  - name: personal
    provider: imap
    imap:
      host: imap.fastmail.com
      username: [email protected]
      password_cmd: security find-generic-password -s mail-muncher -w
      mailboxes: [INBOX]

4. 設定を確認します。

mail-muncher validate
config: /Users/you/.config/mail-muncher/config.yml
1 account(s), 1 rule(s), state_dir /Users/you/.local/state/mail-muncher
OK

IMAP アカウントは問題なく検証できます。探す必要のある認証情報ファイルも、まだ書き込んでいないトークンも、ディスク上のファイルもありません。警告なしで OK と表示されれば、期待どおりです。validate は設定を解析し、各ルールのマッチツリーをコンパイルして、参照先のファイルを確認します。別のプログラムが管理するファイル、つまり from_domains_file や、Gmail パスで使う OAuth 認証情報とトークンが見つからない場合は、エラーではなく警告になります。

warning: rules[0].match.any[0].from_domains_file: file does not exist yet: /Users/you/.local/share/jobsearch/domains.txt (it is maintained by another program; the rule matches nothing until it appears)
OK with 1 warning

5. 実際に実行した場合の動作を確認します。

mail-muncher run --dry-run

ドライランでは、実際の実行と同じように接続、取得、評価を行い、一致したデータの保存先となるパスを表示します。ファイルの書き込みや同期状態の保存は行わないため、何度でも実行できます。ホスト、ユーザー名、password_cmd が間違っている場合も、ここで問題箇所がそのまま表示されます。

error: account "personal": imap: password_cmd "security find-generic-password -s mail-muncher -w" failed: exit status 44: security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.

6. 実行します。

mail-muncher run

init が書き込んだ設定には、72h より新しいものすべてに一致するスタータールールが1つ含まれているため、最初の実行では必ず何かが保存されます。何も保存されない場合、インストールに問題があるのかどうか判断できません。その後、もう一度実行します。すでにディスク上にあるものはすべて skipped と報告され、増分カーソルにより、2回目の実行ではサーバーとの通信がほとんど発生しません。

動作を確認できたら、スタータールールを必要な内容に絞り込み(docs/filters.md を参照)、スケジュールに登録します(スケジュール設定 を参照)。すべてのキーについては docs/configuration.md をご覧ください。

クイックスタート:Gmail

Google によって読み取り専用が保証される Gmail API を特に使いたい場合は、この手順に従ってください。始める前に、次の2つのコストを把握しておいてください。どちらも構造上避けられず、なくなることはありません。

  • Google Cloud Console で最初に約10分かかります。 gmail.readonly は Google の制限付きスコープであるため、mail-muncher は OAuth クライアントを同梱しておらず、 今後も同梱することはありません。独自のプロジェクトとデスクトップアプリ用クライアントを登録し、 JSON をダウンロードする必要があります。
  • トークンは7日ごとに期限切れになります。 Google は、テストモードのままのすべての同意画面にこの制限を適用します。あなたの同意画面もテストモードになります。毎週 mail-muncher auth を再実行する必要があります。これをなくす設定はありません。 docs/gmail-setup.md では、その理由と代替手段にかかるコストを説明しています。

どちらのコストも見合わない場合は、上記の IMAP の手順で Gmail アカウントを使えます。

mail-muncher init --provider gmail        # prints the cost warning, then writes the config
# → follow docs/gmail-setup.md: project, Gmail API, consent screen,
#   Desktop app OAuth client, save its JSON as
#   ~/.config/mail-muncher/credentials.json
mail-muncher auth --account personal      # browser consent; writes token.json 0600
mail-muncher validate
mail-muncher run --dry-run
mail-muncher run

auth は同意 URL を出力し(ブラウザーを開こうとします)、リダイレクトを受信するためにループバックポートで待ち受け、トークンをアカウントの token_file にモード 0600 で書き込みます。これは Gmail 専用のコマンドです。IMAP アカウントでは承認するものがないため、実行を拒否します。上記の IMAP クイックスタートの手順4〜6はそのまま適用されます。認証情報ファイルとトークンファイルが存在するまでは、validate によって2つの警告が報告されます。

外部管理のフィルターファイル

これはこのツールの中核となる機能なので、その動作について正確に説明します。

from_domains_file には、mail-muncher が所有、作成、書き込みを行わないファイルを指定します。

match:
  from_domains_file: ~/.local/share/jobsearch/domains.txt
# ~/.local/share/jobsearch/domains.txt
# written by the job-search tracker

acme.com
globex.io          # inline comments are fine
@initech.com       # a leading @ is stripped
MAIL.Umbrella.COM  # case is irrelevant
  • サイクルごとに、初回使用時に1回読み込みます。 プロセスごとに1回でも、メッセージごとに1回でもありません。run は再読み込みし、デーモンの各ティックでも再読み込みします。複数のルールで参照されているファイルは1回だけ読み込まれ、共有されます。
  • ファイルが存在しない、または読み取れない場合でも、処理が致命的なエラーになることはありません。 条件は何にも一致せず、そのサイクルではそのファイルについて警告が1件ログに記録されます。所有するプログラムがまだファイルを作成していない可能性があり、そのために mail-muncher が失敗してはなりません。
  • 柔軟に解析します。 1行に1つのエントリーを記述します。# 以降はコメントとして扱います。空行はスキップし、前後の空白は削除します。先頭の @ と末尾の . は取り除き、すべて小文字に変換します。重複はまとめられます。ドットを含まないエントリーは削除せず、不審な項目としてログに記録して保持します。このファイルは他者が所有しており、誤った推測によってエントリーを黙って破棄してはならないためです。
  • 完全一致またはサブドメイン一致です。 acme.com は acme.com と careers.acme.com に一致しますが、notacme.com には一致しません。

同じ照合ルールがインラインの from_domains: 条件にも適用されます。異なるのは、リストの所有者だけです。

ドメインリストでは表現できない場合

送信元を事前に列挙できないことがあります。ある企業のメールは wagepoint.teamtailor.com、mail.wagepoint.com、 [email protected] から届くかもしれません。ドメインリストでは、 すでに把握しているホストしか指定できません。パターンを指定する場合も、 同じ考え方で使えるのが from_regex_file です。

match:
  from_regex_file: ~/.local/share/jobsearch/companies.txt
# one RE2 pattern per line, unanchored
wagepoint
(?i)^careers@acme\.io$
teamtailor\.com$

ライフサイクルは同じです。サイクルごとに一度だけ読み込み、ファイルがなくても 致命的なエラーにはならず、カーソルは on_degraded_filter に従います。ドメイン形式とは 意図的に異なる点が2つあります。

  • **小文字には変換されません。**正規表現は仕様上、大文字と小文字を区別するためです。 区別しない場合は (?i) を指定してください。
  • **# がコメントの開始とみなされるのは、行頭にある場合だけです。**行の途中にある パターンをそこで切り詰めると、マッチ対象が意図せず変わってしまいます。

失敗の形は正反対であり、それがガードの違いの理由です。ドメインリストの入力ミスは 何にもマッチしません。つまり、何も通知されないことが問題になります。一方、パターンリストの 入力ミスは、すべてにマッチする可能性があります。.* や、意図せず入った空白で、 メールボックス全体が対象になりかねません。そのため、空のパターンや空文字列にマッチする パターンは無条件に拒否されます。コンパイルに失敗したパターンはそのパターンだけ 拒否され、ファイル内のほかの設定は有効なままです。また、読み込んだパターンの数は サイクルごとにログに記録されるため、12個のパターンが1つの包括的なパターンに減った 場合も、ディスク全体を確認して初めて気付くのではなく、実行出力でその数を確認できます。

設定

設定の完全なリファレンス:docs/configuration.md。実行可能なファイル: examples/imap.yml、 examples/minimal.yml、 examples/job-search.yml。

アカウントブロックだけがプロバイダーによって異なります。IMAP:

accounts:
  - name: personal
    provider: imap
    imap:
      host: imap.fastmail.com
      port: 993                  # default
      tls: true                  # default
      username: [email protected]
      password_cmd: pass show mail/fastmail   # stdout is the password
      mailboxes: [INBOX, Archive]             # default [INBOX]
      initial_lookback: 720h                  # default

Gmail:

state_dir: ~/.local/state/mail-muncher

on_message_failure: quarantine   # or: abort
on_degraded_filter: hold         # or: fail, proceed

accounts:
  - name: personal
    provider: gmail
    gmail:
      credentials_file: ~/.config/mail-muncher/credentials.json
      token_file: ~/.config/mail-muncher/token.json
      query: "-in:chats"
      initial_lookback: 2160h

rules:
  - name: job-search
    account: personal
    match:
      any:
        - from_domains_file: ~/.local/share/jobsearch/domains.txt
        - subject_regex: "(?i)your application"
    dest: ~/Mail/job-search
    formats: [eml, markdown]
キー型デフォルト説明
state_dirパス~/.local/state/mail-muncher同期カーソル(アカウントごとに 1 つの JSON ファイル)、サイクルロック、インスタンスロック、隔離ディレクトリを格納します。
on_message_failurequarantine、abortquarantine解析できないメッセージ、またはシンクで処理に失敗したメッセージの扱いを指定します。詳しくは以下を参照してください。
on_degraded_filterhold、fail、proceedholdルールの from_domains_file または from_regex_file を読み込めない場合の扱いを指定します。詳しくは以下を参照してください。
quarantine_dirパス<state_dir>/quarantine隔離したメッセージの保存先です。
accountsリスト—取得するメールボックスです。少なくとも 1 つ必要です。
accounts[].name文字列—必須で、一意である必要があります。状態ファイルの名前になり、rules[].account から参照されます。
accounts[].providerimap、gmail—必須です。デフォルト値はありません。取得に使うバックエンドを指定します。メールボックスへの接続方法 2 種類を参照してください。
accounts[].imapマッピング—プロバイダーが imap の場合に必須で、それ以外では指定できません。
accounts[].imap.host文字列—必須です。imap.fastmail.com、imap.gmail.com、Proton Bridge の場合は 127.0.0.1 を指定します。
accounts[].imap.port整数993993 は暗黙的 TLS(IMAPS)で、デフォルトの tls: true と組み合わせて使います。
accounts[].imap.username文字列—必須です。通常は完全なメールアドレスですが、プロバイダーによってはローカル部のみを指定します。
accounts[].imap.password_cmdシェルコマンド—必須です。/bin/sh -c で実行され、標準出力がパスワードになります。意図的に password キーは用意されていません。シークレットはパスワードマネージャーに保管してください。
accounts[].imap.mailboxes文字列のリスト[INBOX]取得するフォルダーです。それぞれに独自のカーソルがあり、フォルダー名は label の述語値としても使われます。サーバーに存在しないフォルダーは、空のフォルダーではなくエラーになります。
accounts[].imap.tlsブール値true接続時に暗黙的 TLS を使用します。false にすると、パスワードとすべての本文が暗号化されずに送信されます。validate は警告を出します。ループバック上、または stunnel の背後でのみ正当に使用できます。
accounts[].imap.initial_lookbackGo の期間720h各メールボックスの初回同期時、および UIDVALIDITY の変更後に、どこまで遡るかを指定します。正の値である必要があります。
accounts[].gmailマッピング—プロバイダーが gmail の場合に必須で、それ以外では指定できません。
accounts[].gmail.credentials_fileパス—必須です。Google Cloud からダウンロードした OAuth クライアント JSON を指定します。
accounts[].gmail.token_fileパス—必須です。auth が OAuth トークンをキャッシュする場所を指定します。権限は 0600 です。
accounts[].gmail.query文字列なし任意の Gmail 検索式です。初回スキャンのみのコスト最適化に使います。詳しくは以下を参照してください。
accounts[].gmail.initial_lookbackGo の期間720h初回スキャンでどこまで遡るかを指定します。正の値である必要があります。バックフィルを参照してください。
accounts[].gmail.include_spam_trashブール値false迷惑メールとゴミ箱内のメッセージを取得します。Gmail のどちらの同期方法でも同じように適用されます。true の場合、validate は警告を出します。迷惑メールとゴミ箱(Gmail)を参照してください。
rulesリスト—すべてのメッセージに順番に評価され、最初に一致したルールが適用されます。
rules[].name文字列—必須で、一意である必要があります。ログと Markdown のフロントマターに表示されます。
rules[].account文字列すべてのアカウントルールを 1 つのアカウントに限定します。
rules[].matchマッチノード—必須です。フィルターを参照してください。
rules[].destパス—必須です。保存先ディレクトリです。必要に応じて作成されます。
rules[].formatseml、markdown のリスト[eml]書き出す形式です。

つまずきやすい注意点:

  • 不明なキーは重大なエラーになります。 誤記があると無視されず、読み込みに失敗するため、実行前に validate で initial_lookbak を検出できます。
  • すべてのパス値フィールドで ~ と $VAR が展開されます。 マッチツリー内の from_domains_file と from_regex_file の値も対象です。~user 形式には対応していません。未定義の変数はシェルと同様に空文字列へ展開されます。
  • gmail.query は保存対象を絞り込まず、想定より適用範囲が狭いです。 アカウントの初回スキャン時のみGmail に送信され、それ以外では使われません。増分サイクルでも、履歴カーソルの期限切れ後の復旧スキャンでも使われません。ローカルで再適用されることもありません。保存対象を決める唯一の基準はルールです。クエリは広めにするか、省略してください。
  • デフォルトでは迷惑メールとゴミ箱のメールは取得されません(Gmail)。 Gmail の両方の同期経路でこの動作は共通です。全件スキャンでは includeSpamTrash=false を指定し、増分処理では SPAM または TRASH ラベルの付いたメッセージをパイプラインに渡す前に除外します。それでも取得するには gmail.include_spam_trash: true を設定してください。詳しくは迷惑メールとゴミ箱を参照してください。IMAP に同等のキーはありません。mailboxes: に指定したフォルダーだけを取得するため、迷惑メール用フォルダーをリストに含めないだけで対応できます。
  • gmail: ブロックと imap: ブロックは同時に指定できません。 provider: に一致しないほうを設定すると、無視されるのではなく重大なエラーになります。そのため、Gmail アカウントの下に imap: ブロックを置いても、IMAP 経由で取得していると誤認することはありません。
起こりうる2種類の問題に対するポリシー

どちらのキーも state_dir と同じ階層にあります。デフォルトは安全な設定です。どの障害を許容するか決めた場合にのみ変更してください。

on_message_failure — メッセージを解析できない場合、またはルールで指定されたすべての形式の書き込みに失敗した場合の動作です。

値動作
quarantine(デフォルト)生のバイト列を <quarantine_dir>/<account>/<id>.eml に書き込み、失敗の内容を記した .json サイドカーも保存してから、カーソルをそのメッセージの先へ進めます。データは失われず、問題のあるメッセージ1件でパイプラインが停止することもありません。サマリーとマニフェストでは quarantined としてカウントされ、実行は引き続き終了コード 0 で終了します。
abort失敗を返すため、カーソルは進まず、次のサイクルでメッセージが再取得されます。トレードオフは明確です。恒久的に解析できないメッセージがあると、担当者が対処するまでアカウントの処理が止まります。

隔離への書き込み自体に失敗した場合、そのメッセージについては abort と同じ動作になります。カーソルの更新を拒否する問題は復旧できますが、メッセージの喪失は復旧できません。

on_degraded_filter — ルールの from_domains_file または from_regex_file が存在しない、読み取れない、または途中で切り詰められている場合の動作です。このようなファイルは何にも一致しません。ポリシーを指定しない場合、そのサイクルのすべてのメッセージが空のリストに対して評価され、一致しないと判断されて処理されます。

値動作
hold(デフォルト)サイクルを実行し、一致したものをすべて保存します。劣化をエラーレベルでログに記録しますが、進めたカーソルは保存しません。そのため、ファイルが復旧した後に同じメールを再評価できます。マニフェストには degraded と state_held が報告されます。終了コードは 0 です。
fail何も取得する前にサイクルを終了します。何も保存されず、カーソルも進まず、終了コードは 0 以外になります。
proceed読み取り不能なリストを空のリストとして扱い、そのままカーソルを進めます。以前の動作であり、必要なメールのサイレントな喪失を許容する唯一の選択肢です。validate はこの設定について警告します。

hold のもとですでに保存されたファイルは、そのまま残ります。シンクは冪等であるため、再実行時にはスキップされます。

フィルター

詳しいリファレンスと実用例については、docs/filters.mdを参照してください。

match: の値は、コンビネーターまたは述語のいずれか1つだけをキーに持つマッピングです。1つのマッピングにキーが2つあるとコンパイルエラーになり、all: または any: で組み合わせるよう通知されます。正規表現と期間は設定の読み込み時にコンパイルされるため、不正なパターンは午前3時に突然問題になるのではなく、validate の失敗として検出されます。

コンビネーター
キー値一致する条件
allノードのリストすべての子ノードが一致する(子ノードが1つ以上必要)
anyノードのリスト1つ以上の子ノードが一致する(子ノードが1つ以上必要)
not単一のノード子ノードが一致しない
match:
  all:
    - any:
        - from_domains: [acme.com]
        - from_domains_file: ~/.local/share/agent/domains.txt
    - not:
        subject_regex: "(?i)^\\[newsletter\\]"
述語
キー値一致する条件
from_domainsドメインのリストいずれかの From アドレスのドメインが、リスト内のドメインと一致するか、そのサブドメインである
from_domains_fileパス同上。リストは各サイクルで外部所有のファイルから読み込まれます
from_regexRE2 パターンパターンがいずれかの From addr-spec(表示名なし)に一致する
from_regex_fileパス同上。パターンは各サイクルで外部所有のファイルから読み込まれます
to_regexRE2 パターンパターンがいずれかの To または Cc addr-spec に一致する
subject_regexRE2 パターンパターンがデコードされた Subject に一致する
header{name: X-Foo, regex: ...}パターンがそのヘッダーのいずれかの値に一致する
has_attachmenttrue / falseメッセージが実際の添付ファイルを含む(または含まない)
labelラベル名メッセージがそのプロバイダーのラベルを持ち、完全一致で比較される。Gmail では Gmail のラベル、IMAP ではメッセージの取得元のメールボックス名です
older_thanGo の期間メッセージの Date が指定期間より前である
newer_thanGo の期間メッセージの Date が指定期間より後である

それぞれの使用例:

# Mail from a company or any of its subdomains.
- from_domains: [acme.com, globex.io]

# The same list, owned and updated by another program.
- from_domains_file: ~/.local/share/jobsearch/domains.txt

# A specific sender, however they capitalize it.
- from_regex: "(?i)^no-?reply@acme\\.com$"

# Patterns owned and updated by another program, for senders whose host cannot
# be enumerated in advance (wagepoint.teamtailor.com, mail.wagepoint.com).
- from_regex_file: ~/.local/share/jobsearch/companies.txt

# Anything addressed to a plus-alias you hand out to vendors.
- to_regex: "(?i)^me\\+vendors@example\\.com$"

# Application acknowledgements, case-insensitively.
- subject_regex: "(?i)(your application|application received)"

# Everything a mailing list tags for you.
- header: {name: List-Id, regex: "golang-nuts"}

# Only messages that actually carry a file.
- has_attachment: true

# On Gmail: labels exactly as shown in the UI. Nested labels use "Parent/Child";
# system labels are upper case (INBOX, SENT, UNREAD, STARRED).
# On IMAP: the mailbox the message came from, verbatim as the server names it,
# including its hierarchy separator ("Lists/golang", "Lists.golang").
- label: INBOX

# Message Date older than 90 days / newer than a day.
- older_than: 2160h
- newer_than: 24h

知っておくとよい詳細:

  • from_regex と to_regex は表示名ではなく、アドレス本体([email protected])を検査します。表示名を含む生のヘッダーを検査するには、header: {name: From, regex: ...} を使用してください。
  • has_attachment は、Content-Disposition: attachment としてマークされたパートを数えます。cid: で参照されるインライン画像は添付ファイルではありません。
  • label は大文字と小文字を区別し、完全一致で判定されます。label: inbox は INBOX に一致しません。IMAP アカウントの場合、値は imap.mailboxes で指定したメールボックス名です。そのため、メッセージが持てるのは取得元のメールボックス名のみです。
  • older_than / newer_than はメッセージの Date ヘッダーと比較します。ヘッダーがないか解析できない場合は、プロバイダーの内部日付を使用します。有効な日付がないメッセージはどちらにも一致しません。
  • パターンには Go の RE2 を使用します。後方参照と先読み・後読みは使用できません。大文字と小文字を区別しない場合は、先頭に (?i) を付けます。YAML では、二重引用符を使用してバックスラッシュをエスケープする("\\.")か、エスケープが不要な場合は一重引用符を使用してください。
  • has_attachment には true / false を使用してください。YAML 1.2 では yes と no は文字列として扱われるため、mail-muncher はこれらを拒否します。
Spam と Trash(Gmail)

Spam と Trash は、デフォルトでは取得されません。 これらのフォルダーにあるものはルールに渡されず、ディスクにも保存されません。Spam は、最終的にモデルのコンテキストウィンドウに入るパイプラインで、攻撃者が作成した悪意あるテキストが含まれている可能性が最も高い場所です。そのため、デフォルトでは Gmail が振り分けた場所にそのまま残します。

このセクション全体は Gmail プロバイダーについて説明しています。IMAP には対応するキーはありません。必要がないためです。IMAP アカウントは imap.mailboxes に指定したフォルダーだけを取得するので、迷惑メールが届くのは、フォルダー名を指定して要求した場合に限られます。

それでも取得したい場合(スパムとして誤分類された正規のメッセージや、本当に完全なアーカイブを作成する場合)は、アカウントごとにキーを設定します。

accounts:
  - name: personal
    gmail:
      include_spam_trash: true   # validate warns; that is deliberate

2 つの設定はそれぞれ異なる役割を持ち、両方が必要になる場合もあります。

決定する内容
gmail.include_spam_trashこれらのメッセージを取得するかどうか
SPAM / TRASH ラベルに対するルール取得後にどう処理するか

gmail.query はどちらの役割も果たしません。初回スキャンでのみ送信されるため、そこで -in:spam を指定しても、それ以降のサイクルには適用されません。include_spam_trash: true を設定した場合は、ルールで振り分けてください。すべての取得済みメッセージを確認するのは、フィルターエンジンだけです。

rules:
  - name: job-search
    match:
      all:
        - from_domains_file: ~/.local/share/jobsearch/domains.txt
        - not:
            any:
              - label: SPAM
              - label: TRASH
    dest: ~/Mail/job-search

Gmail のシステムラベルは、大文字と小文字が区別され、大文字で表記されます。Spam と Trash をすべてのルールから除外したい場合は、各ルールに not: を指定してください。意図的に、全体に適用される除外設定はありません。保存する内容を決定する唯一の基準はルールです。

ディスク上のレイアウト

mail-muncher が書き出すファイルは、パブリック API です。 このセクションでは概要を説明します。docs/output-format.md が仕様書です。そこには、すべてのフロントマターキー、フロントマターに適切な YAML パーサーが必要な理由、配信ツリーを安全に列挙するためのルールが記載されています。コンシューマーを作成する前に読み、簡潔で正しい例については examples/read_delivered.py を参照してください。

各出力先では、ルールの dest の下に、メッセージの日付(UTC)に従ってファイルを保存します。

~/Mail/job-search/
└── 2026/
    └── 07/
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.eml
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.md
        └── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/
            ├── offer.pdf
            └── R-sum-2026.docx

ベース名はすべての形式で共通なので、同じメッセージの各形式のファイルはまとめて並びます。

<unix-seconds>-<sha256(account + ":" + message-id)[:16]>-<subject-slug>

ダイジェスト部分は 16 桁の 16 進数、つまり 64 ビットです。メールボックスが生成する規模では、2 つのメッセージがこれに衝突することはありません。また、アーカイブを読み取る側はここからメッセージ ID を解析するため、この桁数はレイアウトの一部として扱ってください。

  • タイムスタンプは、ディレクトリを時系列順に並べます。
  • ダイジェストは、冪等性キーです。アカウント名とプロバイダーのメッセージ ID だけに依存するため、パスはメッセージの識別情報から決まります。
  • スラッグは、件名を小文字に変換し、[a-z0-9] 以外の文字をすべて単一の - にまとめ、前後の - を削除して、40 文字に切り詰めたものです。

スラッグには、意図的に設けられた注意点が 2 つあります。

  • ASCII のみです。 ラテン文字以外の文字や絵文字だけで書かれた件名は、no-subject というスラッグになります。ASCII 以外のファイル名は、ファイルシステムの Unicode 正規化(HFS+ は NFD で保存)を受けるため、書き込まれた名前と次のサイクルで存在確認に使われる名前が異なる場合があります。この存在確認が冪等性の仕組みそのものです。ダイジェストによって、こうしたメッセージも引き続き区別されます。
  • 見た目だけのものです。 識別情報を持つのはダイジェストだけです。件名が同じ 2 つのメッセージが衝突することはありません。
ファイルの書き込み方法

メッセージファイルは、保存先ディレクトリ内の一時ファイルに書き込まれ、fsync された後、link(2) でハードリンクとして配置されます。これにより、次の 3 つの特性が得られます。

  • 不完全なファイルが公開されることはありません。 名前が作成される前に、一時ファイルが完成します。
  • 既存のファイルが上書きされることはありません。 rename(2) と異なり、link(2) は既存のファイルを上書きせず、EEXIST で失敗します。この失敗が冪等性チェックそのものです。カーネルが名前を確保する瞬間に、その名前が空いているかどうかを判定するため、別の書き込み処理がファイルを挿入し、気付かれないまま置き換えられる隙はありません。「すでに存在する」場合は skipped として報告されます。
  • シンボリックリンクはたどらず、拒否されます。 メッセージの最終パスにあるシンボリックリンク、または dest の下にある <YYYY> や <MM> ディレクトリの代わりとなっているシンボリックリンクはエラーとなり、そのメッセージはカウントされ、ログに記録されます。このディレクトリ構成に正当にリンクが含まれることはないため、リンクがある場合は何者かが別の目的で配置したことを意味します。ルール自身の dest: は例外です。別のボリュームを指すのは通常の使い方です。

ハードリンクに対応していないファイルシステム(FAT、一部のネットワークマウント)では、代わりにその場で O_CREAT|O_EXCL を使って書き込みます。部分ファイルが作られないという保証は失われますが、依然としてアトミックに上書きを防ぎ、シンボリックリンクも防止します。

添付ファイルだけは例外で、一時ファイルに書き込んでから rename(2) で配置します。添付ファイル名は冪等性を示す目印ではなく、上位の .md ファイルがその役割を果たすためです。

ディレクトリは 0700、ファイルは 0600 の権限で作成されます。 アーカイブされたメールは個人的なやり取りやデコード済みの添付ファイルであるため、同期カーソルや OAuth トークンと同じ扱いになります。ここにあるものは、ほかのローカルユーザーから読み取れません。自分のユーザーとして実行するツールには影響しません。

.eml ファイル

model.Message.Raw を、プロバイダーから受信したとおりに、バイト単位でそのまま保存します。再エンコード、再ラップ、正規化は一切行わないため、どのメールツールでも元の状態に戻せるほか、DKIM 署名との照合も引き続き可能です。これは忠実性を保つためのコピーです。

.md ファイル

YAML フロントマター、本文、その後に添付ファイルへのリンクが続きます。

  • 本文の選択:text/plain パートがあればそれを使用し、なければ text/html パートを Markdown に変換して使用します。どちらもなければ、文字列 *(no body)* を使用します。改行コードは LF に統一され、各行末の空白は削除され、先頭と末尾の空行は取り除かれます。
  • フロントマターには、常に subject、from、from_address、from_addresses、to、to_addresses、date、message_id、thread_id、thread_id_source、account、rule が含まれます。cc、cc_addresses、in_reply_to、labels、attachments は空の場合に省略されます。アドレスは必ず *_address / *_addresses フィールドから解析してください。from / to / cc から解析しないでください。これらは表示用文字列であり、送信者が指定した表示名に <、>、, が含まれると曖昧になるためです。機械可読フィールドにはアドレス仕様のみが格納されるため、この方法で偽装されることはありません。文字列の書式設定ではなく YAML エンコーダーを使って生成されるため、引用符やコロンだらけの件名でも解析が壊れることはありません。そのため、読み取るには実際の YAML パーサーが必要です。絵文字を含む件名は \U0001F389 エスケープ付きの二重引用符で囲まれ、新しい行を含む件名は |- ブロックスカラーで出力されます。key: value で分割すると、どちらも正しく処理できません。docs/output-format.md を参照してください。
  • スレッド化に使うフィールドは 4 つではなく 3 つです。thread_id は結合キーであり、空になることはありません。プロバイダーに会話 ID があればそれを使用し、なければメッセージの References チェーンから生成されます。特別な場合分けをせず、このフィールドでディレクトリをグループ化できます。thread_id_source は、そのグループ化をどの程度信頼できるかを示します。値は provider、references、in_reply_to、self のいずれかです。復元は最善努力であり、メールソフトがチェーンを壊すとスレッドが分割されるためです。in_reply_to は親を示します。References チェーン全体は意図的に含めていません。長さに上限がなく、ファイルと同じ場所にある .eml にそのまま記録されているためです。
  • 添付ファイルは .md と同じ場所にある <basename>.attachments/ に書き込まれます。ファイル名はサニタイズされ(ディレクトリ部分やパストラバーサルを除去)、拡張子が .md または .eml の場合は .md.attachment / .eml.attachment に置き換えられ、ファイル名の重複は name-2.pdf、name-3.pdf のように解消されます。.md より先に書き込まれるため、ドキュメントが存在しないファイルへリンクすることはありません。添付ファイルの内容は送信者が決めるものです。以下の列挙ルールを参照してください。
  • インラインの cid: 画像は解決されません。 コンテンツ ID で画像を埋め込む HTML 本文は、![alt](cid:...) としてレンダリングされます。これは添付ファイルディレクトリ内のパスではなく、解決されないリンクです。画像データが必要な場合は、.eml にあります。これは既知の制限であり、バグではありません。
  • .md は忠実性を保証する形式ではありません。バイト単位で完全に正確である必要があるものは、.eml から読み取ってください。
配信ツリーの列挙

配信されたメッセージの正規の集合は、厳密に <dest>/<YYYY>/<MM>/*.md(または *.eml)です。2 階層までで、再帰的な グロブは使いません。

find "$dest" -mindepth 3 -maxdepth 3 -type f -name '*.md'   # correct
find "$dest" -name '*.md'                                    # WRONG

再帰的なグロブは <basename>.attachments/ にも入り込みます。そこにあるファイルは、 メールの送信者が提供したものです。ルールに一致するよう仕向けられる攻撃者は、 偽造されたフロントマターを含むファイルを添付し、不注意な利用側に、任意の from:、 subject:、本文を持つ配信済みメッセージとして読み込ませることができます。 .attachments/ ディレクトリ内にあるものはすべて送信者が制御するため、 メッセージとして解析してはいけません。

利用側で mail-muncher 自体を実行する場合は、run --json がさらに適しています。 このサイクルで書き込まれたパスだけを列挙するため、次のようなツリー内のものと 混同されることはありません。 dest=~/Mail/job-search # ルールの dest

メッセージ本文は攻撃者が制御するテキストです。フィルタリング済みでも、審査済みとは限りません。本文の内容はデータとして扱い、指示として扱わないでください。また、mail-muncher 経由で届いたというだけで権限を与えてはいけません。

完全な仕様については docs/output-format.md を、動作するリーダーの例については examples/read_delivered.py をご覧ください。

コマンド

mail-muncher [command]

  init        Write a starter config and print the next command to run
  run         Run one fetch/filter/store cycle
  daemon      Run fetch/filter/store cycles repeatedly on an interval
  mcp         Serve the stored mail archive to agents over MCP (stdio)
  auth        Authenticate interactively against a mail provider (Gmail only)
  validate    Parse the config, resolve referenced files, and report problems
  completion  Generate the autocompletion script for the specified shell

設定ファイルが必要なコマンドが設定ファイルを見つけられない場合、open: no such file エラーの代わりに、クイックスタートのステップ 0に記載されたセットアップ手順を表示します。確認したパス、次に実行するコマンド、各プロバイダーにかかる費用が示されます。

すべてのサブコマンドで使用できる共通フラグ:

フラグデフォルト説明
--config~/.config/mail-muncher/config.yml設定ファイルのパスです。
--log-levelinfodebug、info、warn、または error です。ログは stderr に log/slog 形式のテキストで出力されます。
-v、--version—バージョンを表示します。

コマンドごとのフラグ:

コマンドフラグデフォルト説明
init--provider—imap または gmail です。省略すると入力を求められます。2 つの方法で費用が異なり、正直なデフォルト値はないため、--yes を指定する場合は必須です。
init--accountpersonal設定ファイルに作成するアカウント名です。
init--dest~/Mail/mail-muncher一致したメールを書き込む先のスタータールールの保存先です。
init--host—IMAP サーバーのホスト名です。例:imap.fastmail.com。省略すると入力を求められます。IMAP を使用する場合、--yes を指定するなら必須です。
init--username—IMAP のユーザー名です。通常はメールアドレス全体を指定します。省略すると入力を求められます。IMAP を使用する場合、--yes を指定するなら必須です。
init--password-cmdプラットフォームのデフォルトアプリパスワードを標準出力に表示するシェルコマンドです。macOS では Keychain、Linux では secret-tool、その他では pass がデフォルトです。
init--yesfalseプロンプトを表示せず、正直なデフォルト値がある回答にはその値を使用します。--provider は引き続き必須であり、IMAP の場合は --host と --username も必須です。
init--forcefalse既存の設定ファイルを上書きします。これを指定しない場合、init は既存のルールや認証情報のパスを誤って消さないよう、上書きを拒否して終了コード 1 で終了します。
run--dry-runfalse取得して評価し、書き込まれる内容を報告しますが、何も書き込まず、状態も保存しません。
run--jsonfalseアカウントごとに 1 つの JSON オブジェクトを、機械可読なマニフェストとして標準出力に書き込みます。
daemon--interval5mサイクル間の時間です。最小値は 30s です。各待機時間には最大 ±10% のジッターが加わります。
daemon--dry-runfalse各実行時に run --dry-run と同じ動作をします。
daemon--jsonfalse各サイクルの後にマニフェストを標準出力に書き込みます。改行区切りの JSON で、ティックごとにアカウントあたり 1 つのオブジェクトを出力します。
auth--account—認証するアカウントを指定します。設定に複数のアカウントがある場合は必須です。Gmail アカウント専用です。IMAP アカウントでは承認するものがないため、auth は実行を拒否します。

mcp に固有のフラグはありません。設定はすべて --config で行います。 init は --config から読み込むのではなく、そこに書き込みます。また、必要に応じてディレクトリ(0700)とファイル(0600)を作成します。

--log-level debug を指定すると、すべてのメッセージについてルールの判定がログに記録されます。メッセージ ID、件名、そして一致したルール名または no match が記録されるため、ルールが適用されない理由を調べるのに最も手早い方法です。debug では、パスとともに already stored によるスキップも記録されます。ログはすべて stderr に出力されるため、標準出力への --json の出力に影響しません。

30s 未満の間隔は、何も開始する前に拒否されます。

error: --interval 10s is below the 30s minimum
終了コード
コード意味
0成功です。メッセージ単位のエラー(解析できないメッセージ、失敗した出力先への書き込み)が発生したサイクルでも、終了コードは 0 です。これらはサマリーに集計され、エラーとして扱われません。
1設定または検証エラーです。ファイルを解析できない、ルールをコンパイルできない、必須キーがない、または設定ファイルが存在しない場合です。データは取得されていません。また、設定がすでに存在し、--force が指定されていない場合に init が返すコードです。
2プロバイダーまたは認証の失敗です。一度も書き込まれていない Gmail トークン、Google に拒否されたトークン、失敗した IMAP の password_cmd、サーバーに拒否されたログイン、再試行後も到達できないサーバーなどが該当します。
3別のインスタンスがサイクルロックを保持しています。終了コード 3 は、重複して起動した cron が「前回の処理がまだ実行中」と報告するためのものです。

隔離されたメッセージによって終了ステータスが変わることはありません。これは処理済みのメッセージ単位の失敗であり、quarantined として集計され、マニフェストに報告されます。終了コードではなくカウンターを監視してください。

daemon が終了コード 2 で終了することはありません。サイクルの失敗は連続失敗数とともにログに記録され、次のティックで再試行されます。最初のトークン期限切れで終了するデーモンでは、最悪のタイミングで人手による対応が必要になるためです。ステータスは、0(SIGINT または SIGTERM によって停止。実行中のサイクルを完了させ、状態を保存した後)、1(--interval が最小値未満の場合を含む、設定または検証エラー)、3(起動時に別のインスタンスがインスタンスロックまたはサイクルロックをすでに保持している場合)です。後続のティックでロックが保持されている場合(cron の run がデーモンのティックと重複した場合)は、ログに記録してスキップし、致命的なエラーにはなりません。

実行サマリー

各サイクルでは、アカウントごとに 1 行のサマリーがログに記録され、出力されます。

personal: fetched=128 matched=6 stored=6 skipped=0 parse_errors=0 sink_errors=0 quarantined=0 duration=4.1s
フィールド集計単位意味
fetchedメッセージこのサイクルでプロバイダーが配信したメッセージです。
matchedメッセージいずれかのルールに該当したメッセージです。
storedレンダリング実際に書き込まれたレンダリングです。
skippedレンダリング保存先にすでに存在していたため、書き込まれなかったレンダリングです。
parse_errorsメッセージ解析できなかったメッセージです。ログに記録され、スキップされます。
sink_errorsレンダリング書き込みの失敗です。ログに記録されて集計され、サイクルは続行します。
quarantinedメッセージ配信できなかったため、隔離ディレクトリに退避されたメッセージです。
vanishedメッセージプロバイダーが一覧に含めたものの、その後存在しないことが判明したメッセージです。一覧の取得からダウンロードまでの間に削除されました。スキップされ、カーソルはその先まで進みます。0 以外の場合にのみ出力されます。

フィールドは fetched= から duration= まで連続しているため、サイクル中の異常な状況はアカウントラベルに示されます。

personal (dry-run): fetched=42 ...
personal (degraded, state held): fetched=42 ...
personal (stopped): fetched=12 ...

vanished=N は条件付きで追加される唯一のカウンターです。サイクルの途中でメッセージが消失した場合に限り、quarantined= の後に表示され、それ以外では省略されます。そのため、ほかのすべての実行では公開されるフィールド一覧は変わりません。--json では summary.vanished となり、同様に 0 の場合は省略されます。これを監視する場合は .summary.vanished // 0 として解析してください。

定常状態の cron 実行では fetched=0 になります。同じ時間枠で再実行すると matched=N stored=0 skipped=N となり、冪等性が機能していることを示します。

JSON マニフェスト

run --json と daemon --json では、この行の代わりに、アカウントごとに機械可読なオブジェクト(stored[]、skipped[]、quarantined[]、各カウンター、カーソルが進んだかどうか)が出力されます。stderr にはすべてのログ行が出力されるため、run --json 2>/dev/null は純粋な JSON を出力し、daemon --json は NDJSON ストリームを出力します。

mail-muncher run --json 2>/dev/null | jq -r '.stored[] | "\(.rule)\t\(.path)"'

フィールドごとの完全な仕様については、docs/manifest.md を参照してください。

状態とロック

~/.local/state/mail-muncher/
├── personal.json                # one per account, mode 0600
├── mail-muncher.lock            # cycle lock, shared by run, daemon and mcp sync
├── instance/
│   └── mail-muncher.lock        # daemon lifetime lock — one daemon per state dir
└── quarantine/
    └── personal/
        ├── 18f2a1b2c3.eml       # raw bytes of an undeliverable message
        └── 18f2a1b2c3.json      # why it is here

各アカウントの状態ファイルは JSON です。その中のカーソルはプロバイダー固有のため、形式が異なります。Gmail の場合:

{
  "history_id": 918273,
  "last_sync_time": "2026-07-28T09:15:00Z",
  "seen_ids": ["18f2a...", "18f2b..."]
}
  • history_id は Gmail の増分カーソルです。これが設定されている場合、1 回のサイクルでは Gmail に前回以降の変更だけを問い合わせます。Gmail が履歴を保持する期間はおよそ 1 週間です。カーソルの有効期限が切れると、API は 404 を返し、mail-muncher は警告を記録してカーソルをクリアし、同じサイクル内で全件スキャンに切り替えます。
  • last_sync_time は、全件スキャンの after: 条件の範囲を定めます。復旧スキャンでは、保存されたウォーターマークより 24 時間前までさかのぼるため、前回のサイクルで一覧取得中に届いたメールを見落としません。すでにディスク上にあるものは書き込み時にスキップされるため、重複する範囲があっても問題ありません。
  • seen_ids は、配信済みメッセージ ID の直近 2000 件を保持する FIFO セットです。シンクの冪等なファイル名と合わせて、二重の対策になっています。

IMAP では、メールボックスごとに独立して追跡し、extra 内の UIDVALIDITY/UID の組として保存します。

{
  "last_sync_time": "2026-07-28T09:15:00Z",
  "extra": {
    "imap.INBOX.uidvalidity": "1650000000",
    "imap.INBOX.last_uid": "48213"
  },
  "seen_ids": ["personal:INBOX:1650000000:48213"]
}

保存された UIDVALIDITY がサーバーの値と一致しているサイクルでは、UID FETCH 48214:* を要求し、前回以降に届いたものだけを取得します。値が変わっていることを検出したサイクルでは、保存された UID を破棄し、initial_lookback から再同期します。UIDVALIDITY の変更は、mail-muncher が記憶しているすべての UID が、別のメッセージを指すようになったことをプロトコルが通知しているためです。この再同期では、対象期間のメールを新しいファイル名で再アーカイブします。この経路でのメッセージの識別子は <account>:<mailbox>:<uidvalidity>:<uid> だからです。これは意図的なトレードオフです。重複したメールは削除できますが、気付かないままスキップされたメールは復元できません。詳細については、docs/configuration.md を参照してください。

状態ディレクトリは 0700、状態ファイルは 0600 で作成されます。どのアカウントが存在するか、どのメッセージ ID が確認済みかは、公開情報ではないためです。

アカウントの状態ファイルを削除すると、initial_lookback の範囲で全件再スキャンが実行されます。 シンクはディスク上にすでにあるものをすべてスキップするため安全であり、破損したカーソルから復旧するためにサポートされている方法です。

2 つのロック
  • サイクルロック <state_dir>/mail-muncher.lock は flock ベースで、run、各デーモンのティック、MCP の sync ツールによって、各サイクルの実行中に保持されます。これにより、同じカーソルを使う cron の呼び出しが、実行中のデーモンと競合するのを防ぎます。ロックが取得済みの場合は「今は実行しない」という意味であり、「故障」ではありません。run は 3 で終了し、デーモンのティックはログを記録してスキップします。

  • インスタンスロック <state_dir>/instance/mail-muncher.lock は、デーモンのプロセスが存続する間、保持されます。サイクルロックではこの役割を果たせません。サイクルロックはティックの間に解放されるため、最初のデーモンがスリープしている間に 2 つ目のデーモンが起動すると、ロックを通過して、同じカーソルに対して最初のデーモンと並行してポーリングを続け、API トラフィックが 2 倍になります。2 つ目のデーモンは直ちに 3 で終了します。

    another mail-muncher daemon is already running against this state directory; not starting
    

プロセスがクラッシュした場合、どちらのロックも OS によって解放されます。

隔離

既定の on_message_failure: quarantine では、配信できなかったメッセージは <quarantine_dir>/<account>/<id>.eml に生のバイト列のまま書き込まれ、その隣に .json サイドカーファイルが作成されます。サイドカーファイルには、ルール、失敗した段階(parse または sink)、エラー、時刻が記録されます。ディレクトリの調査に実行時のログが不要となるよう、サイドカーファイルは意図的に単独で完結する形式になっています。

自動的に再配信されることはありません。原因を修正し、.eml を手動で再投入してください。他の状態ディレクトリと同様に、ディレクトリは 0700、ファイルは 0600 です。隔離されたメッセージは、宛先ツリーの外に置かれたメール全体だからです。

スケジュール設定

cron — 10 分ごとに 1 サイクル実行します。前回の実行がまだ終わっていない場合、run は終了コード 3 で終了するため、重複実行しても問題ありません。

*/10 * * * * /usr/local/bin/mail-muncher run --config /home/you/.config/mail-muncher/config.yml >> /home/you/.local/state/mail-muncher/cron.log 2>&1

cron の環境はシェルの環境とは異なり、設定ファイル内の ~ の展開は $HOME が設定されているかどうかに依存するため、バイナリと設定ファイルには絶対パスを使用してください。

cron では設定されない環境変数を含む、より詳しいサンプルは contrib/crontab.sample にあります。

launchd(macOS) — デーモンを launchd の下で実行すると、ログアウト後も動作し、失敗時には再起動します。編集してすぐ使える plist が contrib/launchd/ にあります。コピーして、コピー内の 4 つの絶対パスを編集してください。launchd は ~ も $HOME も展開しません。その後、読み込みます。

cp contrib/launchd/com.craigjmidwinter.mail-muncher.plist ~/Library/LaunchAgents/
# edit the four paths in the copy, then:
launchctl load ~/Library/LaunchAgents/com.craigjmidwinter.mail-muncher.plist

systemd — 現時点ではユニットは同梱されていません。mail-muncher daemon --interval 5m を Type=simple サービスとして実行するか、mail-muncher run とタイマーを組み合わせてください。

バックフィル:初回実行

広い範囲を対象にした初回実行は想定された使い方であり、乱用ではありません。デフォルトの initial_lookback は 720h(30 日)です。これは、初日に最も必要とされること、つまり収集対象の全履歴を一度にディスクへ保存することが、デフォルトでは行われないことを意味します。

初回実行では、対象範囲を広く設定してください。このキーはどちらのプロバイダーにもあり、アカウントで使用するブロックの下に設定します。

停止するには、同じパスを指定して launchctl unload を実行します。完全に削除するには、そのファイルも削除してください。アンインストールを参照してください。

accounts:
  - name: personal
    gmail:
      credentials_file: ~/.config/mail-muncher/credentials.json
      token_file: ~/.config/mail-muncher/token.json
      initial_lookback: 13140h   # ~18 months — first run only
accounts:
  - name: personal
    provider: imap
    imap:
      host: imap.fastmail.com
      username: [email protected]
      password_cmd: pass show mail/fastmail
      initial_lookback: 13140h   # ~18 months — first run only, per mailbox

次に一度実行し、その後は値を元に戻してください。

      initial_lookback: 720h

値を元に戻しても安全です。initial_lookback が範囲を制限するのは、Gmail のアカウントごと、IMAP の各メールボックスごとの初回スキャンのみです。それ以降のサイクルは保存済みのカーソルから再開し、このキーを完全に無視します。この設定が再び適用されるのは、アカウントの状態ファイルを削除した場合、または IMAP サーバーが UIDVALIDITY を変更した場合のみです。

実行時の動作は次のとおりです。

  • 大規模な全件スキャンを 1 回実行します。レート制限とリトライが適用されるため、時間がかかる場合があります。Gmail ではダウンロードを一度に 4 件ずつ実行し、ページあたりのメッセージ数は 500 件です。どちらも設定できません。IMAP では、メールボックスごとに SINCE UID 検索を実行します。

  • 確定する前に確認してください。 mail-muncher run --dry-run は実際の実行とまったく同じように取得と評価を行い、書き込み先のディレクトリツリーには触れずに、書き込む予定のすべてのパスを報告します。件数を数えるには --json を追加します。

    mail-muncher run --dry-run --json 2>/dev/null | jq '.summary'
    
  • その次の実行では、すべてが skipped と報告されるはずです。これにより、バックフィルが実際に完了したことを確認できます。

Go の期間指定には日や年の単位がありません。時間に換算してください。18 か月は 13140h、1 年は 8760h、90 日は 2160h です。

既読項目の上限 2000 件は、バックフィルを制限しません。その理由については docs/architecture.md を参照してください。実際の冪等性キーは決定的なファイル名と既存ファイルのスキップであり、既読項目のセットは念のための仕組みにすぎません。

ステータスと対象範囲

1.0 以前のリリースです。現在のリリースは v0.4.0 で、各バージョンの変更点は CHANGELOG.md に記載されています。make を使わずにビルドした場合、バージョンは dev と報告されます。設定スキーマは十分に安定しており、これに基づいて実装できますが、1.0 までは変更される可能性があるものとして扱ってください。

領域ステータス
IMAP プロバイダー(password_cmd、メールボックスごとの UID カーソル、EXAMINE + BODY.PEEK[])実装済み
Gmail プロバイダー(OAuth、全件スキャン、増分履歴同期、RAW ダウンロード)実装済み
init — どちらのプロバイダーでも、対話形式またはスクリプト形式で検証済みの設定を書き込み実装済み
未設定または設定が不完全なすべてのコマンドでのセットアップガイダンス実装済み
未設定時にプロトコルに準拠して動作する mcp実装済み
設定の読み込みと検証実装済み
フィルターエンジン(上記のすべてのコンビネーターと述語)実装済み
.eml および Markdown シンク実装済み
run、daemon、サイクルロック、インスタンスロック実装済み
--json 実行マニフェスト実装済み
mcp — stdio MCP サーバー、5 つのツール実装済み
隔離と 2 つの失敗ポリシー実装済み
意図的に対象外とするもの
  • メールボックスへの書き込み。 読み取り専用は設計上の制約であり、特定のフェーズに限ったものではありません。 ラベル付け、削除、送信、下書き作成はできず、IMAPではメッセージを既読にすることさえできません。
  • メールクライアントとしての機能。 UIもインデックスもありません。search_messages は保存されたファイルを対象に大文字と小文字を区別せず部分一致で検索する機能であり、検索エンジンではありません。 ステミング、ランキング、クエリ言語には対応していません。スレッドは各メッセージに付与された ID でグループ化され、会話モデルとして再構成されることはありません。
  • ネットワーク API。 ソケットをリッスンするものはありません。mcp は、サブプロセスとして起動したクライアントと stdio プロトコルで通信します。ポートも HTTP エンドポイントもネットワークインターフェースもありません。何かがバインドするのは、Gmail の OAuth リダイレクト時に auth が数秒間だけ開くループバックポートだけです。
  • OAuth クライアントの同梱。 gmail.readonly は Google の制限付きスコープであるため、Gmail を利用するには必ず独自のクライアントを登録する必要があります。これこそ、手軽に利用できる方法として IMAP が用意されている理由です。
既知の制限事項
  • インラインの cid: 画像は、Markdown 出力では未解決のリンクとして残ります。
  • 件名のスラッグは ASCII のみです。非ラテン文字の件名は no-subject として保存されます。
  • 述語がメッセージ本文を参照することはありません。一致判定の対象はヘッダー、ラベル、日付です。

Gmail:

  • Testing モードの同意画面では Gmail のトークンが 7 日ごとに期限切れになるため、mail-muncher auth を毎週実行する必要があります。これは Google のポリシーであり、設定では変更できません。
  • gmail.query が適用されるのは、アカウントの初回スキャンのみです。増分サイクルや、カーソルの期限切れ後の復旧スキャンには適用されません。
  • gmail.include_spam_trash: true を指定しない限り、迷惑メールとゴミ箱は取得されません。取得した場合は、SPAM ラベルと TRASH ラベルに対するルールによって処理が決まります。
  • Gmail のダウンロード並列数(4)とページサイズ(500)は設定できません。

IMAP:

  • アプリパスワードは、メールの認証情報全体にアクセスできるものです。読み取り専用はプロバイダー側ではなく mail-muncher のコードによって適用されます。詳しくは比較を参照してください。
  • サーバーが UIDVALIDITY を変更すると、initial_lookback の期間が新しいファイル名で再アーカイブされます。これは意図的な動作です。重複したメールは復元できますが、取りこぼしたメールは復元できません。
  • IMAP にはサーバー側の会話 ID がないため、thread_id は常に References/In-Reply-To のチェーンから合成され、thread_id_source が provider になることはありません。
  • サーバーに存在しないメールボックスは空のフォルダーではなくエラーとして扱われます。そのため、mailboxes: の入力ミスは、メールがないかのように見過ごされるのではなく、初回実行時にエラーになります。
  • 取得されるのは mailboxes: に記載されたフォルダーのみです。「すべて」を対象にするオプションも、サーバー側検索もありません。

ドキュメント

https://craigjmidwinter.github.io/mail-muncher/ で閲覧できるほか、このリポジトリにもあります。

  • docs/configuration.md — 2 つのプロバイダーそれぞれのすべての設定キー、検証ルール、エラー発生時の動作を記載しています。 accounts[].imap セクションは IMAP のリファレンスです。アプリパスワード以外にセットアップがないため、個別のセットアップページはありません。
  • docs/gmail-setup.md — Gmail の手順のみを扱っています。Google Cloud の手順、7 日間の有効期限、修正方法を含むすべての OAuth エラーメッセージを記載しています。 IMAP アカウントには適用されません。
  • docs/filters.md — マッチツリー言語の全仕様と、実際のルールを使ったクックブックです。
  • docs/output-format.md — ディスク上の契約仕様です。レイアウト、ファイル名、すべてのフロントマターキー、配信ツリーを列挙する際のセキュリティルールを記載しています。利用側の実装を始める前にお読みください。
  • docs/manifest.md — --json マニフェストの契約仕様を、フィールドごとに記載しています。
  • docs/mcp.md — MCP サーバーについて、クライアントの接続方法と、各ツールの引数および戻り値の形式を記載しています。
  • docs/architecture.md — パイプライン、その境界、変更を加えるべき場所を記載しています。
  • CONTRIBUTING.md — ビルド、テスト、コードベースで定めている規約を記載しています。

実行可能な設定ファイル:examples/imap.yml(IMAP の手順を詳しくコメント)、examples/minimal.yml(Gmail 用の実用最小限の設定)、examples/job-search.yml(外部管理のフィルターファイルの実例)、および examples/read_delivered.py(正しい利用側の実装例)。3 つの設定ファイルはすべて mail-muncher validate --config <file> に合格します。

謝辞

ワードマークには Jason Kottke による Silkscreen を使用しており、SIL Open Font License 1.1 に基づいています。ブランドアセット、カラーパレット、使用ルールについては branding/BRAND.md をご覧ください。

ライセンス

MIT。LICENSE をご覧ください。

コメント

MCP は初めてですか?

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

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