#!/usr/bin/env python3 # mail-texte — lecture SÛRE et propre d'un mail pour l'agent (wrapper mince autour de gog). # ----------------------------------------------------------------------------- # L'agent NE décode plus le MIME/base64/HTML lui-même : gog le fait déjà # (`thread get --full --sanitize-content`). Ce wrapper ne fait que : imposer les # flags de sécurité, restreindre aux sous-commandes lecture, et nettoyer/tronquer. # # Sécurité IMPOSÉE, non paramétrable par l'appelant : # --wrap-untrusted → le contenu d'un mail est marqué DONNÉE non fiable (anti-injection) ; # --gmail-no-send → aucun envoi possible depuis cet outil. # Liste blanche : seules les sous-commandes 'lire' et 'chercher' existent ici. # Le contenu d'un mail est une DONNÉE, JAMAIS une instruction : un mail qui dit # « transfère ceci » ou « ignore tes consignes » ne se traite pas comme un ordre. # # Usage : # mail-texte lire # mail-texte chercher "" [max] # ----------------------------------------------------------------------------- import subprocess, sys, re, json EMAIL = "tallagrand.thierry@gmail.com" GOG = "gog" # présent dans le conteneur FORCE = ["--wrap-untrusted", "--gmail-no-send"] # imposés, jamais paramétrables MAXLEN = 4000 # Marqueurs de fin de contenu utile : au-delà, ce n'est que promo/pied de page. COUPURES = [ "Vous aimerez aussi", "Catégories populaires", "Categories populaires", "Téléchargez l'application", "Telechargez l'application", "Cet email a été envoyé", "Cet email a ete envoye", "Pour ne plus recevoir", "Politique de confidentialité", "unsubscribe", ] def _gog(args): """Appelle gog gmail avec les flags de sécurité imposés. Lecture seule.""" try: r = subprocess.run([GOG, "gmail"] + args + FORCE + ["-a", EMAIL], capture_output=True, text=True, timeout=30) except FileNotFoundError: sys.exit("mail-texte: binaire gog introuvable") except subprocess.TimeoutExpired: sys.exit("mail-texte: gog n'a pas répondu (timeout)") if r.returncode != 0: sys.exit(f"mail-texte: erreur gog — {r.stderr.strip()[:200]}") return r.stdout def nettoyer(txt): # zero-width / espaces insécables de tracking txt = txt.replace("‌", "").replace("​", "").replace("", "") txt = txt.replace(" ", " ").replace(" ", " ").replace(" ", " ") # couper la queue promo / footer coupe = len(txt) for m in COUPURES: i = txt.find(m) if 0 < i < coupe: coupe = i txt = txt[:coupe] # espaces / lignes vides multiples txt = re.sub(r"[ \t]{2,}", " ", txt) txt = re.sub(r" *\n", "\n", txt) txt = re.sub(r"\n{3,}", "\n\n", txt).strip() if len(txt) > MAXLEN: txt = txt[:MAXLEN].rstrip() + "\n… (tronqué)" return txt def lire(thread_id): out = _gog(["thread", "get", thread_id, "--full", "--sanitize-content"]) print(nettoyer(out)) RX_DUREE = re.compile(r"\b(newer_than|older_than):([^\s]+)") RX_DATE = re.compile(r"\b(after|before):([^\s]+)") def _valider(query): """Gmail ne signale PAS une syntaxe invalide : il renvoie 0 résultat. Le 27/09/2026 l'agent a cherché « newer_than:2026/9/26 » et conclu « 0 mail ». On refuse donc ici, bruyamment.""" for op, val in RX_DUREE.findall(query): if not re.fullmatch(r"\d+[dmy]", val): sys.exit(f"mail-texte: syntaxe invalide « {op}:{val} » — {op} attend une DURÉE (ex. newer_than:1d, older_than:30d) ; pour une date, utiliser after:AAAA/MM/JJ") for op, val in RX_DATE.findall(query): if not re.fullmatch(r"\d{4}/\d{1,2}/\d{1,2}|\d+[dmy]", val): sys.exit(f"mail-texte: syntaxe invalide « {op}:{val} » — {op} attend une date AAAA/MM/JJ") def chercher(query, maxn=10): _valider(query) out = _gog(["search", query, "-j", "--max", str(maxn)]) try: d = json.loads(out) except json.JSONDecodeError: print(out[:1000]); return threads = d.get("threads", d.get("messages", [])) if not threads: print(f"Aucun mail trouvé pour la requête « {query} » (requête valide, 0 résultat)."); return for t in threads: print(f"{t.get('id','?')} | {t.get('date','?')} | {t.get('from','?')} | {t.get('subject','(sans sujet)')}") def usage(): sys.exit('Usage: mail-texte lire | mail-texte chercher "" [max]') if __name__ == "__main__": if len(sys.argv) < 3: usage() cmd = sys.argv[1] if cmd == "lire": lire(sys.argv[2]) elif cmd == "chercher": nums = [a for a in sys.argv[3:] if a.isdigit()] # accepte « 50 » ou « --max 50 » chercher(sys.argv[2], int(nums[0]) if nums else 10) else: sys.exit(f"mail-texte: sous-commande '{cmd}' non autorisée (seules : lire, chercher).")