#!/usr/bin/env python3 """servizio-gmail-check — controllo meccanico di posta nuova sulla Gmail personale di Mauro, nessun LLM. Sostituisce il disegno precedente (sentinella --bg persistente, dismessa l'08/09/2026 per consumo token quadratico — vedi archivio/Adrian/progetti/project_monitoraggio_gmail.md). Qui il trigger e' un demone Python puro: ogni GMAIL_CHECK_INTERVAL_SECONDS chiama gmail_check.py --since (stessa cartella), e se trova messaggi nuovi scrive le loro intestazioni (mittente, oggetto, data — mai il corpo) direttamente in mailbox.log, evento meccanico senza alcun LLM coinvolto. Il giudizio di importanza (criteri whitelist/ blacklist/fascia grigia in gemello-nave/archivio/criteri-gmail.md) e' compito esclusivo di gemello-nave dall'11/09 (handoff completo, decisione esplicita di Mauro - Adrian non lo applica piu'), invocata a freddo dal suo demone di trigger a ogni evento — non un Claude dedicato per ogni tick. Girato come servizio systemd (vedi servizio-gmail-check.service), Restart=on-failure. """ import json import subprocess import time from datetime import datetime, timezone from pathlib import Path ROOT = Path("/mnt/ssd/data/Dropbox/adrian") HERE = Path(__file__).parent CHECKER = HERE / "gmail_check.py" CHECKER_VENV_PY = HERE / ".venv" / "bin" / "python" STATE_FILE = HERE / "last_check.txt" LOG = Path(__file__).parent / "watch.log" MAILBOX_WRITE = ROOT / "scripts" / "slave_mailbox_write.sh" INTERVAL_SECONDS = 20 * 60 ERROR_NOTIFY_MIN_GAP_SECONDS = 60 * 60 # non piu' di una segnalazione di errore all'ora def log(msg: str) -> None: line = f"{datetime.now(timezone.utc).isoformat()} {msg}" print(line, flush=True) with open(LOG, "a") as f: f.write(line + "\n") def write_mailbox(text: str) -> None: subprocess.run(["bash", str(MAILBOX_WRITE), "gmail-check", text], cwd=str(ROOT), check=False) def read_state() -> str | None: if STATE_FILE.exists(): return STATE_FILE.read_text().strip() or None return None def write_state(iso_ts: str) -> None: STATE_FILE.write_text(iso_ts + "\n") def now_iso() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def run_check(since: str) -> tuple[int, str, str]: python_bin = str(CHECKER_VENV_PY) if CHECKER_VENV_PY.exists() else "python3" result = subprocess.run( [python_bin, str(CHECKER), "--since", since], cwd=str(CHECKER.parent), capture_output=True, text=True, timeout=120, ) return result.returncode, result.stdout, result.stderr def format_report(messages: list[dict]) -> str: lines = [f"{len(messages)} messaggi nuovi:"] for m in messages: lines.append(f" - {m.get('from', '?')} — \"{m.get('subject', '(nessun oggetto)')}\" ({m.get('date_iso', '?')})") return "\n".join(lines) def main() -> None: log("avvio") last_error_notify = 0.0 if read_state() is None: baseline = now_iso() write_state(baseline) log(f"nessuno stato precedente — baseline impostata a {baseline}, primo controllo reale al prossimo tick") while True: time.sleep(INTERVAL_SECONDS) since = read_state() or now_iso() tick_start = now_iso() try: code, stdout, stderr = run_check(since) except subprocess.TimeoutExpired: log("timeout: gmail_check.py non ha risposto entro 120s") if time.time() - last_error_notify > ERROR_NOTIFY_MIN_GAP_SECONDS: write_mailbox("timeout: gmail_check.py non ha risposto entro 120s, nessun aggiornamento stato") last_error_notify = time.time() continue if code == 0: write_state(tick_start) stdout = stdout.strip() if stdout: try: messages = json.loads(stdout) except json.JSONDecodeError: log(f"output non-JSON nonostante exit 0: {stdout[:300]}") continue if messages: log(f"{len(messages)} messaggi nuovi trovati") write_mailbox(format_report(messages)) else: log("nessun messaggio nuovo (array vuoto)") else: log("nessun messaggio nuovo (output vuoto)") else: log(f"gmail_check.py fallito (exit {code}): {stderr[:500]}") if time.time() - last_error_notify > ERROR_NOTIFY_MIN_GAP_SECONDS: write_mailbox(f"gmail_check.py fallito (exit {code}): {stderr[:300]}") last_error_notify = time.time() # stato NON aggiornato: al prossimo tick riprova dallo stesso 'since' if __name__ == "__main__": main()