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

npm license Node Website Smithery Available on CodeGuilds

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:unread a 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ão search reverifica cada resultado contra seus rótulos ao vivo. Paginado via pageToken/nextPageToken.
  • Operações em massa que escalam. bulk_modify arquiva/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 outputSchema e retorna structuredContent validados 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ível read nã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.

CapacidademailwardentaylorwilsdonGoogle oficialmcpemails.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-GCMn/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:

ConsultaResultados do índiceRealmente não lidosFalsos positivos
category:updates is:unread1311787%
category:updates is:unread -in:inbox1281489%
is:unread -in:inbox2359958%

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

FerramentaO que faz
searchSintaxe 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 signalsnewsletter (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_threadTópico completo: cabeçalhos, corpos texto puro + HTML, metadados de anexos
list_labelsTodos os rótulos (sistema + usuário)
get_profileEndereço da conta conectada + contagens totais de mensagens/tópicos — confirme qual caixa de entrada está conectada antes de agir
triage_digestVisã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_unsubscribeQuais opções de opt-out um tópico anuncia (List-Unsubscribe) — não contata ninguém
list_subscriptionsUma 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_labelCriar um rótulo de usuário (idempotente; aninhado via Parent/Child) e retornar seu id
modify_labelsAdicionar/remover rótulos por nome ou id — um nome desconhecido em add é criado automaticamente (arquivar = remover INBOX, ler = remover UNREAD)
bulk_modifyAlteraçõ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_unreadWrappers de conveniência
trash / untrashMover para / restaurar da Lixeira
download_attachmentSalvar um anexo em um caminho local (nunca sobrescreve — colisões recebem um sufixo numérico)
unsubscribeCancelamento 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_unsubscribeO 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
snoozeArquivar 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)
unsnoozeCancelar um adiamento, voltar para a caixa de entrada agora
list_snoozedTodas as threads adiadas + datas de vencimento
sweep_snoozedReapresentar 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_filtersTodos os filtros do Gmail (critérios + ações de rótulo); expõe qualquer endereço forward em filtros existentes para auditoria
create_filterCriar 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_filterExcluir 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 (mondaysunday, 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, excludeChats e size + sizeComparison (smaller/larger, fornecidos juntos). Pelo menos um é obrigatório.
  • Ações (apenas rótulo): addLabels / removeLabels, por nome ou id (um nome desconhecido em addLabels é 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: true para 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 que bulk_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 (negatedQuery ou hasAttachment:false) é recusada para applyToExisting porque corresponderia a quase toda a caixa de correio — crie tal filtro sem a flag. O resultado volta sob applied (o query usado, contagens matchedMessages/modifiedMessages/modifiedThreadCount, capped quando o conjunto correspondente atingiu maxMessages, failed por bloco e uma string error se toda a passagem falhou); é null quando applyToExisting não foi definido. O filtro é criado primeiro, então uma passagem de pendências parcial ou falha é relatada em applied, nunca levantada — a regra permanece.
  • Sem encaminhamento — veja Segurança e privacidade.
  • Exige o escopo gmail.settings.basic; execute --auth novamente 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 simples https: é 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-Click e 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_unsubscribe aceita 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 com duplicateOf e 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 como skippedOutOfTime em 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 (::1 e 0:0:0:0:0:0:0:1 igualmente); 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 (fetch resolve 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 --http vincula-se a 127.0.0.1 (não à LAN) e recusa iniciar sem um token bearer MAILWARDEN_TOKEN — defina MAILWARDEN_ALLOW_NO_TOKEN=1 para substituir em uma rede confiável e isolada. Em um vínculo de loopback, também valida o cabeçalho Host (defesa contra DNS rebinding). Para hospedagem remota, defina MAILWARDEN_HOST e 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_filter segue 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_filters ainda 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çalho List-Unsubscribe da 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_TOOLS anuncia 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 de gmail.settings.basic). O padrão são todos os três; por exemplo, read,manage dá uma superfície de triagem completa sem gerenciamento de filtros. Os escopos OAuth solicitados em --auth são derivados dos níveis habilitados — uma implantação read pede apenas gmail.readonly, e gmail.settings.basic é solicitado somente quando o nível filters está ativo. E as ferramentas de filtro são ocultadas automaticamente quando o token armazenado não carrega gmail.settings.basic (por exemplo, um token autorizado antes de você habilitar o nível) — execute novamente --auth para 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 de MAILWARDEN_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 amplo gmail.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 token gmail.readonly, que os endpoints de envio do Gmail rejeitam de imediato. manage precisa de gmail.modify, e o Gmail aceita esse escopo para envio — mailwarden simplesmente não expõe nenhuma ferramenta que o faça. Portanto, uma implantação read não poderia enviar mesmo se este binário fosse substituído; uma manage nã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_DIR definido, 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.json guarda um token de atualização; no disco, ele é protegido apenas por mode 0o600 (um no-op no Windows). Defina MAILWARDEN_TOKEN_PASSPHRASE com 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 novamente mailwarden --auth uma 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:

  1. 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.
  2. Coloque credentials.json em ~/.mailwarden/ (ou defina MAILWARDEN_CREDENTIALS=/path/to/credentials.json).
  3. Autorize uma vez — abre um navegador, armazena um token de atualização em ~/.mailwarden/token.json:
    npx -y mailwarden --auth
    
    Escopos solicitados: gmail.modify (leitura + rótulo/arquivar/lixeira) e gmail.settings.basic (somente gerenciamento de filtros). Se você autorizou uma versão antes de os filtros existirem, execute novamente --auth uma vez para conceder o escopo adicionado. Para manter um token que o próprio Gmail se recusa a usar para envio, autorize com MAILWARDEN_TOOLS=read — veja Modo somente leitura acima.
  4. Verifique a configuração a qualquer momento com o doctor integrado:
    npx -y mailwarden --check
    
    Ele verifica credentials.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 sem MAILWARDEN_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 Worktoken.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)

VarSignificado
MAILWARDEN_DIRdiretório de configuração (padrão ~/.mailwarden)
MAILWARDEN_CREDENTIALScaminho para credentials.json
MAILWARDEN_ACCOUNTseleciona 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_PASSPHRASEfrase secreta → criptografa token.json em repouso (AES-256-GCM); execute novamente --auth após definir
MAILWARDEN_AUTO_SWEEP1 → 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_DIRrestringe download_attachment a este diretório (fortemente recomendado para hospedagem HTTP)
MAILWARDEN_READONLY1 → 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_TOOLSní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_DEBUG1 → imprime erros completos com stack traces em vez de uma mensagem de uma linha (para relatórios de bugs)
PORTporta HTTP (padrão 8787)
MAILWARDEN_HOSTendereço de bind HTTP (padrão 127.0.0.1; defina por exemplo 0.0.0.0 para hospedagem remota)
MAILWARDEN_TOKENtoken bearer para o endpoint HTTP — obrigatório para --http a menos que sobrescrito
MAILWARDEN_ALLOW_NO_TOKEN1 → permite --http sem token (apenas redes confiáveis/isoladas)
MAILWARDEN_ALLOWED_HOSTSvalores 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