mailwarden
Servidor MCP nativo para Gmail, focado em triagem de caixa de entrada — busca com resultados reverificados, etiquetas, arquivar/excluir, soneca no lado do servidor de e-mail, resumo de triagem, cancelamento de inscrição RFC 8058; sem ferramentas de envio por design.
Documentação
mailwarden
Um servidor MCP Gmail nativo e confiável — triagem completa de caixa de entrada para assistentes de IA, com o recurso que nenhum outro servidor MCP do Gmail oferece: soneca do lado da caixa de entrada.
Destaques
- Soneca — a única soneca do lado da caixa de entrada em um servidor MCP do Gmail. Arquivar um tópico agora, fazê-lo ressurgir na caixa de entrada em uma data. Construída com rótulos datados + uma varredura, funciona de qualquer cliente, é visível no próprio Gmail e sobrevive a reinicializações. (Onde outro servidor oferece uma "soneca", é uma lista de lembretes local — o e-mail nunca sai ou reentra na caixa de entrada.)
- Pesquisa em que você pode confiar. O índice de pesquisa do Gmail pode responder
is:unreada partir de um estado de leitura desatualizado — medido em uma caixa de entrada real: 87% dos resultados já haviam sido lidos; em outra, zero. Você não consegue saber em qual caixa de entrada está sem olhar, entãosearchreverifica cada resultado contra seus rótulos ao vivo. Paginado viapageToken/nextPageToken. - Operações em massa que escalam.
bulk_modifyarquiva/rotula tudo que corresponde a uma consulta a 1000 mensagens por requisição de API — com relatório de sucesso parcial por bloco em vez de tudo-ou-nada. A varredura de soneca usa o mesmo caminho em lote. - Saídas estruturadas. Cada ferramenta declara um
outputSchemae retornastructuredContentvalidados junto com texto JSON cercado — sem adivinhação de parsing para clientes. - Pequena superfície de ataque. Sem ferramentas de envio (sem caminho de exfiltração para e-mail injetado por prompt),
modo somente leitura opcional, sem telemetria, sem portas abertas por padrão, cercamento de download seguro contra symlink,
saída com cerca contra injeção. Uma exceção deliberada:
unsubscribe/bulk_unsubscribe(nível gerenciar) contatam o endpoint de opt-out nomeado no cabeçalho da própria mensagem — o único host não-Google que o mailwarden alcança, e uma implantação de nívelreadnão faz nenhuma requisição de saída. Detalhes em Segurança e privacidade e Cancelamento de inscrição. - Correto com e-mail do mundo real. Cabeçalhos RFC 2047 decodificados (
=?UTF-8?B?…?=→ texto legível), corpos decodificados no charset declarado (sem mojibake para e-mail ISO-8859-1/Shift_JIS), 429/5xx repetidos com backoff exponencial.
Por quê
Conectores que sincronizam ou armazenam em cache sua caixa de entrada podem ficar atrás dela — e até o próprio índice de pesquisa do Gmail às vezes é impreciso (veja abaixo). mailwarden fala direto com a API do Gmail ao vivo (sem snapshot em cache) e reverifica o que o índice retorna, então o que você vê é o que realmente está lá. É uma camada genérica de capacidade do Gmail — mantenha suas próprias regras/lógica no seu cliente de IA, não no servidor.
search vai um passo além da API bruta: o índice threads.list do Gmail pode responder operadores de estado de leitura a partir de uma cópia desatualizada desse estado, então is:unread retorna e-mail que você já leu há muito tempo — em uma caixa de entrada medida, a maioria do que retornou. Como cada resultado é buscado ao vivo de qualquer forma, search re-checa os predicados inequívocos (is:unread/is:read/is:starred/in:inbox/category:…, com negação) contra os rótulos verdadeiros de cada tópico e descarta os falsos positivos do índice.
Comparado a outros servidores MCP do Gmail
A maioria dos servidores MCP do Gmail cobre a mesma superfície de leitura/rotulação/envio. Duas capacidades ainda são exclusivas do mailwarden entre servidores MCP do Gmail (soneca do lado da caixa de entrada, reverificação de pesquisa), e uma omissão deliberada é um recurso de segurança, não uma lacuna. O servidor oficial do Google também é mais estreito do que parece: somente rascunho, e sem lixeira, filtros ou cancelamento de inscrição.
| Capacidade | mailwarden | taylorwilsdon | Google oficial | mcpemails.com |
|---|---|---|---|---|
| Soneca do lado da caixa de entrada — arquivar agora, ressurgir na caixa de entrada em uma data/hora ou predefinição | ✅ | — | — | — |
| Reverificação de resultados de pesquisa — descarta os falsos positivos do índice contra rótulos ao vivo | ✅ | — | — | — |
| Varredura / em massa sobre uma consulta — uma ação em todos os tópicos correspondentes | ✅ 1000/req, sucesso parcial | — | — | ⚠️ organização em massa (sem reverificação de consulta) |
| Cancelamento de inscrição — visão geral por remetente + opt-out de um clique RFC 8058, sem necessidade de escopo de envio | ✅ | — | — | — |
| Sem ferramentas de envio — por design — um e-mail injetado por prompt não tem caminho de exfiltração | ✅ sem composição alguma | ❌ envia | ⚠️ somente rascunho | ❌ envia |
| Níveis de ferramentas com privilégio mínimo — escopos OAuth derivados das ferramentas que você habilita | ✅ | — | ⚠️ divisão de escopo | ⚠️ chaves com escopo |
| Criptografia de token em repouso (opcional) | ✅ AES-256-GCM | ✅ | n/a (hospedado) | ✅ AES-256-GCM |
| Executa totalmente local — sem cópia em nuvem do seu e-mail | ✅ | ✅ | ❌ hospedado | ❌ SaaS |
Saídas estruturadas — cada ferramenta declara um outputSchema | ✅ | — | — | — |
Snapshot de agosto de 2026, a partir dos docs/repo públicos de cada projeto; — = não oferecido / não documentado. A capacidade de envio é listada como uma propriedade de segurança: a falta dela no mailwarden é intencional (veja Segurança e privacidade).
O fosso não é nenhuma linha única — é soneca + reverificação ao vivo juntas: uma camada real de fluxo de trabalho de caixa de entrada que age no estado atual da caixa, não em um snapshot em cache. Onde concorrentes alcançaram (ações em massa, criptografia em repouso) está anotado honestamente acima.
Por que a reverificação importa — um caso concreto
Peça a um assistente para "arquivar o e-mail promocional não lido que já pulou minha caixa de entrada" e ele alcançará a consulta óbvia, category:updates is:unread -in:inbox. Um servidor que confia no índice do Gmail agora arquiva tópicos que você já tinha lido — e-mail que você nunca quis tocar, perdido em uma ação em massa que você não pode reverter facilmente.
Medido, não afirmado. Uma caixa de entrada real (~70.000 mensagens), 15.08.2026, somente leitura:
| Consulta | Resultados do índice | Realmente não lidos | Falsos positivos |
|---|---|---|---|
category:updates is:unread | 131 | 17 | 87% |
category:updates is:unread -in:inbox | 128 | 14 | 89% |
is:unread -in:inbox | 235 | 99 | 58% |
O índice não está ignorando o predicado — a mesma consulta sem is:unread retorna 800+ tópicos, então ele está sendo aplicado. É aplicado contra um estado de leitura que o índice não acompanhou: e-mail lido há semanas ainda conta como não lido lá. Um tópico retornado carregava um único rótulo, SENT. E não é uma peculiaridade de combinações exóticas de operadores: a consulta mais simples das três também mostra isso — com a menor proporção (58%) mas o mais tópicos errados em termos absolutos (136).
Uma segunda caixa de entrada, medida da mesma forma no mesmo dia, não se desviou nada — zero resultados de índice bruto para is:unread, embora seja marcada como lida através da API muitas vezes por dia. Então isso é uma propriedade de uma caixa de entrada, não do Gmail em todos os lugares. O que as separa está em aberto: elas diferem em volume (cerca de três ordens de magnitude) e idade, e a segunda está faltando algo mais básico — nenhum tópico nela foi arquivado enquanto ainda não lido, que é a única forma que um estado de leitura desatualizado pode aparecer. Então não é um contra-exemplo para nenhuma causa particular; é uma caixa de entrada sem o candidato.
Que é exatamente o ponto: um servidor não pode saber em que tipo de caixa de entrada está. A reverificação não custa nada onde nada se desvia, e salva você onde se desvia — na medição acima, cada tópico que search descartou era genuinamente lido, e descartou nenhum e-mail genuinamente não lido.
Onde não é grátis: as ferramentas em massa. search reverifica porque busca cada resultado de qualquer forma; bulk_modify (e a varredura applyToExisting do create_filter) é dimensionada em milhares de mensagens, onde uma busca por resultado é uma ordem de custo diferente. Essas agem no que o índice retorna — então agora relatam unverifiedPredicates, as condições da sua consulta que foram aceitas pela palavra do índice (+UNREAD, -INBOX, …). Vazio significa que não havia nada para desconfiar. Não vazio e o resultado precisa ser preciso quanto ao estado de leitura? Resolva o conjunto com search primeiro e aja nesses IDs de tópico. Um dryRun não fecha essa lacuna: ele relê o mesmo índice, então confirma quão grande é o conjunto, nunca se está correto.
mailwarden busca cada resultado ao vivo de qualquer forma, então search re-checa os predicados inequívocos (is:unread, is:read, in:inbox, category:…, com negação) contra os rótulos verdadeiros de cada tópico e descarta os falsos positivos do índice antes que qualquer ferramenta os veja. A ação em massa então roda exatamente no conjunto que você pediu. Essa é a diferença entre agir no que o Gmail indexou e agir no que realmente está na caixa de entrada agora — e é por isso que soneca/varredura são seguras para entregar a um assistente: a varredura ressurgue apenas tópicos cuja soneca está genuinamente vencida, verificada contra rótulos ao vivo em tempo de execução.
Veja você mesmo — sem precisar de conta Gmail. De um clone do repositório (a demo é um script de verificação apenas do repositório, não parte do pacote npm):
git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node scripts/demo-reverify.mjs
Há um segundo script ao lado, node scripts/probe-reverify.mjs, que mede a mesma coisa na sua caixa de entrada em vez de uma falsa — somente leitura, apenas metadados (sem assunto, remetente ou corpo buscado), imprimindo contagens e nomes de rótulos. É assim que os números acima foram produzidos, e como você pode verificar se sua caixa de entrada se desvia.
A demo dirige o search() real contra uma API Gmail falsa cujo índice está deliberadamente desatualizado (retorna um tópico lido para uma consulta is:unread, exatamente como o Gmail faz) e mostra o mailwarden descartando o falso positivo. Ela afirma o resultado, então sai com código não-zero se o comportamento regredir. O mesmo caso é travado por testes unitários em test/gmail.test.ts ("descarta falsos positivos do índice via reverificação de rótulo ao vivo").
Ferramentas
| Ferramenta | O que faz |
|---|---|
search | Sintaxe de consulta Gmail → resumos de tópicos (de/assunto/data/rótulos/trecho); predicados de estado de leitura/categoria são reverificados contra os rótulos ao vivo de cada resultado; paginado via pageToken/nextPageToken. Cada resultado carrega signals — newsletter (List-Id / List-Unsubscribe / Precedence bulk ou list), automated (Auto-Submitted, cabeçalhos de auto-resposta/supressão, remetentes estilo no-reply), calendar (parte text/calendar ou .ics), replyToMismatch (Reply-To em outro domínio que From; um subdomínio do mesmo domínio conta como o mesmo) — lidos dos cabeçalhos/MIME da primeira mensagem, sem chamada extra |
get_thread | Tópico completo: cabeçalhos, corpos texto puro + HTML, metadados de anexos |
list_labels | Todos os rótulos (sistema + usuário) |
get_profile | Endereço da conta conectada + contagens totais de mensagens/tópicos — confirme qual caixa de entrada está conectada antes de agir |
triage_digest | Visão geral estruturada de uma fatia da caixa de entrada para decisões: principais remetentes (cada um com os sinais que seus tópicos carregam), baldes de rótulo e idade, contagens de não lidos + anexos, e quantos tópicos são newsletters / automatizados / convites de calendário / incompatibilidades de reply-to — em vez de uma lista bruta de tópicos |
list_unsubscribe | Quais opções de opt-out um tópico anuncia (List-Unsubscribe) — não contata ninguém |
list_subscriptions | Uma fatia da caixa de entrada agrupada por remetente: contagens de tópicos/não lidos, o intervalo de datas em que cada um foi visto, e as opções de opt-out de cada um — uma busca de cabeçalho por remetente, não contata ninguém. sendersFound relata quantos remetentes havia antes de topN truncar a lista |
create_label | Criar um rótulo de usuário (idempotente; aninhado via Parent/Child) e retornar seu id |
modify_labels | Adicionar/remover rótulos por nome ou id — um nome desconhecido em add é criado automaticamente (arquivar = remover INBOX, ler = remover UNREAD) |
bulk_modify | Alterações de rótulo em lote para cada mensagem que corresponde a uma consulta — 1000 mensagens por requisição de API, sucesso parcial relatado por bloco (lista de IDs de thread limitada a 500, modifiedThreadCount tem o total). Atua no índice bruto, então unverifiedPredicates nomeia as condições que não pôde garantir (veja abaixo). dryRun: true resolve a consulta e relata as threads correspondentes e os rótulos que criaria, sem tocar em nada |
archive / mark_read / mark_unread | Wrappers de conveniência |
trash / untrash | Mover para / restaurar da Lixeira |
download_attachment | Salvar um anexo em um caminho local (nunca sobrescreve — colisões recebem um sufixo numérico) |
unsubscribe | Cancelamento de assinatura com um clique (RFC 8058) usando o endpoint do próprio cabeçalho da mensagem — a única ferramenta que contata um host que não é do Google (detalhes) |
bulk_unsubscribe | O mesmo para várias threads, sequencialmente e no máximo uma requisição por remetente; sucesso parcial relatado por thread. dryRun: true executa as mesmas leituras de cabeçalho e deduplicação e relata o endpoint que cada thread wouldCall — sem contatar ninguém |
snooze | Arquivar agora, reaparecer em/após uma data (YYYY-MM-DD), uma data+hora (2026-06-20 9am) ou um predefinido (tomorrow, tomorrow 9am, weekend, next week, um nome de dia da semana, in N days, in N hours) |
unsnooze | Cancelar um adiamento, voltar para a caixa de entrada agora |
list_snoozed | Todas as threads adiadas + datas de vencimento |
sweep_snoozed | Reapresentar threads cujo adiamento venceu (executar sob demanda, via cron ou o daemon); em lote, com relatório de falha parcial. dryRun: true responde "o que vence agora?" (dueLabels/dueThreads) sem ativar nada |
list_filters | Todos os filtros do Gmail (critérios + ações de rótulo); expõe qualquer endereço forward em filtros existentes para auditoria |
create_filter | Criar uma regra de triagem automática no servidor (critérios → apenas ações de rótulo; sem encaminhamento — veja abaixo). Opcionalmente applyToExisting para também varrer correspondência correspondente já na caixa de correio |
delete_filter | Excluir um filtro por id |
Todas as ferramentas declaram um outputSchema e retornam conteúdo estruturado (validado, legível por máquina)
junto com o mesmo JSON como texto cercado — os clientes nunca precisam analisar prosa.
Como funciona o adiamento (não existe snooze na API do Gmail — nós o construímos)
snooze remove INBOX e aplica um rótulo datado MCP/Snoozed/<key>, onde a chave é YYYY-MM-DD (vence o dia todo) ou YYYY-MM-DDTHHMM (vence naquele minuto local). O argumento until aceita uma data explícita, uma data+hora (2026-06-20 9am, …T17:00) ou um predefinido resolvido no servidor — today, tomorrow, weekend (próximo sábado), next week (próxima segunda), um nome de dia da semana (monday–sunday, próxima ocorrência), in N days ou in N hours — e um predefinido de data pode carregar uma hora no final (tomorrow 9am, monday 8:30), então o chamador nunca precisa calcular o momento em si. sweep_snoozed encontra rótulos vencidos e retorna essas threads para a caixa de entrada (marcadas como não lidas); um adiamento com hora desperta na primeira varredura em/após seu minuto, então a latência de despertar é igual ao seu intervalo de varredura. Execute a varredura:
- sob demanda (ferramenta
sweep_snoozed), - via cron:
mailwarden --sweep, - ou automaticamente: defina
MAILWARDEN_AUTO_SWEEP=1(varredura horária enquanto o servidor estiver em execução).
Filtros (regras persistentes de triagem automática)
create_filter configura uma regra no servidor do Gmail: correspondência que corresponde aos critérios recebe automaticamente as
ações de rótulo fornecidas — a caixa de correio continua se triando sem assistente no processo.
- Critérios:
from,to,subject,query(sintaxe completa de pesquisa do Gmail),negatedQuery,hasAttachment,excludeChatsesize+sizeComparison(smaller/larger, fornecidos juntos). Pelo menos um é obrigatório. - Ações (apenas rótulo):
addLabels/removeLabels, por nome ou id (um nome desconhecido emaddLabelsé criado automaticamente, aninhado via/). Receitas comuns: pular a caixa de entrada →removeLabels: ["INBOX"]; marcar como lido automaticamente →removeLabels: ["UNREAD"]; lixeira automática →addLabels: ["TRASH"]; estrela →addLabels: ["STARRED"]; nunca-spam →removeLabels: ["SPAM"]; arquivar sob um rótulo →addLabels: ["Receipts"]. - Correspondência existente: um filtro só atua em mensagens que chegam depois de sua criação. Passe
applyToExisting: truepara também aplicar as mesmas ações uma vez à correspondência já na caixa de correio — mailwarden constrói uma pesquisa do Gmail a partir dos critérios e executa uma modificação em massa (atémaxMessages, padrão 1000; mesma ressalva de índice não verificado quebulk_modify, e a passagem única exclui Spam/Lixeira). Isso exige pelo menos um critério positivo (from/to/subject/query/hasAttachment:true/size): uma regra apenas de exclusão (negatedQueryouhasAttachment:false) é recusada paraapplyToExistingporque corresponderia a quase toda a caixa de correio — crie tal filtro sem a flag. O resultado volta sobapplied(oqueryusado, contagensmatchedMessages/modifiedMessages/modifiedThreadCount,cappedquando o conjunto correspondente atingiumaxMessages,failedpor bloco e uma stringerrorse toda a passagem falhou); énullquandoapplyToExistingnão foi definido. O filtro é criado primeiro, então uma passagem de pendências parcial ou falha é relatada emapplied, nunca levantada — a regra permanece. - Sem encaminhamento — veja Segurança e privacidade.
- Exige o escopo
gmail.settings.basic; execute--authnovamente uma vez se você autorizou uma versão mais antiga. Não disponível no modo somente leitura.
Cancelamento de assinatura — a única requisição de saída
list_unsubscribe (nível de leitura) relata o que o remetente oferece, sem contatar ninguém. Ele lê a
mensagem mais recente que realmente carrega um cabeçalho List-Unsubscribe — uma resposta encadeada em um boletim informativo
fica no final e não anuncia nada, o que de outra forma seria lido como "esta lista não tem opção de saída".
list_subscriptions (nível de leitura) faz o mesmo em uma fatia inteira, agrupado por remetente, para que você possa ver
quem continua escrevendo e quais deles podem realmente ser deixados — uma busca de cabeçalho por remetente em vez de
por thread. unsubscribe e bulk_unsubscribe (nível de gerenciamento) agem sobre isso — e esse é o único
lugar onde mailwarden fala com um host que não é o Google, então as regras são rígidas:
- Não há parâmetro de URL. O endpoint vem do próprio cabeçalho da mensagem e de nenhum outro lugar. Um argumento de URL permitiria que um e-mail com injeção de prompt transformasse a ferramenta em um canal de exfiltração (conteúdo da caixa de correio em uma string de consulta); o cabeçalho não pode carregar dados escolhidos pelo modelo.
- Apenas o one-click RFC 8058 é executado — o remetente deve ter optado por participar via
List-Unsubscribe-Post. Um link simpleshttps:é destinado a um humano em um navegador e é devolvido, não buscado. - Cancelamentos
mailto:nunca são executados. Eles exigiriam enviar e-mail, o que mailwarden não pode fazer. O endereço é relatado para que você possa agir por conta própria. - Requisição fixa, resposta descartada. O corpo do POST é sempre
List-Unsubscribe=One-Clicke nunca é derivado de nada; o corpo da resposta é cancelado sem leitura. O que retorna ao modelo é o código de status e a URL realmente chamada — nenhum conteúdo do endpoint, então ele não pode responder com instruções. (Um redirecionamento 301/302/303 é seguido como GET, ou seja, sem corpo algum.) - Uma requisição por remetente, sequencialmente, dentro de um orçamento.
bulk_unsubscribeaceita IDs de thread (nunca uma consulta — um lote orientado por consulta dispararia uma requisição por remetente correspondente antes que alguém tivesse olhado). Threads de um remetente cuja requisição já foi enviada são relatadas comduplicateOfe não custam uma segunda requisição: duas threads de uma lista compartilham um cancelamento, e chamá-lo duas vezes apenas confirma seu endereço duas vezes. Um remetente só é registrado quando uma requisição realmente alcançou um endpoint, então uma recusa ou uma conexão perdida ainda deixa a próxima thread com sua própria tentativa — e se a thread pulada anuncia um endpoint diferente, o motivo diz isso, já que um remetente pode operar várias listas. Limitado a 25 threads e 60 segundos por chamada; o que o orçamento não cobrir volta comoskippedOutOfTimeem vez de ser silenciosamente desfeito. Nada disso pode ser revertido, e é por isso que os três limites existem. - Proteções SSRF. Apenas https, apenas porta padrão, sem credenciais na URL, e cada salto — incluindo
redirecionamentos, seguidos no máximo 3 vezes — deve resolver exclusivamente para endereços globalmente alcançáveis. A
verificação analisa cada endereço em seus bytes e o compara com o registro de propósitos especiais da IANA, então
cada grafia do mesmo endereço recebe o mesmo veredito (
::1e0:0:0:0:0:0:0:1igualmente); um endereço que não pode ser analisado é recusado. A resolução de DNS e todos os saltos compartilham um orçamento de 10 segundos. Não é à prova de rebinding (fetchresolve novamente ao conectar) — veja SECURITY.md; o que sobrevive a essa lacuna é um POST cego cuja resposta nunca é lida.
Verifique com seu próprio e-mail antes de confiar. A partir de um clone do repositório (apenas repositório, não no
pacote npm), após npm run build e mailwarden --auth:
node scripts/probe-unsubscribe.mjs --vet # category:promotions, 25 threads
node scripts/probe-unsubscribe.mjs "from:substack.com" --max 50 --vet
Ele imprime cada cabeçalho List-Unsubscribe real ao lado do que o analisador fez dele, e --vet também
executa o endpoint pela verificação de URL e pela proteção de endereço — então você vê tanto se o analisador
entendeu o cabeçalho quanto se as proteções teriam permitido esse cancelamento. Estritamente
somente leitura: nenhuma requisição é feita a um remetente, e nada na caixa de correio muda.
O que ele não pode desfazer: a requisição informa ao remetente que seu endereço está ativo. Um remetente que ignora seu próprio
cancelamento está além do alcance de qualquer cliente — combine unsubscribe com create_filter ou trash para esses casos.
Não oferecer uma opção automatizável é relatado como unsubscribed:false com as alternativas, não como um
erro. Uma implantação apenas com read recebe list_unsubscribe e list_subscriptions, e nunca faz a
requisição.
Segurança e privacidade
Para o modelo de ameaças completo — limite de confiança, mitigações por ameaça, não-objetivos explícitos e como relatar uma vulnerabilidade — veja SECURITY.md. Os destaques:
-
Sem telemetria. Nada faz contato com a origem — sem analytics, sem relatórios de falha, sem rastreamento.
-
Sem portas abertas por padrão. Apenas stdio. O listener opcional
--httpvincula-se a127.0.0.1(não à LAN) e recusa iniciar sem um token bearerMAILWARDEN_TOKEN— definaMAILWARDEN_ALLOW_NO_TOKEN=1para substituir em uma rede confiável e isolada. Em um vínculo de loopback, também valida o cabeçalhoHost(defesa contra DNS rebinding). Para hospedagem remota, definaMAILWARDEN_HOSTe coloque TLS na frente. -
Sem ferramentas de envio — por design. mailwarden não pode compor, responder ou encaminhar. Uma instrução injetada por prompt dentro de um e-mail não tem caminho de exfiltração através deste servidor.
create_filtersegue a mesma regra: pode rotular, arquivar, enviar para a lixeira, estrelar ou marcar correspondência, mas nunca cria um filtro de encaminhamento (que seria um caminho de exfiltração).list_filtersainda expõe qualquer filtro de encaminhamento já existente na conta, para que você possa identificá-lo. Isso se mantém porque tal ferramenta não existe e nenhuma pode ser registrada em tempo de execução; para a variante mais forte, onde o Google se recusa a enviar em vez de mailwarden recusar, veja Modo somente leitura abaixo. -
Um host de saída, sem URL escolhida pelo modelo. A ferramenta
unsubscribeé o único caminho de código que contata um host que não é do Google. Seu endpoint é lido do cabeçalhoList-Unsubscribeda mensagem — nunca a partir de um argumento de ferramenta — o corpo da requisição é fixo e o corpo da resposta é descartado, portanto não pode se tornar um canal de dados. Apenas https/porta padrão, redirecionamentos revalidados, e qualquer salto que resolva para um endereço privado, loopback, link-local ou de metadados é recusado. Veja Cancelamento de inscrição. -
Níveis de ferramentas (divulgação progressiva + menor escopo).
MAILWARDEN_TOOLSanuncia apenas os níveis que você nomeia —read(as ferramentas de leitura),manage(mutações de caixa de correio, adiamento, downloads),filters(CRUD de filtros no servidor, o único nível cujas ferramentas precisam degmail.settings.basic). O padrão são todos os três; por exemplo,read,managedá uma superfície de triagem completa sem gerenciamento de filtros. Os escopos OAuth solicitados em--authsão derivados dos níveis habilitados — uma implantaçãoreadpede apenasgmail.readonly, egmail.settings.basicé solicitado somente quando o nívelfiltersestá ativo. E as ferramentas de filtro são ocultadas automaticamente quando o token armazenado não carregagmail.settings.basic(por exemplo, um token autorizado antes de você habilitar o nível) — execute novamente--authpara concedê-lo. Tokens mais antigos sem escopo registrado são anunciados como antes, com a mensagem de escopo insuficiente em tempo de execução como fallback. -
Modo somente leitura. Defina
MAILWARDEN_READONLY=1(abreviação deMAILWARDEN_TOOLS=read) e apenas as ferramentas de leitura (search,get_thread,list_labels,list_snoozed,get_profile,triage_digest,list_unsubscribe,list_subscriptions) são registradas — nada que possa alterar a caixa de correio ou gravar arquivos é sequer anunciado aos clientes (as ferramentas de filtro, que precisam do escopo mais amplogmail.settings.basic, também são excluídas). Recomendado para implantações compartilhadas/HTTP que apenas fazem triagem. É também o único nível cuja propriedade de não envio o Google impõe: ele mantém um tokengmail.readonly, que os endpoints de envio do Gmail rejeitam de imediato.manageprecisa degmail.modify, e o Gmail aceita esse escopo para envio — mailwarden simplesmente não expõe nenhuma ferramenta que o faça. Portanto, uma implantaçãoreadnão poderia enviar mesmo se este binário fosse substituído; umamanagenão pode enviar porque não há nada para chamar. (Não há escopo de gravação sem envio para alternar — veja SECURITY.md, ameaça 1.) -
Downloads isolados. Com
MAILWARDEN_DOWNLOAD_DIRdefinido, as gravações de anexos são confinadas a esse diretório (canonicalizado via realpath, ciente de symlinks) e nunca sobrescrevem um arquivo existente. -
Isolamento de conteúdo não confiável. Cada resultado de ferramenta é envolvido em marcadores
<untrusted-tool-output>e removido de caracteres invisíveis/override BiDi, para que os clientes possam distinguir conteúdo de e-mail citado de instruções. -
API ao vivo, sem cópia. Nenhum espelho de caixa de correio ou índice de busca é armazenado em qualquer lugar. O único estado local é seu token OAuth em
~/.mailwarden/. -
Criptografia opcional do token em repouso.
token.jsonguarda um token de atualização; no disco, ele é protegido apenas pormode 0o600(um no-op no Windows). DefinaMAILWARDEN_TOKEN_PASSPHRASEcom uma frase secreta e o token será armazenado criptografado com AES-256-GCM (chave derivada de scrypt), de modo que uma cópia do arquivo — um backup, uma pasta sincronizada, outra máquina — seja inútil sem a frase secreta. Execute novamentemailwarden --authuma vez após defini-la para criptografar o token existente. Observe o limite: isso defende contra roubo de arquivo, não contra malware executando como seu usuário (que também pode ler a frase secreta do ambiente).
Quick start
claude mcp add mailwarden -- npx -y mailwarden
Essa é a instalação completa — npx baixa e executa o pacote publicado, sem etapa de clone ou build. Você só precisa das credenciais OAuth do Google uma vez (abaixo).
Configuração
Primeira vez configurando um app OAuth do Google? Siga o guia de configuração passo a passo — ele percorre o Google Cloud Console com caminhos de clique exatos, explica a tela de "app não verificado" e cobre a armadilha que faz os tokens expirarem após 7 dias. A versão resumida:
- Google Cloud: crie um projeto → habilite a API Gmail → configure a tela de consentimento OAuth e publique-a em Produção (no status Teste, o Google expira tokens de atualização após 7 dias) → crie um ID de cliente OAuth do tipo Aplicativo de desktop → baixe-o como
credentials.json. - Coloque
credentials.jsonem~/.mailwarden/(ou definaMAILWARDEN_CREDENTIALS=/path/to/credentials.json). - Autorize uma vez — abre um navegador, armazena um token de atualização em
~/.mailwarden/token.json:
Escopos solicitados:npx -y mailwarden --authgmail.modify(leitura + rótulo/arquivar/lixeira) egmail.settings.basic(somente gerenciamento de filtros). Se você autorizou uma versão antes de os filtros existirem, execute novamente--authuma vez para conceder o escopo adicionado. Para manter um token que o próprio Gmail se recusa a usar para envio, autorize comMAILWARDEN_TOOLS=read— veja Modo somente leitura acima. - Verifique a configuração a qualquer momento com o doctor integrado:
Ele verificanpx -y mailwarden --checkcredentials.json, se existe um token (e se está criptografado), se os escopos concedidos cobrem seus níveis habilitados, e faz uma chamada Gmail ao vivo para provar que o token ainda funciona — imprimindo uma correção concreta para qualquer problema e saindo com código não zero se houver (útil em CI/health checks). Diagnostica as armadilhas comuns: arquivo de credenciais ausente/wrong, nunca autorizado, um token criptografado semMAILWARDEN_TOKEN_PASSPHRASE, um escopo ausente, ou a expiração do token de consentimento "Teste" de 7 dias.
Conectar
Claude Code (stdio local):
claude mcp add mailwarden -- npx -y mailwarden
Plugin Claude Code — o mesmo servidor mais uma skill /mailwarden:setup que guia você pela
configuração OAuth e diagnostica uma quebrada. A raiz do repositório é o plugin (.claude-plugin/plugin.json), então
a partir de um clone:
claude --plugin-dir /path/to/mailwarden
Ele está submetido ao marketplace da comunidade da Anthropic; uma vez listado, /plugin marketplace add anthropics/claude-plugins-community
e depois /plugin install mailwarden@claude-community fazem o mesmo sem clone. O plugin executa a superfície
completa de ferramentas — para um nível mais restrito (MAILWARDEN_TOOLS=read) ou uma segunda conta, use claude mcp add com
o env que você quiser (veja Config e Múltiplas contas).
Claude Desktop — adicione a claude_desktop_config.json:
{
"mcpServers": {
"mailwarden": { "command": "npx", "args": ["-y", "mailwarden"] }
}
}
Ou instale o bundle MCPB (mailwarden-<version>.mcpb, anexado aos
lançamentos do GitHub a partir de 0.10.0) como uma extensão do Desktop — Configurações →
Extensões → Instalar extensão… — o mesmo servidor, autocontido em tempo de execução (sem npx; o Claude
Desktop traz o runtime Node), com os níveis de ferramentas como uma configuração. O bundle é construído a partir do
pacote npm empacotado (mesmo conjunto de arquivos do publicado; npm run mcpb, verificado em CI: validado, descompactado e iniciado)
e é o mesmo conjunto de arquivos que o Smithery distribui. O npx -y mailwarden --auth único ainda se aplica
(Node necessário uma vez para isso) — o bundle lê o mesmo token ~/.mailwarden/.
Smithery — listado como csitte/mailwarden, que serve
esse bundle:
npx -y @smithery/cli install csitte/mailwarden --client claude # local stdio entry in the client's config
Observe qual dos dois caminhos do Smithery você segue. A instalação acima escreve uma entrada de servidor local simples: o
processo, seu token e seu e-mail permanecem na sua máquina, exatamente como com npx. Adicioná-lo à
toolbox do Smithery (smithery mcp add) também executa o bundle localmente, mas retransmite o tráfego de ferramentas
através do gateway do Smithery para que um cliente remoto possa alcançá-lo — o conteúdo da caixa de correio nessas respostas então
passa por um terceiro. Isso é uma propriedade do gateway, não do mailwarden; se você quiser a
garantia de sem terceiros, use a instalação local, o pacote npm, ou o .mcpb da página de lançamentos.
Remoto (Streamable HTTP) — para um VPS / conector personalizado do claude.ai:
# Loopback + token required by default. For real hosting, bind outward and keep the token:
MAILWARDEN_TOKEN=<secret> MAILWARDEN_HOST=0.0.0.0 npx -y mailwarden --http # :8787/mcp
Depois no claude.ai: Configurações → Conectores → Adicionar conector personalizado → sua URL https://your-host/mcp. No Claude Code: claude mcp add --transport http mailwarden https://your-host/mcp.
Múltiplas contas
Um app OAuth (um credentials.json) pode autorizar várias contas Gmail. Cada conta mantém seu
próprio token de atualização em um arquivo separado, selecionado por MAILWARDEN_ACCOUNT:
mailwarden --auth --account work # stores token.work.json
mailwarden --auth --account personal # stores token.personal.json
Execute-os lado a lado registrando o servidor uma vez por conta, cada um com seu próprio
MAILWARDEN_ACCOUNT. Cada instância é totalmente isolada — seu próprio token, seus próprios escopos concedidos, sua
própria superfície de ferramentas — então nada pode agir na caixa de correio errada:
{
"mcpServers": {
"gmail-work": { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work" } },
"gmail-personal": { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "personal" } }
}
}
Os nomes de conta são insensíveis a maiúsculas/minúsculas — eles se tornam nomes de arquivo, então Work e work seriam o
mesmo arquivo no Windows/macOS. mailwarden os converte para minúsculas (--account Work → token.work.json) para que um
nome sempre mapeie para exatamente uma caixa de correio.
Qual arquivo --auth escreve depende apenas de --account / MAILWARDEN_ACCOUNT — nunca da
conta que você escolhe no navegador. Autorizar uma segunda caixa de correio sem --account iria
portanto direto para o arquivo de token da primeira, então --auth verifica primeiro e recusa
em vez de substituir o token de outra caixa de correio; --force o substitui deliberadamente. Os dois controles
não são intercambiáveis: MAILWARDEN_ACCOUNT é o que serve para várias caixas de correio a partir de um diretório de configuração
(ele escolhe token.<name>.json), enquanto MAILWARDEN_DIR move o diretório inteiro —
útil para manter configurações totalmente separadas, mas não dá a você uma segunda conta dentro de uma.
npm run auth de um clone do repositório não passa nenhum, ou seja, sempre atende a conta padrão.
mailwarden --check mostra a conta ativa e lista as outras que encontra. Sem
MAILWARDEN_ACCOUNT definido, tudo usa o padrão token.json exatamente como antes — isso é totalmente
compatível com versões anteriores.
A partir do código-fonte
git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node dist/index.js --auth
Config (env)
| Var | Significado |
|---|---|
MAILWARDEN_DIR | diretório de configuração (padrão ~/.mailwarden) |
MAILWARDEN_CREDENTIALS | caminho para credentials.json |
MAILWARDEN_ACCOUNT | seleciona uma conta nomeada (seu token é token.<name>.json; nomes são convertidos para minúsculas); não definido = o padrão token.json. Veja Múltiplas contas |
MAILWARDEN_TOKEN_PASSPHRASE | frase secreta → criptografa token.json em repouso (AES-256-GCM); execute novamente --auth após definir |
MAILWARDEN_AUTO_SWEEP | 1 → varredura de adiamento na inicialização + a cada hora enquanto estiver em execução (escreve rótulos — precisa do escopo manage/gmail.modify; uma concessão apenas com read não pode varrer) |
MAILWARDEN_DOWNLOAD_DIR | restringe download_attachment a este diretório (fortemente recomendado para hospedagem HTTP) |
MAILWARDEN_READONLY | 1 → registra apenas as ferramentas de leitura (search/get_thread/list_labels/list_snoozed/get_profile/triage_digest/list_unsubscribe/list_subscriptions). Abreviação de MAILWARDEN_TOOLS=read |
MAILWARDEN_TOOLS | níveis de ferramentas separados por vírgula para anunciar: read, manage, filters (padrão: todos). Também deriva os escopos OAuth solicitados em --auth. Ex.: read,manage remove as ferramentas de filtro e seu escopo gmail.settings.basic |
MAILWARDEN_DEBUG | 1 → imprime erros completos com stack traces em vez de uma mensagem de uma linha (para relatórios de bugs) |
PORT | porta HTTP (padrão 8787) |
MAILWARDEN_HOST | endereço de bind HTTP (padrão 127.0.0.1; defina por exemplo 0.0.0.0 para hospedagem remota) |
MAILWARDEN_TOKEN | token bearer para o endpoint HTTP — obrigatório para --http a menos que sobrescrito |
MAILWARDEN_ALLOW_NO_TOKEN | 1 → permite --http sem token (apenas redes confiáveis/isoladas) |
MAILWARDEN_ALLOWED_HOSTS | valores extras de host:port separados por vírgula aceitos pela allowlist de loopback Host |
Status
Funcionando e usado na automação diária de caixas de correio. Ferramentas principais do Gmail + adiamento implementados contra googleapis, cobertos por uma suíte vitest (769 testes — npm run coverage). Versão atual: veja o badge npm acima, o changelog, ou os lançamentos. PRs são bem-vindos.
Licença
MIT © C.Sitte Softwaretechnik