Nettoyage de 7 700 mails depuis le terminal avec MCP, Gmail API et Microsoft Graph

58 000 mails non lus sur Outlook. 338 spams sur Gmail. Des newsletters de 2019, des notifications BitGo d'un wallet que j'ai fermé, des relances Acadomia pour des cours de maths que je n'ai jamais donnés, et quelque part au milieu, probablement des trucs importants. Le plan initial : "je ferai le tri ce week-end". Ce week-end-là remontait à 2023.

L'idée : brancher MCP (Model Context Protocol) sur Gmail et Outlook pour que Claude Code fasse le tri depuis le terminal. Pas de clics dans les UIs web, pas de sélection manuelle. Des filtres serveur-side, des désabonnements automatiques, et un repo chiffré pour réinstaller le tout sur n'importe quelle machine. Résultat après 4 heures : ~7 700 mails traités, ~50 filtres créés, ~28 désabonnements automatiques.

Le setup MCP : deux serveurs, deux mondes

Le Model Context Protocol permet à Claude Code d'appeler des outils externes — ici, les APIs Gmail et Outlook — via des serveurs MCP qui exposent des "tools" standardisés. Deux serveurs, deux expériences très différentes.

Gmail : le MCP natif de claude.ai (celui intégré dans l'interface web) est volontairement bridé côté Anthropic : pas d'accès au dossier Spam, pas de création de filtres. Utile pour lire ses mails, inutile pour automatiser. Le MCP communautaire @gongrzhe/server-gmail-autoauth-mcp expose tout : create_filter, batch_modify_emails, batch_delete_emails, list_filters. Setup : un projet Google Cloud avec OAuth desktop, un npx qui lance le flow d'auth dans le browser, et un refresh_token sauvegardé dans ~/.gmail-mcp/credentials.json.

Outlook : le MCP ryaker/outlook-mcp est un projet communautaire qui wrappe Microsoft Graph. Fonctionnel, mais avec deux bugs à patcher avant de pouvoir s'en servir sérieusement.

Azure AD : le piège OAuth qui fait perdre 45 minutes

Premier réflexe pour l'app registration Azure : plateforme "Public client/native", comme le disent tous les tutos pour les apps CLI. Erreur immédiate au token exchange :

AADSTS90023: Public clients can't send a client secret

Le MCP Outlook envoie un client_secret en mode confidential, mais Azure interprète la plateforme "Public client" comme PKCE-only. Les deux sont incompatibles.

Fix : supprimer la plateforme "Public client", ajouter "Web" avec le même redirect URI, et désactiver "Autoriser les flux clients publics" dans les Settings de l'app. Si on oublie cette dernière case, Azure continue à traiter l'app comme publique même avec une plateforme Web.

Deuxième piège, plus vicieux : pour un compte personnel Microsoft (MSA, type @hotmail.com ou @outlook.com), le tenant dans l'URL OAuth doit être consumers :

https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize

Et pas le directory ID que le portail Azure affiche par défaut dans l'overview de l'app. Sinon : AADSTS50020 — "user account from identity provider does not exist in tenant". Le message d'erreur ne mentionne pas le tenant, évidemment.

Patcher le MCP Outlook : scopes et env vars

Une fois l'OAuth fonctionnel, deux problèmes dans le code du MCP Outlook lui-même :

Bug 1 — Scopes trop étroits. Le serveur d'auth hardcode Mail.Read, Calendars.Read, Contacts.Read. Pour créer des rules de tri automatique sur Outlook, il faut Mail.ReadWrite et MailboxSettings.ReadWrite. Sans le second, l'API Graph retourne un 403 silencieux sur POST /me/mailFolders/inbox/messageRules. Patch d'une ligne dans outlook-auth-server.js.

Bug 2 — Noms d'env vars incohérents. Le serveur d'auth attend MS_CLIENT_ID / MS_CLIENT_SECRET. Le runtime attend OUTLOOK_CLIENT_ID / OUTLOOK_CLIENT_SECRET. Le .env doit setter les deux paires, ou il faut passer les variables via claude mcp add -e KEY=value au scope user.

Deux bugs, deux patchs triviaux — mais sans eux, rien ne marche et les messages d'erreur ne pointent pas vers la cause.

RFC 8058 : le désabonnement instantané que personne n'utilise

C'est la découverte la plus intéressante de toute la session. La RFC 8058 (2017) définit un mécanisme de désabonnement en un seul HTTP POST. Le header List-Unsubscribe-Post: List-Unsubscribe=One-Click est présent dans le mail, et il suffit de poster sur l'URL indiquée dans List-Unsubscribe avec le body List-Unsubscribe=One-Click.

Pas de page de préférences. Pas de "confirmez votre désabonnement". Pas de CAPTCHA. Un POST, un 200/202/204, c'est fait.

import json, urllib.request, urllib.parse, re

TOKEN = "..."  # OAuth token Gmail
mid = "18f..."  # message ID

# Récupérer les headers List-Unsubscribe
url = (f"https://gmail.googleapis.com/gmail/v1/users/me/messages/{mid}"
       "?format=metadata&metadataHeaders=List-Unsubscribe"
       "&metadataHeaders=List-Unsubscribe-Post")
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {TOKEN}"})
data = json.loads(urllib.request.urlopen(req).read())
headers = {h['name'].lower(): h['value']
           for h in data['payload']['headers']}

# Si le sender supporte RFC 8058
if 'One-Click' in headers.get('list-unsubscribe-post', ''):
    target = re.search(r'<(https?://[^>]+)>',
                       headers['list-unsubscribe']).group(1)
    urllib.request.urlopen(urllib.request.Request(
        target,
        data=b"List-Unsubscribe=One-Click",
        method='POST',
        headers={'Content-Type': 'application/x-www-form-urlencoded'}
    ))
    print(f"Désabonné de {target}")

70% des senders sérieux l'implémentent : LinkedIn, BitGo, Coursera, AWS, Hellowork, Meteojob, JeVeuxAider, NVIDIA, Indeed... Mais aucune UI grand public ne l'expose. Tout le monde clique "unsubscribe" dans le corps du mail et passe par 3 pages de preference center. La RFC a 9 ans et elle est invisible.

Résultat de la session : 28 désabonnements réussis sur ~30 candidats, en quelques millisecondes par sender. Les 2 échecs : des URLs malformées dans le header (un mailto: sans alternative HTTP, et un lien cassé).

Pour les senders sans RFC 8058, le fallback est le lien dans le body du mail. Ça veut dire parser le HTML, trouver le href qui contient "unsubscribe" ou "preferences", et souvent soumettre un formulaire Marketo ou Eloqua. Beaucoup plus fragile, beaucoup plus lent. La RFC 8058 est un monde à part.

Quand le MCP foire : Graph API en direct

Le tool search-emails du MCP Outlook a un problème sournois : il ignore silencieusement le filtre de date dans la query string. received<2025-05-12 retourne des mails de 2026. Pour des sweeps sérieux sur des milliers de mails, il faut bypasser le MCP et taper Microsoft Graph en direct.

# Récupérer le token depuis le fichier du MCP
TOKEN=$(jq -r .access_token ~/.outlook-mcp-tokens.json)

# Requête Graph avec filtre date réel
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://graph.microsoft.com/v1.0/me/messages?\$filter=receivedDateTime+lt+2025-05-12T00:00:00Z+and+hasAttachments+eq+false&\$top=999"

Pour les déplacements en masse, le endpoint $batch de Graph accepte 20 requêtes en une seule. Le throughput devient correct : 5 000 mails déplacés en quelques minutes.

import json, urllib.request

TOKEN = "..."

def move_batch(message_ids, destination="deleteditems"):
    """Déplace jusqu'à 20 mails par batch via Graph $batch"""
    body = {"requests": [
        {"id": str(i), "method": "POST",
         "url": f"/me/messages/{mid}/move",
         "headers": {"Content-Type": "application/json"},
         "body": {"destinationId": destination}}
        for i, mid in enumerate(message_ids[:20])
    ]}
    req = urllib.request.Request(
        "https://graph.microsoft.com/v1.0/$batch",
        data=json.dumps(body).encode(),
        headers={"Authorization": f"Bearer {TOKEN}",
                 "Content-Type": "application/json"}
    )
    return json.loads(urllib.request.urlopen(req).read())

Gmail a son équivalent : POST /gmail/v1/users/me/messages/batchModify avec jusqu'à 1 000 IDs en une seule requête. Encore plus efficace — Google a clairement optimisé pour le batch.

Le token qui expire au milieu du sweep

Le token Outlook a une durée de vie d'une heure. Sur un sweep de 5 000 mails avec des pauses entre les batches, il expire au milieu du traitement. La première fois, c'est une surprise — les 200 premiers mails passent, puis 403 Unauthorized en boucle.

import json, urllib.request, urllib.parse

MS_CLIENT_ID = "..."
MS_CLIENT_SECRET = "..."

def refresh_token(creds):
    """Refresh le token Outlook via OAuth2"""
    data = urllib.parse.urlencode({
        'client_id': MS_CLIENT_ID,
        'client_secret': MS_CLIENT_SECRET,
        'refresh_token': creds['refresh_token'],
        'grant_type': 'refresh_token',
        'scope': 'offline_access Mail.ReadWrite MailboxSettings.ReadWrite',
    }).encode()
    r = json.loads(urllib.request.urlopen(
        urllib.request.Request(
            "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
            data=data
        )
    ).read())
    creds['access_token'] = r['access_token']
    return creds

Classique, mais ça fait partie du vécu. Le script final intègre un auto-refresh avant chaque batch, et le bootstrap.sh du repo backup gère ça automatiquement.

Folders Outlook vs labels Gmail

Sur le tri automatique, Outlook et Gmail ont des philosophies opposées.

Outlook : des dossiers hiérarchiques + des rules avec moveToFolder. J'ai créé 7 folders thématiques (Finance, Serveur, Commandes, FTX Recovery, Farnell, Acadomia, Lauréat) + 30 rules de tri. Résultat : l'inbox principal est reset à zéro pour les mails transactionnels. Chaque nouveau mail qui matche un pattern part directement dans le bon dossier.

Gmail : des labels, et c'est tout. Un mail peut avoir N labels et il reste dans l'inbox. Pour obtenir le même résultat qu'un folder Outlook, il faut combiner un label + "Skip the Inbox" dans le filtre. Faisable, mais moins clean visuellement — le compteur de l'inbox ne bouge pas de la même façon.

19 filtres Gmail créés dans la session : principalement pour archiver automatiquement les notifications GitHub, les alertes serveur, et les newsletters que je veux garder mais pas voir en inbox.

Ce que Graph ne peut pas faire

La liste des expéditeurs bloqués (Junk Email blocked senders list) n'est pas exposée par Microsoft Graph pour les comptes perso MSA. Il faut passer par l'UI web d'Outlook ou par EWS legacy — une API que Microsoft décommissionne depuis 2022 mais qui reste la seule à exposer certaines features.

Workaround : créer des rules Graph qui matchent les domaines scam et déplacent les mails dans Deleted Items. Effet équivalent, mais moins propre que la vraie blocklist Junk qui empêche la livraison.

Microsoft promet d'ajouter cette API "bientôt" depuis 2 ans. C'est dans leur backlog public. Toujours pas là.

Le repo backup chiffré

Pour réinstaller le système sur une nouvelle machine en 2 minutes, j'ai créé un repo privé GitHub avec :

  • Les secrets OAuth chiffrés avec openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt
  • Un bootstrap.sh qui demande la passphrase, déchiffre, place les fichiers aux bons paths, clone le MCP Outlook, patche les scopes, npm install, et enregistre les deux MCPs dans Claude Code en scope user
  • La passphrase dans 1Password

Un détail amusant : GitHub a une "push protection" qui détecte les patterns de client secrets Google (GOCSPX-*) et bloque le push — même sur un repo privé. Le chiffrement AES résout ce problème en plus de la sécurité, puisque le fichier poussé est du binaire opaque.

Le classifier auto-mode de Claude Code

Plusieurs fois pendant la session, le classifier de Claude Code a bloqué des actions : "déplacer des mails du Spam vers l'Inbox", "supprimer un filtre que tu n'as pas créé dans cette session". Validation explicite demandée, même après un go général.

Ce n'est pas un bug — c'est une protection volontaire contre les agents qui prennent des actions non explicitement autorisées sur des données sensibles. Dans le contexte d'un nettoyage email, c'est parfois frustrant (on voudrait un --yes-i-know-what-im-doing), mais c'est probablement la bonne approche pour un outil qui a accès à votre boîte mail.

Bilan chiffré

Métrique Volume
MCPs installés et configurés2 (Gmail + Outlook)
Bugs MCP patchés2 (scopes + env vars Outlook)
Filtres Gmail créés19
Rules Outlook créées30+
Folders Outlook organisés7
Désabonnements RFC 805828
Mails Gmail traités~2 470
Mails Outlook traités~5 250
Lignes Python ad-hoc~400
Lignes bash~200
Temps total session~4h

Conclusion

Le setup a pris plus de temps que prévu — patcher le MCP Outlook, debugger OAuth Azure, gérer les tokens qui expirent. Mais une fois les deux serveurs MCP fonctionnels, la vitesse d'exécution est d'un autre ordre. Là où j'aurais passé des jours à cliquer dans les UIs web, Claude Code enchaîne les filtres, les batch moves et les désabonnements RFC 8058 comme un script — parce que c'en est un.

La vraie découverte, c'est la RFC 8058. Un standard vieux de 9 ans, implémenté par la majorité des senders sérieux, et totalement invisible dans les interfaces grand public. Aucun client mail ne l'expose en un clic. Il faut aller chercher le header soi-même. C'est exactement le genre de plomberie que MCP rend accessible : pas de la magie IA, juste un accès programmatique à des APIs que personne ne prend le temps d'appeler manuellement.

Les liens utiles pour reproduire le setup : Gmail API, Microsoft Graph, RFC 8058, Gmail MCP Server, Outlook MCP.

Commentaires (0)