mail-muncher
E-mail estritamente somente leitura para agentes: regras de filtro ordenadas arquivam mensagens correspondentes de qualquer caixa de correio IMAP ou da API do Gmail para o disco como .eml mais markdown, servidas de volta via MCP.
Documentação
mail-muncher
Dê a um programa sua própria caixa de correio somente leitura, filtrada exatamente para o e-mail que ele pediu, entregue como arquivos no disco.
mail-muncher puxa mensagens de um provedor de e-mail, avalia cada uma contra
regras ordenadas e grava as correspondências em um diretório — fiel ao byte .eml,
e opcionalmente uma renderização em markdown com os cabeçalhos como frontmatter YAML, o
corpo como texto e anexos extraídos ao lado. Uma regra pode obter sua entrada de filtro
de um arquivo de texto simples que algum outro programa possui, que o mail-muncher
relê no início de cada ciclo. Esse outro programa muda uma linha nesse
arquivo, e o próximo ciclo entrega e-mails diferentes — sem editar configuração, sem
reiniciar, sem reimplantar.
Ele lê de qualquer caixa de correio IMAP — Gmail, Fastmail, iCloud, Proton Bridge, uma conta de trabalho, seu próprio servidor — ou da API do Gmail com um escopo OAuth somente leitura. Ele executa uma única vez para cron, ou como um daemon de polling, ou como um servidor MCP stdio que um agente pode consultar diretamente. Cada modo emite o mesmo manifesto legível por máquina do que fez, e nenhum modo jamais grava na sua caixa de correio.
Duas maneiras de conectar uma caixa de correio
Escolha uma antes de instalar qualquer coisa. Ambas são suportadas, e tudo a jusante — regras, formatos, nomes de arquivo, o layout do arquivo, as ferramentas MCP — é idêntico de qualquer forma.
provider: imap | provider: gmail | |
|---|---|---|
| Tempo de configuração | ~2 min | ~10 min no Google Cloud Console |
| O que você registra | nada | seu próprio projeto Google Cloud e cliente OAuth de aplicativo de desktop |
| Credencial | uma senha de aplicativo da página de configurações do seu provedor | um token OAuth, escopo gmail.readonly |
| Quão ampla é essa credencial | uma credencial de e-mail completa. Uma senha de aplicativo pode enviar e excluir | somente leitura, e nada mais |
| Quem impõe somente leitura | o próprio código do mail-muncher | |
| Expiração | nenhuma | a cada 7 dias em uma tela de consentimento em modo Teste; mail-muncher auth precisa ser reexecutado semanalmente |
| Onde o segredo vive | onde seu gerenciador de senhas já o mantém: password_cmd é executado e seu stdout é a senha. Deliberadamente não há chave password | token.json, modo 0600, escrito por mail-muncher auth |
| Quais caixas de correio | as pastas que você lista em mailboxes:; [INBOX] por padrão | toda a conta Gmail, menos Spam e Lixeira, a menos que você as peça |
| Funciona com | Gmail, Fastmail, iCloud, Proton Bridge, contas de trabalho, auto-hospedado | Somente Gmail |
| Etapas extras | nenhuma. Não há comando auth neste caminho | mail-muncher auth, após docs/gmail-setup.md |
Os números ~2 min / ~10 min / 7 dias acima são os mesmos que mail-muncher init
e as orientações de execução não configurada imprimem, porque são os números que
decidem isso.
A garantia de somente leitura é real em ambos os caminhos, mas não é a mesma garantia, e achatar os dois seria desonesto.
- Gmail: imposto pelo Google. O único escopo solicitado é
gmail.readonly. O token que volta é incapaz de enviar, excluir, rotular ou modificar — não porque o mail-muncher se recusa, mas porque o Google recusará a chamada. Um bug neste programa não pode alcançar sua caixa de correio. - IMAP: imposto pelo mail-muncher. IMAP não tem credencial somente leitura para pedir.
Uma senha de aplicativo é uma credencial de e-mail completa; o protocolo permitirá de bom grado
que seu titular exclua uma pasta. O que o mail-muncher faz em vez disso é recusar: cada
pasta é aberta com
EXAMINEe nuncaSELECT, cada corpo é buscado comBODY.PEEK[]e nuncaBODY[](então o e-mail nunca é marcado como lido), e não há caminho de código em nenhum lugar do provedor que emitaSTORE,APPENDouEXPUNGE. Ambos os cintos são usados porque um servidor não é obrigado a proteger um cliente de si mesmo. Essa é uma garantia forte e auditável — é apenas a garantia deste programa, não a do seu provedor de e-mail.
Se você não tem nenhuma razão específica para querer a API do Gmail, comece com IMAP. Funciona também em uma conta Gmail, e é o caminho que o quickstart segue.
O problema
Um processo automatizado precisa de algum e-mail. Um rastreador de busca de emprego quer respostas de empresas para as quais você se candidatou. Um bot de suporte quer mensagens do domínio de um fornecedor. Um agente de pesquisa quer cada newsletter de três editoras, como texto que ele possa realmente ler.
As respostas usuais são todas ruins. Entregue ao processo suas credenciais de caixa de entrada e ele pode ler (e enviar, e excluir) tudo. Dê a ele uma integração de API de e-mail e você agora mantém um fluxo OAuth, um cursor de sincronização, análise MIME e uma história de deduplicação dentro de cada processo que quer e-mail. Ou codifique o filtro em um arquivo de configuração, e cada mudança no que ele quer é uma edição de configuração e um reimplantar.
mail-muncher divide isso ao meio. Ele possui as credenciais, a sincronização incremental, a análise e a deduplicação. O programa consumidor possui um arquivo de texto listando o que quer e um diretório do qual lê resultados — e, se preferir perguntar em vez de observar, um punhado de ferramentas MCP sobre esse mesmo diretório.
O fluxo de trabalho do agente
Existem duas formas suportadas, e elas se compõem. Escolha com base em se seu agente executa em um loop próprio ou espera ser perguntado.
- Entrega de arquivo — mail-muncher executa em um agendamento e grava arquivos; o agente lê o diretório. Nada chama nada. Esta é a forma abaixo.
- Chamada de ferramenta — o agente fala com
mail-muncher mcpvia MCP e faz perguntas diretamente: do que estou assinado, o que chegou, o que este tópico diz, busque agora. Veja Forma 2: chamada de ferramenta.
Ambos leem o mesmo arquivo, e executar ambos ao mesmo tempo é normal: um daemon preenche o diretório enquanto o servidor MCP responde perguntas sobre ele.
Forma 1: entrega de arquivo
O loop é totalmente desacoplado: mail-muncher nunca chama o agente, e o agente nunca precisa chamar o mail-muncher. Eles compartilham dois caminhos no disco.
1. O agente declara o que quer. Acrescente a um arquivo que ele possui:
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 assina essa declaração. Uma regra, apontada para o arquivo:
rules:
- name: agent-inbox
match:
from_domains_file: ~/.local/share/agent/domains.txt
dest: ~/mail/agent-inbox
formats: [eml, markdown]
3. Cada ciclo relê o arquivo. Execute-o a partir do cron, ou deixe o daemon rodando:
mail-muncher run # one cycle — the cron entrypoint
mail-muncher daemon --interval 5m # poll forever
4. E-mails correspondentes caem em dest como arquivos que o agente lê.
~/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
O .md é a renderização consumível — analise o frontmatter, alimente o corpo
a um modelo, abra os anexos do diretório irmão:
---
subject: 'Re: Your application for Senior Engineer'
from: Jane Doe <jane@acme.com>
from_address: jane@acme.com
from_addresses: [jane@acme.com]
to: [me@example.com]
to_addresses: [me@example.com]
date: 2026-07-28T09:15:00Z
message_id: <abc123@acme.com>
thread_id: 18fe9c0d1a2b3c4d
thread_id_source: provider
in_reply_to: <application-000@example.com>
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 está em cada mensagem e nunca está vazio, então agrupar um diretório em
conversas é um sort em um campo — sem cadeias de referência para remontar.
5. Opcionalmente, pegue o manifesto em vez de percorrer a árvore. --json
grava um registro legível por máquina do ciclo no stdout, um objeto por conta,
enquanto cada linha de log vai para o stderr:
mail-muncher run --json 2>/dev/null | jq -r '.stored[].path'
Contrato completo: docs/manifest.md.
Três propriedades tornam isso seguro para colocar em um loop autônomo:
- Somente leitura por construção. Nada no mail-muncher grava em uma caixa de correio.
No Gmail, isso é a imposição do Google do escopo
gmail.readonly; no IMAP, éEXAMINEeBODY.PEEK[]e nenhum caminho de gravação. De qualquer forma, o que consome a saída — e qualquer bug que tenha — não pode enviar, excluir, ou modificar e-mail. Veja a comparação acima para saber qual dessas duas garantias você está obtendo. - Entrega idempotente. O nome do arquivo de uma mensagem incorpora um digest de
account + ":" + message id, então seu caminho de destino é uma função pura de sua identidade. Um arquivo que já está lá significa "um ciclo anterior armazenou isso", e o sink não grava nada. Reexecute, reproduza após perder estado, falhe no meio do ciclo ou sobreponha duas invocações de cron: a árvore converge, e nada é processado duas vezes. - Roteamento determinístico. As regras são ordenadas e a primeira correspondência vence, então cada
mensagem é gravada por exatamente uma regra. Dê a cada consumidor sua própria regra e
seu próprio
dest, e cada um obtém uma caixa de correio privada na qual nada mais grava.
A entrega são arquivos no disco, e nada aqui escuta em uma rede. O contrato é o diretório, com o manifesto como um registro opcional e legível por máquina do que mudou.
Forma 2: chamada de ferramenta
mail-muncher mcp é um servidor MCP stdio sobre o e-mail já arquivado. O
agente pergunta; nada é agendado.
{
"mcpServers": {
"mail-muncher": {
"command": "/usr/local/bin/mail-muncher",
"args": ["mcp", "--config", "/Users/you/.config/mail-muncher/config.yml"]
}
}
}
Cinco ferramentas:
| Ferramenta | O que ela responde |
|---|---|
list_rules | O que estou coletando, e de quais remetentes estou assinado agora? Cada from_domains_file é relido em cada chamada. |
list_messages | O que chegou? Filtre por regra, conta, tópico ou data; opcionalmente agrupado em conversas. |
search_messages | Onde está a mensagem que menciona X? Pesquisa de substring sobre assunto, remetente, destinatários, rótulos, nomes de anexos e corpo. |
read_message | Uma mensagem completa — metadados, corpo, nomes e tamanhos de anexos — e opcionalmente todo o tópico em ordem. |
sync | Busque novos e-mails uma vez, retornando o mesmo manifesto que run --json grava. |
É somente leitura sobre e-mail: nenhuma ferramenta envia, exclui ou modifica nada, e
sync — a única ferramenta que muda qualquer coisa — só pode adicionar arquivos.
O acesso ao sistema de arquivos é restrito às raízes dest da regra configurada, então a configuração,
qualquer credencial armazenada e o diretório de estado são inacessíveis e sem nome.
Um servidor mcp não configurado inicia mesmo assim, e isso é deliberado. Se um
cliente lança mail-muncher mcp antes de haver uma configuração, o servidor
não sai — ele completa o handshake, registra os mesmos cinco nomes de ferramentas e
responde a cada chamada com a orientação de configuração como um erro de ferramenta, então o agente tem
algo para repassar em vez de "o servidor falhou ao iniciar". Se você está conectando isso
para um operador, esse é o comportamento esperado e não um bug para relatar. A
orientação também vai para o stderr na inicialização, onde os clientes exibem o log do servidor.
Referência completa, conexão do cliente e cada argumento e campo de retorno: docs/mcp.md.
list_rules é o que fecha o loop. O agente grava um domínio em seu
próprio arquivo, então pergunta a list_rules e vê sua própria assinatura refletida de volta —
a mesma lista que o próximo ciclo corresponderá.
Alternativas
Leia isso antes de adotar. Várias ferramentas fazem a forma buscar-filtrar-entregar bem, e algumas delas são um ajuste melhor do que esta.
| Ferramenta | Use-a em vez disso quando |
|---|---|
| getmail6 | Você quer um buscador maduro e amplamente empacotado. Ele faz IMAP e OAuth2 do Gmail, entrega para Maildir/MDAs e filtra através de programas externos. Se um humano (ou mutt, ou notmuch) é o consumidor, esta é a ferramenta mais forte. |
| fdm | Você quer destinos Maildir por regra com uma configuração compacta e bem testada — exatamente a forma desta ferramenta, menos a fonte de filtro externa. O acesso ao Gmail é IMAP com senha de aplicativo. |
| lieer | Você quer toda a sua caixa de correio Gmail sincronizada bidirecionalmente em um Maildir local para notmuch, não um subconjunto filtrado extraído dela. |
gmail-archive | Era quase exatamente isso — consulta Gmail para Maildir, incremental — e seria a resposta óbvia se ainda fosse mantido. Não é desde 2018. |
gmail-exporter | Você quer uma exportação única, baseada em rótulos, em formato de planilha em vez de sincronização incremental. |
mbsync / offlineimap | Você quer replicação completa da caixa de correio e filtrará localmente depois. |
O que nenhuma dessas ferramentas faz, e para o que esta ferramenta existe: receber entrada de filtro de um arquivo que outro programa possui e relê-la a cada ciclo, e emitir uma renderização construída para um programa consumir, em vez de para um cliente de e-mail exibir. Se você não precisa de ambas as coisas, uma das ferramentas acima atenderá melhor e tem anos a mais de uso.
Instalação
Nenhuma toolchain Go é necessária para as duas primeiras opções.
Homebrew
brew install craigjmidwinter/tap/mail-muncher
Isso adiciona o tap craigjmidwinter/homebrew-tap
e instala um binário pré-compilado. brew upgrade mail-muncher rastreia novos
lançamentos.
Baixar um binário
Cada lançamento
inclui arquivos para macOS e Linux em amd64 e arm64, além de um
checksums.txt e uma assinatura sobre ele.
# 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
Se o binário então se recusar a executar completamente —
bash: mail-muncher: cannot execute binary file: Exec format error
— você tem um arquivo para a arquitetura errada. Essa mensagem vem do
kernel e não diz nada sobre o mail-muncher, então vale a pena conhecer o formato dela. Compare uname -m com o _amd64 / _arm64 no nome do arquivo que você
baixou; a linha ARCH= acima calcula a correta para você, então isso só
morde se você definir o nome manualmente.
Sem root? /usr/local/bin precisa disso; ~/.local/bin não. Solte o
sudo e instale lá em vez disso — nada sobre o mail-muncher quer um local
em todo o sistema:
install -d ~/.local/bin
install -m 0755 mail-muncher ~/.local/bin/mail-muncher
Se mail-muncher for então "command not found", ~/.local/bin não está no seu
PATH; adicione-o no seu perfil de shell.
Pular o sudo sem mudar o destino falha com um Permission denied from install em si — no macOS nomeando um arquivo temporário em vez de
mail-muncher, o que é confuso na primeira vez que você vê:
install: /usr/local/bin/INS@LPh1Hz: Permission denied # macOS
install: cannot create regular file '/usr/local/bin/mail-muncher': Permission denied # GNU
Qualquer mensagem significa a mesma coisa: escolha a rota ~/.local/bin acima, ou
coloque o sudo de volta.
No macOS, um binário que você baixou é colocado em quarentena pelo Gatekeeper. Limpe
com xattr -d com.apple.quarantine /usr/local/bin/mail-muncher, ou use a
instalação via Homebrew acima, que faz isso por você.
Verificar o que você baixou
Esta ferramenta lê seu e-mail. Verifique se o arquivo é o que o fluxo de lançamento construiu. Primeiro o checksum:
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
Depois a assinatura sobre checksums.txt. Lançamentos são assinados sem chave com
cosign — não há
chave pública para buscar e nenhuma chave privada que alguém precise proteger. O
certificado de assinatura é emitido para a identidade OIDC do próprio fluxo de lançamento do GitHub
e registrado no log público de transparência Rekor, então o que você está
verificando é "isso foi construído por release.yml neste repositório, a partir de uma tag":
cosign não é instalado por padrão em nenhuma plataforma e não está nos repositórios
usuais de distribuição, então cosign: command not found aqui significa "ainda não instalado",
não "verificação falhou". Obtenha-o primeiro — brew install cosign, ou
go install github.com/sigstore/cosign/v2/cmd/cosign@latest, ou um binário de lançamento dos documentos de
instalação.
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 significa que o arquivo de checksum é autêntico; o passo sha256sum então
amarra seu arquivo a ele. cosign 3 imprime um aviso de depreciação para
--certificate e --signature — a verificação ainda roda, e esses arquivos
destacados são o que cosign 2 também entende.
go install
O caminho certo se você já tem Go 1.25 ou mais novo:
go install github.com/craigjmidwinter/mail-muncher/cmd/mail-muncher@latest
Note que builds go install reportam dev para --version, porque a versão
é carimbada no momento da linkagem e a ferramenta go não faz isso. Binários lançados e
make build reportam a tag real. Se você registrar um bug a partir de um build go install,
diga qual commit você instalou.
Compilar a partir do código-fonte
git clone https://github.com/craigjmidwinter/mail-muncher
cd mail-muncher
make build # -> ./mail-muncher, version stamped from git describe
make snapshot compila o conjunto completo de arquivos de lançamento localmente (requer
goreleaser) se você quiser verificar o que um lançamento
conteria.
Os exemplos de configuração referenciados abaixo vivem em examples/ —
imap.yml, minimal.yml e
job-search.yml. Eles também estão incluídos em cada
arquivo de lançamento, então um download de binário também os tem. Você não precisa deles para
começar, no entanto: mail-muncher init escreve uma configuração do zero.
Imagem de contêiner
docker pull ghcr.io/craigjmidwinter/mail-muncher:latest
linux/amd64 e linux/arm64, construídos a partir dos mesmos binários que os arquivos
de lançamento carregam. O comando padrão da imagem é mcp, porque servir o
arquivo via stdio é o modo que um contêiner se adequa: um cliente o inicia, fala
com ele e o para. run e daemon também funcionam — sobrescreva o comando — mas em um
host esses são uma linha de cron e uma unidade launchd/systemd, que se encaixam melhor.
Duas montagens, e ambas importam:
# -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
Esse export combina com password_cmd: printenv IMAP_PASSWORD na configuração —
veja a nota abaixo sobre por que seu gerenciador de senhas do host não é alcançável de dentro
do contêiner.
Todo caminho dentro de config.yml tem que ser um caminho que o contêiner possa ver. Um
dest: de ~/Mail/receipts resolve contra o diretório home do contêiner,
não o seu, então o e-mail cai em uma camada que desaparece quando o contêiner sai.
Aponte dest: para o diretório montado — /home/muncher/archive/receipts para
a montagem acima — ou você arquivará no vazio e o manifesto
dirá alegremente que funcionou.
password_cmd roda dentro do contêiner, sob /bin/sh, o que significa que seu
gerenciador de senhas do host não está lá. pass show mail/fastmail não pode funcionar. Use
o material secreto que o contêiner tem:
password_cmd: printenv IMAP_PASSWORD # -e IMAP_PASSWORD
password_cmd: cat /run/secrets/imap-password # docker secret or a mounted file
Este é o único lugar onde o caminho do contêiner é genuinamente pior que uma instalação
no host: ele move a credencial para fora do seu gerenciador de senhas e para dentro do
ambiente do contêiner. Se essa troca não vale a pena para você, instale o
binário — password_cmd é projetado para o caso do host, e este é o
compromisso, não a intenção.
A imagem também é o que sustenta a listagem do MCP Registry;
server.json é essa entrada, e seu name tem que corresponder
ao rótulo io.modelcontextprotocol.server.name embutido na imagem.
Publicar essa entrada é automático. Marcar um lançamento compila e envia a
imagem, e então um segundo trabalho reescreve version e a tag da imagem em
server.json a partir da tag git e publica no registro, autenticando
com a identidade OIDC do próprio fluxo de trabalho em vez de um token armazenado.
Então o version commitado em server.json é do último lançamento, e fica atrasado por
uma tag de propósito. A tag é a fonte da verdade; o arquivo é um modelo
que o CI carimba. Aumentá-lo manualmente não alcança nada.
Como uma skill do Claude Code
O repositório inclui um pacote de skill e plugin sob skills/, que
instala o mail-muncher como algo que um agente pode configurar e dirigir para você —
escrevendo a configuração, rodando auth, e conectando o servidor MCP ao seu cliente.
Se é assim que você quer adotá-lo, comece lá em vez do quickstart
abaixo.
A skill lidera com provider: imap e dirige mail-muncher init, então ela
segue a mesma rota de dois minutos que este README faz em vez de enviar você para o
Google Cloud Console.
Windows
Não há build para Windows, e nenhuma das opções acima contorna isso silenciosamente.
Homebrew não roda no Windows. Os arquivos de lançamento são darwin e
linux apenas, e o trecho de download acima é um script de shell POSIX construído sobre
uname, que PowerShell e cmd não conseguem rodar de jeito nenhum.
go install é o único caminho que produz algo, e esse é o problema
que vale a pena declarar claramente. Go cross-compila este módulo limpo — sem cgo, sem
tags de build de plataforma fora de um arquivo de teste — então você obtém um mail-muncher.exe que
inicia, e mail-muncher init que escreve uma configuração sem reclamar. Ele
para no primeiro run. O provedor IMAP, o caminho de ~2 min que este README lidera,
executa imap.password_cmd entregando-o a /bin/sh -c
(internal/provider/imap/password.go),
e uma máquina Windows padrão não tem /bin/sh. init é cuidadoso o suficiente para não
semear uma configuração Windows com uma ferramenta de segredo macOS ou Linux, mas o comando que
ele semeia ainda vai para um shell que não está lá, então a falha chega tarde
e culpa a coisa errada.
O provedor Gmail não tem tal dependência — seu fluxo OAuth já escolhe
rundll32 no Windows — então pode funcionar de ponta a ponta. É não testado lá e
não suportado.
O que funciona no Windows: a imagem de contêiner sob
Docker Desktop, ou qualquer caminho de instalação dentro do WSL2, onde um binário Linux e
/bin/sh ambos existem.
Atualização
brew upgrade mail-muncher # Homebrew
go install github.com/craigjmidwinter/mail-muncher/cmd/mail-muncher@latest
Para um binário baixado, repita os passos de download acima — o passo install
sobrescreve no lugar. Nada mais precisa mudar: o esquema de configuração e o
layout em disco são estáveis dentro de 0.x, e cursores de sincronização em state_dir são
lidos por qualquer versão mais nova. CHANGELOG.md registra qualquer coisa que
tornaria isso falso, e não há nada lá ainda.
Verifique no que você caiu com mail-muncher --version. Um build sem make
reporta dev — esse é o problema do carimbo de versão go install, não uma instalação
quebrada.
Desinstalação
Remover o binário deixa todo o resto para trás, então isso está na ordem que remove o material mais sensível primeiro. Nada aqui é feito por você:
# 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
Seu e-mail arquivado está deliberadamente fora dessa lista. Ele vive em qualquer
dest que suas regras nomearam — ~/Mail/mail-muncher se você pegou o padrão init
— e esses são arquivos comuns que sobrevivem à ferramenta, que é o ponto
inteiro do formato. grep -n 'dest:' ~/.config/mail-muncher/config.yml antes do passo
3 se você quiser os caminhos, e delete-os você mesmo se quiser o e-mail sumido.
O cask Homebrew não carrega nenhuma estrofe zap, então brew uninstall remove o
binário e nada sob seu diretório home. Isso é de propósito — e-mail que esta
ferramenta já escreveu é seu, e um desinstalador é um lugar ruim para
descobrir o contrário.
Quickstart
Cerca de cinco minutos, sem navegador, sem clone. Este é o caminho IMAP; para a API Gmail em vez disso, leia Quickstart: Gmail abaixo antes de começar, porque custa cerca de dez minutos no Google Cloud Console e o token expira semanalmente.
0. Verifique a instalação. Logo após instalar, antes de haver qualquer configuração:
mail-muncher run
Isso é um teste de fumaça genuinamente útil em vez de um erro. Ele sai com 1 e diz exatamente onde procurou, o que rodar em seguida, e o que cada provedor custa:
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/
Todo comando que precisa de uma configuração diz isso, então uma instalação quebrada e uma não configurada nunca parecem iguais.
1. Obtenha uma senha de app. Da própria página de configurações do seu provedor de e-mail — Gmail, Fastmail, iCloud, Proton, sua conta de trabalho. Ela é escopada para este uso único e você pode revogá-la sem tocar em mais nada. Coloque-a onde quer que você já guarde segredos:
# 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 nunca armazena isso. Ele roda um comando que você nomeia e lê a
senha da saída stdout desse comando, então o segredo fica no seu gerenciador
de senhas. Não há deliberadamente nenhuma chave password: no esquema de configuração.
2. Escreva uma configuração.
mail-muncher init --provider imap
Account name [personal]:
Write matched mail to [~/Mail/mail-muncher]:
IMAP host: imap.fastmail.com
IMAP username: you@fastmail.com
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/
Não há passo de edição. init pergunta tudo o que o caminho IMAP precisa
e escreve uma configuração que valida na primeira tentativa. O comando de senha é
oferecido com o padrão da sua plataforma já preenchido — Keychain no macOS,
secret-tool no Linux, pass em outros lugares — então pressionar Enter através dele é uma
resposta real, não um placeholder.
Toda pergunta tem uma flag, para responder antecipadamente ou scriptar a coisa toda:
mail-muncher init --provider imap --yes \
--host imap.fastmail.com --username you@fastmail.com
--yes assume o padrão para tudo que tem um padrão honesto, por isso
ainda exige --provider, --host e --username. Esses três não têm
padrão que valha a pena adivinhar, e ele diz isso em vez de escrever um espaço reservado:
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
Adicione --account NAME, --dest DIR e --password-cmd CMD para responder ao restante.
Uma configuração existente nunca é sobrescrita sem --force.
~/.config/mail-muncher/config.yml é o caminho padrão; --config o substitui
em todos os lugares, inclusive para init.
3. Verifique se o comando de senha imprime a senha, e nada mais.
security find-generic-password -s mail-muncher -w | cat -A
| cat -A torna visível um prompt perdido, aviso ou linha em branco no final.
Qualquer coisa extra no stdout vira parte da senha e o login falha —
essa é a razão mais comum para uma primeira execução não autenticar.
Aqui está o que init escreveu, para referência; está comentado por completo, e
examples/imap.yml é uma versão mais completa e trabalhada:
accounts:
- name: personal
provider: imap
imap:
host: imap.fastmail.com
username: you@fastmail.com
password_cmd: security find-generic-password -s mail-muncher -w
mailboxes: [INBOX]
4. Verifique a configuração.
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
Uma conta IMAP valida de forma limpa: nenhum arquivo de credenciais para encontrar, nenhum token para
ter escrito ainda, nada no disco. OK sem avisos é o resultado
esperado. validate analisa a configuração, compila a árvore de correspondência de cada regra e
verifica os arquivos que ela referencia. Arquivos ausentes que pertencem a outro programa — um
from_domains_file, ou no caminho Gmail as credenciais OAuth e o token — são
avisos, não erros:
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. Veja o que uma execução real faria.
mail-muncher run --dry-run
Uma execução de teste conecta, busca e avalia exatamente como uma execução real, e
relata o caminho para onde cada correspondência seria gravada. Ela não grava arquivos e não
salva o estado de sincronização, então você pode executá-la quantas vezes quiser. É aqui também
que um host, nome de usuário ou password_cmd errado aparece, nomeado exatamente:
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. Execute.
mail-muncher run
A configuração que init escreveu carrega uma regra inicial que corresponde a tudo mais novo que
72h, então esta primeira execução tem garantia de armazenar algo — uma execução que não
armazena nada é indistinguível de uma instalação quebrada. Depois execute novamente:
tudo que já está no disco é relatado como skipped, e o cursor incremental
faz com que a segunda execução mal fale com o servidor.
Quando isso funcionar, restrinja a regra inicial para o que você realmente quer (docs/filters.md), depois coloque-a em um agendamento (veja Agendamento). docs/configuration.md tem todas as chaves.
Início rápido: Gmail
Siga este caminho se você quer especificamente a API Gmail e uma garantia de somente leitura imposta pelo Google em vez deste programa. Conheça os dois custos antes de começar, porque ambos são estruturais e nenhum desaparece:
- Cerca de dez minutos no Google Cloud Console, logo de início.
gmail.readonlyé um escopo restrito do Google, então o mail-muncher não inclui cliente OAuth e nunca incluirá — você registra seu próprio projeto e cliente Desktop e baixa o JSON dele. - O token expira a cada 7 dias. O Google aplica isso a toda tela de
consentimento ainda em modo Teste, que será o seu caso.
mail-muncher authprecisa ser executado novamente toda semana. Não há configuração que remova isso; docs/gmail-setup.md explica o porquê e quanto custam as alternativas.
Se nenhum dos dois valer a pena para você, o caminho IMAP acima funciona em uma conta 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 imprime uma URL de consentimento (e tenta abrir um navegador), escuta em uma
porta de loopback pelo redirecionamento e grava o token no token_file da conta
com modo 0600. É um comando exclusivo do Gmail — em uma conta IMAP ele
recusa, porque não há nada para autorizar. Os passos 4 a 6 do início rápido
IMAP acima então se aplicam sem alterações; validate relatará dois avisos até que os
arquivos de credenciais e token existam.
Arquivos de filtro gerenciados externamente
Este é o recurso em torno do qual a ferramenta é construída, então vale a pena ser preciso sobre a semântica.
from_domains_file nomeia um arquivo que o mail-muncher não possui, não
cria e nunca grava:
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
- Lido uma vez por ciclo, no primeiro uso. Não uma vez por processo, e não uma vez por
mensagem.
runo relê; cada tick do daemon o relê. Um arquivo referenciado por várias regras é lido uma vez e compartilhado. - Ausente ou ilegível nunca é fatal. O predicado simplesmente não corresponde a nada e um aviso é registrado para aquele arquivo naquele ciclo. O programa dono pode ainda não tê-lo criado, e o mail-muncher não deve falhar por causa disso.
- Análise liberal. Uma entrada por linha;
#inicia um comentário; linhas em branco são ignoradas; espaços ao redor são removidos; um@inicial e um.final são removidos; tudo é convertido para minúsculas; duplicatas colapsam. Uma entrada sem ponto é mantida e registrada como suspeita em vez de descartada, porque o arquivo pertence a outra pessoa e adivinhar errado não deve descartar silenciosamente uma entrada. - Igualdade ou subdomínio.
acme.comcorresponde aacme.comecareers.acme.com, mas não anotacme.com.
As mesmas regras de correspondência se aplicam ao predicado inline from_domains:; a única
diferença é quem é dono da lista.
Quando uma lista de domínios não consegue expressar
Alguns remetentes não podem ser enumerados antecipadamente. O e-mail de uma empresa pode chegar
de wagepoint.teamtailor.com, mail.wagepoint.com e
notifications@wagepoint-hr.example — uma lista de domínios só pode nomear hosts que você
já conhece. from_regex_file é a mesma ideia para padrões:
match:
from_regex_file: ~/.local/share/jobsearch/companies.txt
# one RE2 pattern per line, unanchored
wagepoint
(?i)^careers@acme\.io$
teamtailor\.com$
O ciclo de vida é idêntico — lido uma vez por ciclo, ausência nunca é fatal,
on_degraded_filter governa o cursor. Duas diferenças deliberadas em relação ao
formato de domínio:
- Nada é convertido para minúsculas, porque uma regex é sensível a maiúsculas por construção.
Escreva
(?i)quando quiser o contrário. #só inicia um comentário no início de uma linha. Truncar um padrão em um#no meio da linha mudaria silenciosamente o que ele corresponde.
Os modos de falha são opostos, e é por isso que as proteções diferem. Um erro de digitação
em uma lista de domínios corresponde a nada — o custo é o silêncio. Um erro de digitação
em uma lista de padrões pode corresponder a tudo: .* ou uma linha em branco perdida
reivindica a caixa de entrada inteira. Então um padrão vazio, ou qualquer padrão que corresponda à
string vazia, é recusado de imediato; um padrão que falha ao compilar é recusado sozinho enquanto
o restante do arquivo permanece em vigor; e a contagem de padrões carregados é registrada
a cada ciclo, então um arquivo que caiu de doze padrões para um pega-tudo é um
número na saída da sua execução em vez de uma descoberta por meio de um disco cheio.
Configuração
Referência completa: docs/configuration.md. Arquivos executáveis:
examples/imap.yml,
examples/minimal.yml,
examples/job-search.yml.
O bloco de conta é a única parte que difere por provedor. IMAP:
accounts:
- name: personal
provider: imap
imap:
host: imap.fastmail.com
port: 993 # default
tls: true # default
username: you@fastmail.com
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]
| Chave | Tipo | Padrão | Descrição |
|---|---|---|---|
state_dir | caminho | ~/.local/state/mail-muncher | Cursors de sincronização (um arquivo JSON por conta), o bloqueio de ciclo, o bloqueio de instância e o diretório de quarentena. |
on_message_failure | quarantine, abort | quarantine | O que fazer com uma mensagem que não pode ser analisada ou que um sink falhou. Veja abaixo. |
on_degraded_filter | hold, fail, proceed | hold | O que fazer quando o from_domains_file ou from_regex_file de uma regra não pode ser lido. Veja abaixo. |
quarantine_dir | caminho | <state_dir>/quarantine | Onde mensagens em quarentena são estacionadas. |
accounts | lista | — | Caixas de correio para buscar. Pelo menos uma é obrigatória. |
accounts[].name | string | — | Obrigatório, único. Nomeia o arquivo de estado e é ao que rules[].account se refere. |
accounts[].provider | imap, gmail | — | Obrigatório; não há padrão. Qual backend busca. Veja Duas maneiras de conectar uma caixa de correio. |
accounts[].imap | mapeamento | — | Obrigatório — e somente permitido — quando o provedor é imap. |
accounts[].imap.host | string | — | Obrigatório. imap.fastmail.com, imap.gmail.com, 127.0.0.1 para a Proton Bridge. |
accounts[].imap.port | inteiro | 993 | 993 é TLS implícito (IMAPS) e combina com o padrão tls: true. |
accounts[].imap.username | string | — | Obrigatório. Geralmente o endereço completo; alguns provedores querem a parte local simples. |
accounts[].imap.password_cmd | comando shell | — | Obrigatório. Executado sob /bin/sh -c; o stdout dele é a senha. Deliberadamente não há chave password — o segredo permanece no seu gerenciador de senhas. |
accounts[].imap.mailboxes | lista de strings | [INBOX] | Pastas para buscar, cada uma com seu próprio cursor. Um nome também serve como valor do predicado label. Uma pasta que o servidor não tem é um erro, não uma pasta vazia. |
accounts[].imap.tls | booleano | true | TLS implícito na conexão. false envia a senha e cada corpo em texto claro; validate avisa. Legítimo apenas em loopback ou atrás de um stunnel. |
accounts[].imap.initial_lookback | duração Go | 720h | Até onde uma primeira sincronização de cada caixa de correio alcança, e novamente após qualquer mudança de UIDVALIDITY. Deve ser positivo. |
accounts[].gmail | mapeamento | — | Obrigatório — e somente permitido — quando o provedor é gmail. |
accounts[].gmail.credentials_file | caminho | — | Obrigatório. O JSON de cliente OAuth baixado do Google Cloud. |
accounts[].gmail.token_file | caminho | — | Obrigatório. Onde auth armazena em cache o token OAuth, modo 0600. |
accounts[].gmail.query | string | nenhum | Expressão de busca Gmail opcional. Uma otimização de custo apenas para a varredura inicial — veja abaixo. |
accounts[].gmail.initial_lookback | duração Go | 720h | Até onde a varredura inicial alcança. Deve ser positivo. Veja Backfill. |
accounts[].gmail.include_spam_trash | booleano | false | Buscar mensagens em Spam e Lixeira. Honrado de forma idêntica por ambos os caminhos de sincronização Gmail. validate avisa quando verdadeiro. Veja Spam e Lixeira. |
rules | lista | — | Avaliada em ordem contra cada mensagem; a primeira correspondência vence. |
rules[].name | string | — | Obrigatório, único. Aparece em logs e no frontmatter markdown. |
rules[].account | string | todas as contas | Restringe a regra a uma conta. |
rules[].match | nó de correspondência | — | Obrigatório. Veja Filtros. |
rules[].dest | caminho | — | Obrigatório. Diretório de destino; criado sob demanda. |
rules[].formats | lista de eml, markdown | [eml] | Renderizações para gravar. |
Notas que pegam as pessoas:
- Chaves desconhecidas são um erro grave. Um erro de digitação falha o carregamento em vez de ser ignorado,
então
validatecapturainitial_lookbakantes de uma execução acontecer. ~e$VARsão expandidos em todo campo com valor de caminho, incluindo valores defrom_domains_fileefrom_regex_filedentro de uma árvore de correspondência. Formas~usernão são suportadas. Uma variável indefinida expande para a string vazia, como em um shell.gmail.querynão filtra o que é mantido e se aplica a menos do que você pensa. Ele é enviado ao Gmail na primeira varredura de uma conta e em nenhum outro lugar — não em ciclos incrementais, e não em uma varredura de recuperação após o cursor de histórico expirar. Ele nunca é reaplicado localmente. Suas regras são a única autoridade sobre o que é armazenado. Mantenha a consulta ampla ou omita-a.- Spam e Lixeira não são buscados por padrão (Gmail). Ambos os caminhos de sincronização do Gmail
concordam nisso: varreduras completas passam
includeSpamTrash=false, e o caminho incremental descarta mensagens rotuladas comoSPAMouTRASHantes de chegarem ao pipeline. Definagmail.include_spam_trash: truepara buscá-las mesmo assim — veja Spam e Lixeira. No IMAP não há chave equivalente: você busca exatamente as pastas que lista emmailboxes:, então simplesmente não listar a pasta de lixo é todo o mecanismo. - Os blocos
gmail:eimap:são mutuamente exclusivos. Definir aquele que não corresponde aprovider:é um erro grave em vez de um bloco silenciosamente ignorado, então um blocoimap:sob uma conta Gmail não pode deixar você acreditando que está buscando via IMAP quando não está.
Políticas para as duas coisas que podem dar errado
Ambas as chaves ficam no nível superior, ao lado de state_dir. Os padrões são as escolhas
seguras; você só as altera se decidiu qual falha prefere.
on_message_failure — uma mensagem que não será analisada, ou onde cada
renderização que sua regra pediu falhou ao gravar.
| Valor | Comportamento |
|---|---|
quarantine (padrão) | Grava os bytes brutos em <quarantine_dir>/<account>/<id>.eml com um sidecar .json nomeando a falha, então deixa o cursor avançar além da mensagem. Nada é perdido, e uma mensagem venenosa não pode travar o pipeline. Contado como quarantined no resumo e manifesto; a execução ainda sai com 0. |
abort | Retorna a falha, então o cursor não avança e a mensagem é re-buscada no próximo ciclo. A troca é explícita: uma mensagem permanentemente não analisável trava a conta até que um humano lide com ela. |
Uma gravação de quarentena que falha cai para a semântica de abort para essa
mensagem — recusar avançar é recuperável, perder a mensagem não é.
on_degraded_filter — o from_domains_file ou from_regex_file de uma regra está ausente, ilegível,
ou truncado no meio. Tal arquivo não corresponde a nada, então sem uma política
cada mensagem naquele ciclo seria avaliada contra uma lista vazia, considerada não
correspondente e consumida.
| Valor | Comportamento |
|---|---|
hold (padrão) | Executa o ciclo e armazena tudo que correspondeu, registra a degradação no nível de erro, mas não salva o cursor avançado — então o mesmo e-mail é reavaliado quando o arquivo retorna. O manifesto relata degraded e state_held. Saída 0. |
fail | Encerra o ciclo antes que qualquer coisa seja buscada. Nada armazenado, nada avançado, saída não zero. |
proceed | Trata uma lista ilegível como vazia e avança mesmo assim. O comportamento antigo, e a única opção que aceita perda silenciosa de e-mails desejados — validate avisa sobre isso. |
Arquivos já armazenados sob hold permanecem armazenados: os sinks são idempotentes, então a
re-execução os ignora.
Filtros
Referência completa e livro de receitas: docs/filters.md.
Um valor match: é um mapeamento com exatamente uma chave — um combinador ou um
predicado. Duas chaves em um mapeamento é um erro de compilação que diz para combiná-las
com all: ou any:. Regexes e durações são compilados quando a configuração
carrega, então um padrão ruim é uma falha de validate, não uma surpresa às 3h da manhã.
Combinadores
| Chave | Valor | Corresponde quando |
|---|---|---|
all | lista de nós | todo filho corresponde (pelo menos um filho é necessário) |
any | lista de nós | pelo menos um filho corresponde (pelo menos um filho é necessário) |
not | um único nó | o filho não corresponde |
match:
all:
- any:
- from_domains: [acme.com]
- from_domains_file: ~/.local/share/agent/domains.txt
- not:
subject_regex: "(?i)^\\[newsletter\\]"
Predicados
| Chave | Valor | Corresponde quando |
|---|---|---|
from_domains | lista de domínios | o domínio de qualquer endereço From é igual ou um subdomínio de um domínio listado |
from_domains_file | caminho | o mesmo, com a lista lida de um arquivo de propriedade externa a cada ciclo |
from_regex | padrão RE2 | o padrão corresponde a qualquer addr-spec From (sem nome de exibição) |
from_regex_file | caminho | o mesmo, com os padrões lidos de um arquivo de propriedade externa a cada ciclo |
to_regex | padrão RE2 | o padrão corresponde a qualquer addr-spec To ou Cc |
subject_regex | padrão RE2 | o padrão corresponde ao Subject decodificado |
header | {name: X-Foo, regex: ...} | o padrão corresponde a qualquer valor desse cabeçalho |
has_attachment | true / false | a mensagem tem (ou não tem) um anexo real |
label | nome do rótulo | a mensagem carrega esse rótulo do provedor, comparado exatamente. No Gmail, isso é um rótulo do Gmail; no IMAP, é o nome da caixa de correio de onde a mensagem veio |
older_than | duração Go | a mensagem Date está mais no passado do que a duração |
newer_than | duração Go | a mensagem Date é mais recente do que a duração |
Um exemplo trabalhado de cada:
# 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
Detalhes que valem a pena saber:
from_regexeto_regextestam o endereço puro (jane@acme.com), nunca o nome de exibição. Useheader: {name: From, regex: ...}para testar o cabeçalho bruto incluindo o nome de exibição.has_attachmentconta partes marcadas comoContent-Disposition: attachment. Imagens inline referenciadas porcid:não são anexos.labelé sensível a maiúsculas e exato —label: inboxnão corresponde aINBOX. Em uma conta IMAP, os valores são os nomes das caixas de correio que você listou sobimap.mailboxes, então uma mensagem pode carregar apenas aquela de onde foi buscada.older_than/newer_thancomparam com o cabeçalhoDateda mensagem, caindo para a data interna do provedor quando o cabeçalho está ausente ou não analisável. Uma mensagem sem data utilizável não corresponde a nenhum.- Padrões são Go RE2: sem
backreferences e sem lookaround. Prefixe com
(?i)para insensibilidade a maiúsculas. Em YAML, prefira aspas duplas e escape barras invertidas ("\\."), ou use aspas simples onde nenhum escape é necessário. - Use
true/falseparahas_attachment. YAML 1.2 tratayesenocomo strings, e mail-muncher os rejeita.
Spam e Lixeira (Gmail)
Spam e Lixeira não são buscados por padrão. Nada nessas pastas alcança suas regras, e nada chega ao disco. Spam é a fonte mais provável de texto hostil e escrito por atacantes em um pipeline que termina na janela de contexto de um modelo, então o padrão é deixá-lo onde o Gmail o colocou.
Esta seção inteira é sobre o provedor Gmail. IMAP não tem chave equivalente
porque não precisa: uma conta IMAP busca exatamente as pastas nomeadas em
imap.mailboxes, então lixo chega apenas se você pedir por nome.
Se você quiser mesmo assim — uma mensagem legítima classificada erroneamente como spam, ou um arquivo genuinamente completo — defina a chave por conta:
accounts:
- name: personal
gmail:
include_spam_trash: true # validate warns; that is deliberate
As duas configurações fazem trabalhos diferentes, e você pode querer ambas:
| Decide | |
|---|---|
gmail.include_spam_trash | se essas mensagens são buscadas de forma alguma |
Uma regra nos rótulos SPAM / TRASH | o que acontece com elas uma vez buscadas |
gmail.query não pode fazer nenhum dos dois trabalhos. Ele é enviado apenas na primeira varredura, então
-in:spam lá não faz nada para qualquer ciclo posterior. Com
include_spam_trash: true definido, discrimine com uma regra — o mecanismo de filtro é
a única coisa que vê cada mensagem buscada:
rules:
- name: job-search
match:
all:
- from_domains_file: ~/.local/share/jobsearch/domains.txt
- not:
any:
- label: SPAM
- label: TRASH
dest: ~/Mail/job-search
Os rótulos de sistema do Gmail são exatos e em maiúsculas. Se você quiser Spam e Lixeira fora
de toda regra, coloque o not: em cada uma — não há exclusão global, por
design: regras são a autoridade única sobre o que é armazenado.
Layout no disco
Os arquivos que mail-muncher grava são sua API pública. Esta seção é o tour; docs/output-format.md é o contrato — cada chave de frontmatter, por que o frontmatter precisa de um analisador YAML real, e as regras para enumerar uma árvore de entrega com segurança. Leia antes de escrever um consumidor, e veja examples/read_delivered.py para um correto e curto.
Cada sink arquiva uma mensagem sob o dest da regra pela data da mensagem, em 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
O nome base é compartilhado por todo formato, então as renderizações de uma mensagem se ordenam juntas:
<unix-seconds>-<sha256(account + ":" + message-id)[:16]>-<subject-slug>
O fragmento de digest é 16 caracteres hexadecimais — 64 bits. Duas mensagens colidindo nele não é alcançável em qualquer volume que uma caixa de correio produza, e leitores do arquivo o analisam de volta como um id de mensagem, então trate a largura como parte do layout.
- O timestamp ordena um diretório cronologicamente.
- O digest é a chave de idempotência. Ele depende apenas do nome da conta e do id de mensagem do provedor, então o caminho é uma função pura da identidade da mensagem.
- O slug é o assunto em minúsculas, com cada caractere fora de
[a-z0-9]colapsado para um único-, aparado e truncado para 40 caracteres.
Duas ressalvas sobre o slug, ambas deliberadas:
- É apenas ASCII. Um assunto escrito inteiramente em um script não latino, ou
inteiramente em emoji, vira slug
no-subject. Nomes de arquivo não ASCII estariam sujeitos à normalização Unicode do sistema de arquivos (HFS+ armazena NFD), o que pode fazer o nome gravado diferir do nome que o próximo ciclo verifica — e essa verificação de existência é toda a história da idempotência. O digest ainda mantém tais mensagens separadas. - É cosmético. Apenas o digest carrega identidade. Duas mensagens com o mesmo assunto nunca colidem.
Como os arquivos são gravados
Um arquivo de mensagem é gravado em um arquivo temporário em seu diretório de destino, sincronizado,
e então hard-linkado no lugar com link(2). Três consequências que valem a pena
confiar:
- Um arquivo parcial nunca é publicado. O arquivo temporário está completo antes do nome existir.
- Um arquivo existente nunca é sobrescrito.
link(2)falha comEEXISTem vez de sobrescrever, ao contrário derename(2). Essa falha é a verificação de idempotência — o kernel decide se o nome está livre no instante em que é reivindicado, então não há janela em que outro gravador possa inserir um arquivo e tê-lo silenciosamente substituído. "Já está lá" é relatado comoskipped. - Symlinks são recusados, não seguidos. Um symlink no caminho final de uma mensagem,
ou representando o diretório
<YYYY>ou<MM>abaixo dedest, é um erro para o qual a mensagem é contada e registrada. Nada no layout é legitimamente um link, então um significa que outra coisa os está colocando lá. O própriodest:da regra é isento — apontá-lo para outro volume é comum.
Em um sistema de arquivos sem hard links (FAT, alguns mounts de rede), o fallback é uma
gravação O_CREAT|O_EXCL no lugar: ainda atomicamente sem sobrescrita e ainda
à prova de symlink, ao custo da garantia de não haver arquivo parcial.
Anexos são a única exceção: eles são gravados com um arquivo temporário e
rename(2), porque seus nomes não são o marcador de idempotência — o .md
acima deles decide isso.
Diretórios são criados com 0700 e arquivos com 0600. Correspondência arquivada é
correspondência privada e anexos decodificados, então recebe o mesmo tratamento que os
cursores de sincronização e o token OAuth: nada aqui é legível por outros usuários locais.
Uma ferramenta que você executa como você mesmo não é afetada.
O arquivo .eml
model.Message.Raw, byte por byte, exatamente como o provedor o entregou.
Nada é recodificado, re-embrulhado ou normalizado, então ele passa por qualquer
ferramenta de e-mail e ainda verifica contra assinaturas DKIM. Esta é a cópia de fidelidade.
O arquivo .md
Frontmatter YAML, depois o corpo, depois links para quaisquer anexos.
- Seleção do corpo: a parte
text/plainse houver; caso contrário, a partetext/htmlconvertida para markdown; caso contrário, o literal*(no body)*. Finais de linha são normalizados para LF, espaços em branco finais são removidos por linha, e linhas em branco iniciais e finais são aparadas. - Frontmatter sempre carrega
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,labelseattachmentssão omitidos quando vazios. Analise endereços dos campos*_address/*_addresses, nunca defrom/to/cc— esses são strings de exibição, e um nome de exibição escolhido pelo remetente contendo<,>ou,os torna ambíguos. Os campos legíveis por máquina carregam addr-specs simples e não podem ser falsificados dessa forma. É produzido com um codificador YAML, não com formatação de string, então um assunto cheio de aspas e dois-pontos não pode quebrar a análise — o que também significa que você precisa de um analisador YAML real para lê-lo. Um assunto com emoji chega entre aspas duplas com um escape\U0001F389, e um assunto contendo uma nova linha chega como um bloco escalar|-. Um divisorkey: valueerra em ambos. Veja docs/output-format.md. - Threading são três campos, não quatro.
thread_idé a chave de junção e é nunca vazio: o id de conversa do próprio provedor quando houver um, caso contrário, um sintetizado a partir da cadeiaReferencesda mensagem. Agrupe um diretório por ele sem casos especiais.thread_id_sourcediz o quanto confiar nesse agrupamento —provider,references,in_reply_toouself— porque a reconstrução é de melhor esforço e um mailer que quebra a cadeia divide um thread.in_reply_tonomeia o pai. A cadeia completaReferencesé deliberadamente omitida: ela é ilimitada, e o.emlao lado do arquivo a tem verbatim. - Anexos são escritos em
<basename>.attachments/ao lado do.md, com nomes de arquivo sanitizados (sem componentes de diretório, sem travessia de caminho), uma extensão.mdou.emlneutralizada para.md.attachment/.eml.attachment, e colisões de-duplicadas comoname-2.pdf,name-3.pdf. Eles são escritos antes do.md, então o documento nunca linka para um arquivo que não está lá. Seus conteúdos são escolhidos pelo remetente — veja a regra de enumeração abaixo. - Imagens inline
cid:não são resolvidas. Um corpo HTML que incorpora imagens por id de conteúdo renderiza como— um link não resolvido, não um caminho para o diretório de anexos. Se você precisar dos bytes da imagem, eles estão no.eml. Esta é uma limitação conhecida, não um bug. - O
.mdnão é um formato de fidelidade. Qualquer coisa que importe byte-exatamente deve ser lida do.eml.
Enumerando uma árvore de entrega
O conjunto autoritativo de mensagens entregues é exatamente
<dest>/<YYYY>/<MM>/*.md (ou *.eml) — dois níveis de profundidade, nunca um glob
recursivo:
find "$dest" -mindepth 3 -maxdepth 3 -type f -name '*.md' # correct
find "$dest" -name '*.md' # WRONG
Um glob recursivo desce em <basename>.attachments/, onde os arquivos vieram
de quem enviou o e-mail. Um atacante que consegue fazer uma regra corresponder pode anexar
um arquivo contendo frontmatter forjado e ter um consumidor descuidado lendo-o como uma
mensagem entregue com um from:, subject: e corpo arbitrários. Qualquer coisa
sob um diretório .attachments/ é controlada pelo remetente e nunca deve ser analisada
como uma mensagem.
Se o seu consumidor executa o próprio mail-muncher, run --json é ainda melhor: ele
lista exatamente os caminhos que este ciclo escreveu, então não pode ser confundido por nada
dest=~/Mail/job-search # o dest da regra
sentado na árvore.
Corpos de mensagens são texto controlado pelo atacante. Filtrado não é verificado. Trate o conteúdo do corpo como dados, nunca como instruções, e não conceda autoridade a ele meramente porque chegou através do mail-muncher.
docs/output-format.md tem o contrato completo, e examples/read_delivered.py é um leitor funcional.
Comandos
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
Qualquer comando que precise de uma configuração e não consiga encontrar uma imprime a orientação de configuração
mostrada em passo 0 do quickstart — o caminho que ele procurou, o
próximo comando, e o que cada provedor custa — em vez de um erro open: no such file.
Flags persistentes, disponíveis em todos os subcomandos:
| Flag | Padrão | Descrição |
|---|---|---|
--config | ~/.config/mail-muncher/config.yml | Caminho para o arquivo de configuração. |
--log-level | info | debug, info, warn ou error. Logs são texto log/slog no stderr. |
-v, --version | — | Imprime a versão. |
Flags por comando:
| Comando | Flag | Padrão | Descrição |
|---|---|---|---|
init | --provider | — | imap ou gmail. Solicitado quando omitido; obrigatório com --yes, porque os dois caminhos custam coisas diferentes e não há padrão honesto. |
init | --account | personal | Nome para a conta que a configuração cria. |
init | --dest | ~/Mail/mail-muncher | Onde a regra inicial escreve o e-mail correspondido. |
init | --host | — | Hostname do servidor IMAP, ex. imap.fastmail.com. Solicitado quando omitido; obrigatório com --yes no caminho IMAP. |
init | --username | — | Nome de usuário IMAP, geralmente o endereço completo. Solicitado quando omitido; obrigatório com --yes no caminho IMAP. |
init | --password-cmd | padrão da plataforma | Comando shell que imprime a senha do app no stdout. Padrão é Keychain no macOS, secret-tool no Linux, pass em outros lugares. |
init | --yes | false | Nunca perguntar; usar o padrão para cada resposta que tem um honesto. Ainda requer --provider, e no IMAP --host e --username. |
init | --force | false | Sobrescrever uma configuração existente. Sem isso, init recusa e sai com 1 em vez de sobrescrever as regras e caminhos de credenciais de alguém. |
run | --dry-run | false | Buscar e avaliar, relatar o que seria escrito, não escrever nada e não salvar estado. |
run | --json | false | Escrever um manifesto legível por máquina no stdout, um objeto JSON por conta. |
daemon | --interval | 5m | Tempo entre ciclos, mínimo 30s. Cada sleep é jittered em até ±10%. |
daemon | --dry-run | false | Como run --dry-run, a cada tick. |
daemon | --json | false | Escrever um manifesto no stdout após cada ciclo: JSON delimitado por nova linha, um objeto por conta por tick. |
auth | --account | — | Qual conta autenticar. Obrigatório quando a configuração tem mais de uma. Apenas contas Gmail — em uma conta IMAP auth recusa, porque não há nada para autorizar. |
mcp não aceita flags próprias; é configurado inteiramente por --config. Note
que init escreve em --config em vez de ler dele, e cria o
diretório (0700) e o arquivo (0600) conforme necessário.
--log-level debug registra a decisão da regra para cada mensagem — o id da mensagem,
seu assunto, e o nome da regra vencedora ou no match — que é a maneira mais rápida
de descobrir por que uma regra não está disparando. Debug também registra cada skip already stored
com seu caminho. Tudo vai para o stderr, então nunca perturba --json no
stdout.
Um intervalo abaixo de 30s é rejeitado antes que qualquer coisa comece:
error: --interval 10s is below the 30s minimum
Códigos de saída
| Código | Significado |
|---|---|
| 0 | Sucesso. Um ciclo que encontrou erros por mensagem (uma mensagem que não analisava, uma escrita de sink que falhou) ainda sai com 0 — esses são contados no resumo, não escalados. |
| 1 | Erro de configuração ou validação: o arquivo não analisava, uma regra não compilava, uma chave obrigatória está faltando, ou não há arquivo de configuração. Nada foi buscado. Também o que init retorna quando uma configuração já existe e --force não foi dado. |
| 2 | Falha de provedor ou autenticação: um token Gmail que nunca foi escrito ou que o Google rejeitou, um password_cmd IMAP que falhou ou um login que o servidor recusou, o servidor inacessível após tentativas. |
| 3 | Outra instância segura o lock do ciclo. Código de saída 3 é como uma invocação cron sobreposta relata "a anterior ainda está em andamento". |
Uma mensagem em quarentena não muda o status de saída: é uma falha no nível da mensagem
que foi tratada, contada como quarantined, e relatada no
manifesto. Alerte no contador, não no código de saída.
daemon nunca sai com 2. Um ciclo com falha é registrado com uma contagem de falhas consecutivas
e tentado novamente no próximo tick — um daemon que saísse no primeiro token expirado
precisaria de um humano exatamente no momento errado. Seus status são 0
(parado por SIGINT ou SIGTERM, depois de deixar o ciclo em andamento terminar e
salvar o estado), 1 (erro de configuração ou validação, incluindo um --interval abaixo
do mínimo), e 3 (outra instância já segurava o lock de instância ou ciclo no
início). Um lock segurado por um tick posterior — um run cron sobrepondo um tick do daemon
— é registrado e pulado, não fatal.
O resumo da execução
Cada ciclo registra e imprime uma linha de resumo por conta:
personal: fetched=128 matched=6 stored=6 skipped=0 parse_errors=0 sink_errors=0 quarantined=0 duration=4.1s
| Campo | Conta | Significado |
|---|---|---|
fetched | mensagens | Mensagens que o provedor entregou neste ciclo. |
matched | mensagens | Mensagens que alguma regra reivindicou. |
stored | renderizações | Renderizações realmente escritas. |
skipped | renderizações | Renderizações não escritas porque o destino já existia. |
parse_errors | mensagens | Mensagens que não analisavam; registradas e puladas. |
sink_errors | renderizações | Falhas de escrita; registradas e contadas, o ciclo continua. |
quarantined | mensagens | Mensagens estacionadas sob o diretório de quarentena porque não puderam ser entregues. |
vanished | mensagens | Mensagens que o provedor listou e depois descobriu que não existiam mais — deletadas entre a listagem e o download. Puladas, e o cursor avançou além delas. Apenas impresso quando não é zero. |
A lista de campos roda contiguamente de fetched= a duration=, então qualquer coisa
incomum sobre o ciclo é marcada no rótulo da conta em vez disso:
personal (dry-run): fetched=42 ...
personal (degraded, state held): fetched=42 ...
personal (stopped): fetched=12 ...
vanished=N é o único contador anexado condicionalmente. Ele aparece depois de
quarantined= apenas nas execuções onde uma mensagem desapareceu no meio do ciclo, e é
omitido em todos os outros lugares, então a lista de campos publicada é inalterada para todas as
outras execuções. Sob --json é summary.vanished, igualmente omitido quando zero
— analise-o como .summary.vanished // 0 se quiser alertar sobre ele.
Uma execução cron em estado estável se parece com fetched=0. Uma reexecução sobre a mesma janela
se parece com matched=N stored=0 skipped=N — isso é a idempotência funcionando.
O manifesto JSON
run --json e daemon --json substituem essa linha por um objeto legível por máquina
por conta — stored[], skipped[], quarantined[], os contadores e
se o cursor avançou. O stderr mantém cada linha de log, então
run --json 2>/dev/null é JSON puro e daemon --json é um fluxo NDJSON.
mail-muncher run --json 2>/dev/null | jq -r '.stored[] | "\(.rule)\t\(.path)"'
Contrato completo campo por campo: docs/manifest.md.
Estado e bloqueio
~/.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
O arquivo de estado de cada conta é JSON. O cursor dentro dele é o do provedor, então sua forma difere. Gmail:
{
"history_id": 918273,
"last_sync_time": "2026-07-28T09:15:00Z",
"seen_ids": ["18f2a...", "18f2b..."]
}
history_idé o cursor incremental do Gmail. Quando está definido, um ciclo pergunta ao Gmail apenas o que mudou desde então. O Gmail mantém aproximadamente uma semana de histórico; quando o cursor expira, a API responde 404, o mail-muncher registra um aviso, limpa o cursor e volta a uma varredura completa no mesmo ciclo.last_sync_timelimita o termoafter:de uma varredura completa. Uma varredura de recuperação alcança 24 horas além do watermark armazenado, para que e-mails que chegaram enquanto o ciclo anterior estava listando não sejam ignorados. A sobreposição é inofensiva: tudo o que já está no disco é pulado na gravação.seen_idsé um conjunto FIFO dos últimos 2000 IDs de mensagens entregues — cinto e suspensórios junto com os nomes de arquivo idempotentes dos sinks.
O IMAP rastreia cada caixa de correio de forma independente, como um par UIDVALIDITY/UID sob extra:
{
"last_sync_time": "2026-07-28T09:15:00Z",
"extra": {
"imap.INBOX.uidvalidity": "1650000000",
"imap.INBOX.last_uid": "48213"
},
"seen_ids": ["personal:INBOX:1650000000:48213"]
}
Um ciclo cujo UIDVALIDITY armazenado ainda corresponde ao do servidor pede
UID FETCH 48214:* — apenas o que chegou desde então. Um ciclo que descobre que ele mudou
descarta o UID armazenado e ressincroniza a partir de initial_lookback, porque
a mudança de UIDVALIDITY é o protocolo anunciando que cada UID que o mail-muncher
lembra agora nomeia uma mensagem diferente. Essa ressincronização re-arquiva a janela que
cobre sob nomes de arquivo novos, já que a identidade de uma mensagem neste caminho é
<account>:<mailbox>:<uidvalidity>:<uid>. A troca é deliberada: e-mails
duplicados podem ser excluídos, e-mails pulados silenciosamente não podem ser recuperados. Detalhe completo:
docs/configuration.md.
O diretório de estado é criado com 0700 e os arquivos de estado com 0600: saber quais contas existem e quais IDs de mensagens foram vistos não é informação pública.
Excluir o arquivo de estado de uma conta força uma nova varredura completa limitada por
initial_lookback. Isso é seguro — os sinks pulam tudo o que já está no disco —
e é a forma suportada de se recuperar de um cursor corrompido.
Os dois bloqueios
-
O bloqueio de ciclo,
<state_dir>/mail-muncher.lock, é baseado emflocke mantido durante a duração de cada ciclo porrun, por cada tick do daemon e pela ferramentasyncdo MCP. É o que impede uma invocação cron de competir com um daemon em execução nos mesmos cursores. Encontrá-lo mantido significa "agora não", não "quebrado":runsai com código 3, e um tick do daemon registra e pula. -
O bloqueio de instância,
<state_dir>/instance/mail-muncher.lock, é mantido por um daemon durante toda a vida do seu processo. O bloqueio de ciclo não pode fazer esse trabalho — ele é liberado entre ticks, então um segundo daemon iniciando enquanto o primeiro dorme passaria por ele e então faria polling para sempre ao lado dele, dobrando o tráfego de API contra os mesmos cursores. Um segundo daemon sai com código 3 imediatamente:another mail-muncher daemon is already running against this state directory; not starting
Ambos são liberados pelo SO se o processo falhar.
Quarentena
Sob o padrão on_message_failure: quarantine, uma mensagem que não pôde ser
entregue é gravada em <quarantine_dir>/<account>/<id>.eml — os bytes brutos,
verbatim — com um sidecar .json ao lado nomeando a regra, o estágio que
falhou (parse ou sink), o erro e a hora. O sidecar é
autocontido de propósito: varrer o diretório não deve exigir os logs da
execução.
Nada reentrega automaticamente. Corrija a causa e alimente o .eml de volta
manualmente. O diretório é 0700 e os arquivos 0600, como o resto do diretório
de estado — uma mensagem em quarentena é um e-mail inteiro sentado fora da sua
árvore de destino.
Agendamento
cron — um ciclo a cada dez minutos. run sai com código 3 se a invocação
anterior ainda estiver em andamento, então sobreposições são inofensivas:
*/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
Use um caminho absoluto para o binário e para a configuração: o ambiente do cron não é
o do seu shell, e a expansão de ~ na configuração depende de $HOME estar definido.
Um exemplo mais completo com o ambiente que o cron não fornece está em
contrib/crontab.sample.
launchd (macOS) — execute o daemon sob launchd para que ele sobreviva ao logout e
reinicie em caso de falha. Um plist pronto para editar está em
contrib/launchd/. Copie-o, edite os quatro caminhos absolutos
dentro da cópia — launchd não expande nem ~ nem $HOME — então carregue-o:
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 — nenhuma unit é enviada ainda; mail-muncher daemon --interval 5m é um
serviço Type=simple direto, ou emparelhe mail-muncher run com um timer.
Backfill: a primeira execução
Uma primeira execução ampla é um modo intencional, não um abuso. O padrão
initial_lookback é 720h (30 dias), o que silenciosamente significa que a coisa que você mais
quer no primeiro dia — todo o histórico do que você está coletando, no disco, uma vez —
é a coisa que não acontece por padrão.
Defina-o amplo para a primeira execução. A chave existe em ambos os provedores, sob qualquer bloco que sua conta use:
Para interrompê-lo, launchctl unload o mesmo caminho. Removê-lo permanentemente significa
excluir esse arquivo também — veja Desinstalação.
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: you@fastmail.com
password_cmd: pass show mail/fastmail
initial_lookback: 13140h # ~18 months — first run only, per mailbox
Então execute uma vez e reduza o valor de volta depois:
initial_lookback: 720h
Reduzi-lo de volta é seguro porque initial_lookback apenas limita a
varredura primeira de todas — de uma conta no Gmail, de cada caixa de correio no IMAP. Cada ciclo
posterior retoma do cursor armazenado e ignora a chave completamente. Ela é
rearmada apenas excluindo o arquivo de estado da conta, ou, no IMAP, pelo servidor
mudando o UIDVALIDITY.
O que esperar:
-
É uma grande varredura completa, com limite de taxa e tentativas, e pode levar um tempo. No Gmail, os downloads ocorrem quatro por vez e as páginas têm 500 mensagens; nenhum dos dois é configurável. No IMAP, torna-se uma busca UID
SINCEpor caixa de correio. -
Olhe antes de se comprometer.
mail-muncher run --dry-runbusca e avalia exatamente como uma execução real e relata cada caminho que escreveria, sem tocar na árvore de destino. Adicione--jsonpara contá-los:mail-muncher run --dry-run --json 2>/dev/null | jq '.summary' -
A execução seguinte deve relatar tudo como
skipped. Esse é o check de que o backfill realmente foi aplicado.
Durações Go não têm unidade de dia ou ano — multiplique horas. 18 meses é 13140h,
um ano é 8760h, 90 dias é 2160h.
O limite de 2000 entradas do conjunto visto não limita um backfill. Veja docs/architecture.md para o porquê: o nome de arquivo determinístico mais pular-se-existe é a chave real de idempotência, e o conjunto visto é apenas cinto e suspensórios.
Status e escopo
Pré-1.0. A versão atual é
v0.4.0, e
o que mudou em cada uma está em CHANGELOG.md; uma build sem
make relata sua versão como dev. O esquema de configuração é estável o suficiente para
escrever contra ele, mas trate-o como sujeito a mudanças até 1.0.
| Área | Status |
|---|---|
Provedor IMAP (password_cmd, cursores UID por caixa de correio, EXAMINE + BODY.PEEK[]) | Construído |
| Provedor Gmail (OAuth, varredura completa, sincronização incremental de histórico, download RAW) | Construído |
init — grava uma configuração validada para qualquer provedor, interativo ou scriptado | Construído |
| Orientação de configuração em todo comando não configurado ou parcialmente configurado | Construído |
mcp servindo corretamente o protocolo quando não configurado | Construído |
| Carregamento e validação de configuração | Construído |
| Motor de filtros (todos os combinadores e predicados listados acima) | Construído |
Sinks .eml e markdown | Construído |
run, daemon, bloqueio de ciclo, bloqueio de instância | Construído |
Manifestos de execução --json | Construído |
mcp — servidor MCP stdio, cinco ferramentas | Construído |
| Quarentena e as duas políticas de falha | Construído |
Deliberadamente fora do escopo
- Escrever na sua caixa de correio. Somente leitura é uma restrição de design, não uma fase. Sem rotulagem, sem exclusão, sem envio, sem rascunhos e, no IMAP, nem mesmo marcar uma mensagem como lida.
- Ser um cliente de e-mail. Sem UI e sem índice.
search_messagesé uma varredura de substring sem distinção de maiúsculas/minúsculas sobre os arquivos armazenados, não um mecanismo de busca: sem stemming, sem classificação, sem linguagem de consulta. Threads são agrupadas por um id carregado em cada mensagem, não remontadas em um modelo de conversa. - Uma API de rede. Nada escuta em um socket.
mcpfala um protocolo stdio com um cliente que o lançou como subprocesso — sem porta, sem endpoint HTTP, sem superfície de rede. A única coisa que sempre faz bind é a porta de loopbackauthque abre por alguns segundos durante o redirecionamento OAuth no caminho do Gmail. - Empacotar um cliente OAuth.
gmail.readonlyé um escopo restrito do Google, então o caminho do Gmail sempre significará registrar o seu próprio — que é exatamente por que o IMAP existe como o caminho de baixo atrito para entrar.
Limitações conhecidas
- Imagens inline
cid:são deixadas como links não resolvidos na saída markdown. - Slugs de assunto são apenas ASCII; assuntos não latinos são arquivados como
no-subject. - Nenhum predicado vê o corpo da mensagem; a correspondência é em cabeçalhos, rótulos e datas.
Gmail:
- O token do Gmail expira a cada 7 dias em uma tela de consentimento em modo Testing, então
mail-muncher authé uma tarefa semanal. Isso é política do Google, não uma configuração. gmail.queryaplica-se apenas à varredura primeira de todas de uma conta — não a ciclos incrementais e não a uma varredura de recuperação após o cursor expirar.- Spam e Lixeira não são buscados de forma alguma, a menos que
gmail.include_spam_trash: true. Uma vez buscados, uma regra nos rótulosSPAMeTRASHdecide o que acontece com eles. - A concorrência de download do Gmail (4) e o tamanho da página (500) não são configuráveis.
IMAP:
- Uma senha de app é uma credencial completa de e-mail. Somente leitura é aplicada pelo código do mail-muncher, não pelo seu provedor — veja a comparação.
- Um servidor mudando UIDVALIDITY re-arquiva a janela
initial_lookbacksob nomes de arquivo novos. Deliberado: e-mail duplicado é recuperável, e-mail pulado não é. - O IMAP não tem id de conversa no lado do servidor, então
thread_idé sempre sintetizado a partir da cadeiaReferences/In-Reply-Toethread_id_sourcenunca éprovider. - Uma caixa de correio que o servidor não tem é um erro, não uma pasta vazia — então um
erro de digitação em
mailboxes:falha a primeira execução em vez de parecer e-mail silencioso. - Apenas as pastas listadas em
mailboxes:são buscadas; não há opção "tudo" e nenhuma busca no lado do servidor.
Documentação
Navegável em https://craigjmidwinter.github.io/mail-muncher/, ou neste repositório:
- docs/configuration.md — todas as chaves de configuração para ambos os provedores, suas regras de validação e seus modos de falha. A seção
accounts[].imapé a referência IMAP; não há página de configuração separada porque não há configuração além de uma senha de aplicativo. - docs/gmail-setup.md — apenas o caminho Gmail: passo a passo do Google Cloud, a expiração de sete dias e cada mensagem de erro OAuth com sua correção. Nada disso se aplica a uma conta IMAP.
- docs/filters.md — a linguagem completa de árvore de correspondência, além de um livro de receitas com regras reais.
- docs/output-format.md — o contrato em disco: layout, nomes de arquivos, cada chave de frontmatter e as regras de segurança para enumerar uma árvore de entrega. Leia antes de escrever um consumidor.
- docs/manifest.md — o contrato de manifesto
--json, campo por campo. - docs/mcp.md — o servidor MCP: conexão do cliente e os argumentos e formatos de retorno de cada ferramenta.
- docs/architecture.md — o pipeline, suas junções e onde sua alteração se encaixa.
- CONTRIBUTING.md — build, testes e as convenções que o código segue.
Configs executáveis: examples/imap.yml (o caminho IMAP, bem comentado), examples/minimal.yml (a menor configuração Gmail útil), examples/job-search.yml (um arquivo de filtro gerenciado externamente em uso real) e examples/read_delivered.py (um consumidor correto). Todas as três configs passam em mail-muncher validate --config <file>.
Agradecimentos
A marca é definida em Silkscreen por Jason Kottke, usada sob a SIL Open Font License 1.1. Os ativos da marca, a paleta e as regras de uso estão em branding/BRAND.md.
Licença
MIT. Veja LICENSE.