From 42ff25ccd5203ffdae7ab21e1f9d59d566a83537 Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Thu, 23 Apr 2026 15:01:40 +0200 Subject: [PATCH] Wire git-issues mail_common to LeCoffre automation/imap-bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivations:** - Consume canonical imap_common from lecoffre_ng_test; align Gitea token resolution. **Root causes:** - IMAP logic duplicated under ia_dev only; token path ignored LeCoffre central tree. **Correctives:** - mail_common imports imap_common via LECOFFRE_REPO_ROOT or sibling lecoffre_ng_test; lib.sh token fallback order updated. **Evolutions:** - GIT_ISSUES_SCRIPTS_AGENTS.md documents new secret paths and centralize script. **Page affectées:** - git-issues/mail_common.py, git-issues/lib.sh, git-issues/mail-to-issue.py, projects/ia_dev/docs/GIT_ISSUES_SCRIPTS_AGENTS.md --- git-issues/lib.sh | 18 ++- git-issues/mail-to-issue.py | 4 +- git-issues/mail_common.py | 125 +++++++----------- .../ia_dev/docs/GIT_ISSUES_SCRIPTS_AGENTS.md | 8 +- 4 files changed, 67 insertions(+), 88 deletions(-) diff --git a/git-issues/lib.sh b/git-issues/lib.sh index e819230..23bc94a 100755 --- a/git-issues/lib.sh +++ b/git-issues/lib.sh @@ -59,13 +59,25 @@ load_gitea_token() { fi fi if [[ -z "$token_file" ]]; then - token_file="${ia_dev_root}/.secrets/git-issues/token" + if [[ -n "${LECOFFRE_REPO_ROOT:-}" && -f "${LECOFFRE_REPO_ROOT}/.secrets/automation/git-issues/token" ]]; then + token_file="${LECOFFRE_REPO_ROOT}/.secrets/automation/git-issues/token" + elif [[ -f "${ia_dev_root}/../lecoffre_ng_test/.secrets/automation/git-issues/token" ]]; then + token_file="${ia_dev_root}/../lecoffre_ng_test/.secrets/automation/git-issues/token" + fi fi - if [[ -f "$token_file" ]]; then + if [[ -z "$token_file" ]]; then + for cand in "${ia_dev_root}/.secrets/git-issues/token" "${ia_dev_root}/.secrets/gitea-issues/token"; do + if [[ -f "$cand" ]]; then + token_file="$cand" + break + fi + done + fi + if [[ -n "$token_file" && -f "$token_file" ]]; then GITEA_TOKEN="$(cat "$token_file")" return 0 fi - echo "[git-issues] ERROR: GITEA_TOKEN not set and ${token_file} not found" >&2 + echo "[git-issues] ERROR: GITEA_TOKEN not set and no token file found (tried LECOFFRE_REPO_ROOT, lecoffre_ng_test sibling, ia_dev .secrets/git-issues|gitea-issues)" >&2 echo "[git-issues] Set GITEA_TOKEN or create the token file with a Gitea Personal Access Token." >&2 return 1 } diff --git a/git-issues/mail-to-issue.py b/git-issues/mail-to-issue.py index 35d8d89..33652b9 100755 --- a/git-issues/mail-to-issue.py +++ b/git-issues/mail-to-issue.py @@ -12,8 +12,8 @@ This script (mail-to-issue) is a **batch** fallback: it creates one issue per un message with title=subject and body=text+From, then marks messages as read. Use only when the agent-driven flow is not used. -Reads IMAP config from .secrets/git-issues/imap-bridge.env (or env vars). -Reads Gitea token from GITEA_TOKEN or .secrets/git-issues/token. +Reads IMAP config via automation/imap-bridge (LeCoffre) — see imap_common.resolve_imap_bridge_env_path (or env vars). +Reads Gitea token from GITEA_TOKEN or centralized / legacy token paths (mail_common.load_gitea_config). """ from __future__ import annotations diff --git a/git-issues/mail_common.py b/git-issues/mail_common.py index 61d32cb..3ee45c3 100644 --- a/git-issues/mail_common.py +++ b/git-issues/mail_common.py @@ -1,38 +1,49 @@ -# Shared config and helpers for git-issues mail scripts (IMAP/SMTP, Gitea). -# Used by mail-list-unread, mail-send-reply, mail-create-issue-from-email, mail-mark-read. +# Shared config and helpers for git-issues mail scripts (Gitea + re-exported IMAP/SMTP from LeCoffre). +# IMAP/SMTP implementation: lecoffre_ng_test/automation/imap-bridge/imap_common.py from __future__ import annotations import json import os import re -import ssl +import sys from pathlib import Path from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen -# Only consider messages on or after this date (IMAP format DD-Mon-YYYY). Override with env MAIL_SINCE_DATE. -MAIL_SINCE_DATE_DEFAULT = "10-Mar-2026" + +def _imap_bridge_python_dir() -> Path: + if os.environ.get("LECOFFRE_REPO_ROOT"): + p = Path(os.environ["LECOFFRE_REPO_ROOT"]).expanduser().resolve() / "automation" / "imap-bridge" + if (p / "imap_common.py").is_file(): + return p + here = Path(__file__).resolve().parent + ia_dev_root = here.parent + sibling = ia_dev_root.parent / "lecoffre_ng_test" / "automation" / "imap-bridge" + if (sibling / "imap_common.py").is_file(): + return sibling + raise ImportError( + "imap_common not found: set LECOFFRE_REPO_ROOT to the LeCoffre monorepo root " + "or clone lecoffre_ng_test beside ia_dev (../lecoffre_ng_test/automation/imap-bridge/imap_common.py)." + ) -def imap_since_date() -> str: - """Return IMAP SINCE date (DD-Mon-YYYY). Messages before this date are ignored by fetch/list scripts.""" - return os.environ.get("MAIL_SINCE_DATE", MAIL_SINCE_DATE_DEFAULT).strip() or MAIL_SINCE_DATE_DEFAULT +_pkg = _imap_bridge_python_dir() +if str(_pkg) not in sys.path: + sys.path.insert(0, str(_pkg)) - -def imap_search_criterion_all() -> str: - """IMAP search: all messages on or after MAIL_SINCE_DATE.""" - return f"SINCE {imap_since_date()}" - - -def imap_search_criterion_unseen() -> str: - """IMAP search: unread messages on or after MAIL_SINCE_DATE.""" - return f"(UNSEEN SINCE {imap_since_date()})" +from imap_common import ( + MAIL_SINCE_DATE_DEFAULT, + imap_search_criterion_all, + imap_search_criterion_unseen, + imap_since_date, + imap_ssl_context, + load_imap_config, + load_smtp_config, +) def repo_root() -> Path: - # Root = directory containing git-issues (ia_dev). .secrets and logs live under ia_dev (./.secrets, ./logs) - # so they do not depend on a specific project; same ia_dev works for any project (PROJECT_ID from MAIL_TO or AI_AGENT_TOKEN). env_root = os.environ.get("REPO_ROOT") if env_root: return Path(env_root).resolve() @@ -42,72 +53,28 @@ def repo_root() -> Path: return Path(__file__).resolve().parent.parent -def load_env_file(path: Path) -> None: - if not path.is_file(): - return - with open(path, encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - if "=" in line: - key, _, value = line.partition("=") - key = key.strip() - value = value.strip().strip("'\"") - if key and key not in os.environ: - os.environ[key] = value - - -def load_imap_config() -> dict[str, str]: - root = repo_root() - env_path = root / ".secrets" / "git-issues" / "imap-bridge.env" - load_env_file(env_path) - ssl_verify_raw = os.environ.get("IMAP_SSL_VERIFY", "true").lower() - ssl_verify = ssl_verify_raw not in ("0", "false", "no") - return { - "host": os.environ.get("IMAP_HOST", "127.0.0.1"), - "port": os.environ.get("IMAP_PORT", "1143"), - "user": os.environ.get("IMAP_USER", ""), - "password": os.environ.get("IMAP_PASSWORD", ""), - "use_starttls": os.environ.get("IMAP_USE_STARTTLS", "true").lower() in ("1", "true", "yes"), - "ssl_verify": ssl_verify, - "filter_to": os.environ.get("MAIL_FILTER_TO", "ai.support.lecoffreio@4nkweb.com").strip().lower(), - } - - -def imap_ssl_context(ssl_verify: bool = True) -> ssl.SSLContext: - """Return SSL context for IMAP STARTTLS. Use ssl_verify=False only for local Bridge with self-signed cert.""" - if ssl_verify: - return ssl.create_default_context() - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - return ctx - - -def load_smtp_config() -> dict[str, str]: - root = repo_root() - env_path = root / ".secrets" / "git-issues" / "imap-bridge.env" - load_env_file(env_path) - ssl_verify_raw = os.environ.get("IMAP_SSL_VERIFY", os.environ.get("SMTP_SSL_VERIFY", "true")).lower() - ssl_verify = ssl_verify_raw not in ("0", "false", "no") - return { - "host": os.environ.get("SMTP_HOST", os.environ.get("IMAP_HOST", "127.0.0.1")), - "port": os.environ.get("SMTP_PORT", "1025"), - "user": os.environ.get("SMTP_USER", os.environ.get("IMAP_USER", "")), - "password": os.environ.get("SMTP_PASSWORD", os.environ.get("IMAP_PASSWORD", "")), - "use_starttls": os.environ.get("SMTP_USE_STARTTLS", "true").lower() in ("1", "true", "yes"), - "ssl_verify": ssl_verify, - } +def _lecoffre_automation_token_path() -> Path | None: + lr = os.environ.get("LECOFFRE_REPO_ROOT", "").strip() + if lr: + p = Path(lr).expanduser().resolve() / ".secrets" / "automation" / "git-issues" / "token" + return p if p.is_file() else None + sib = repo_root().parent / "lecoffre_ng_test" / ".secrets" / "automation" / "git-issues" / "token" + return sib if sib.is_file() else None def load_gitea_config() -> dict[str, str]: root = repo_root() token = os.environ.get("GITEA_TOKEN") if not token: - token_path = root / ".secrets" / "git-issues" / "token" - if token_path.is_file(): - token = token_path.read_text(encoding="utf-8").strip() + lt = _lecoffre_automation_token_path() + if lt is not None: + token = lt.read_text(encoding="utf-8").strip() + if not token: + for sub in ("git-issues", "gitea-issues"): + token_path = root / ".secrets" / sub / "token" + if token_path.is_file(): + token = token_path.read_text(encoding="utf-8").strip() + break return { "api_url": os.environ.get("GITEA_API_URL", "https://git.4nkweb.com/api/v1").rstrip("/"), "owner": os.environ.get("GITEA_REPO_OWNER", "4nk"), diff --git a/projects/ia_dev/docs/GIT_ISSUES_SCRIPTS_AGENTS.md b/projects/ia_dev/docs/GIT_ISSUES_SCRIPTS_AGENTS.md index bee6e9e..9abb36a 100644 --- a/projects/ia_dev/docs/GIT_ISSUES_SCRIPTS_AGENTS.md +++ b/projects/ia_dev/docs/GIT_ISSUES_SCRIPTS_AGENTS.md @@ -34,12 +34,12 @@ Spooler tickets (nouveau) : mails dans **projects//data/issues/** (filtre pa - **Emplacement (usage standalone)** : ia_dev est un dépôt autonome. `git-issues/` est à la racine de ia_dev. Un même clone ia_dev peut servir plusieurs projets (id résolu par MAIL_TO ou AI_AGENT_TOKEN). - **Projet cible** : le projet est identifié **dynamiquement** par **MAIL_TO** (adresse « to » des mails, recherchée dans les configs ticketing de tous les projets) ou **AI_AGENT_TOKEN** (token des requêtes). L'id sert à charger la config dans `projects//`. Pas de fallback. Voir `docs/repo/ia-dev-project-conf-schema.md` (racine monorepo **smart_ide**). - **Lancement** : tous les scripts sont invoqués depuis la **racine de ia_dev** : `./git-issues/