Wire git-issues mail_common to LeCoffre automation/imap-bridge

**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
This commit is contained in:
Nicolas Cantu 2026-04-23 15:01:40 +02:00
parent e5e07741d3
commit 42ff25ccd5
4 changed files with 67 additions and 88 deletions

View File

@ -59,13 +59,25 @@ load_gitea_token() {
fi fi
fi fi
if [[ -z "$token_file" ]]; then 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 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")" GITEA_TOKEN="$(cat "$token_file")"
return 0 return 0
fi 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 echo "[git-issues] Set GITEA_TOKEN or create the token file with a Gitea Personal Access Token." >&2
return 1 return 1
} }

View File

@ -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 message with title=subject and body=text+From, then marks messages as read. Use only
when the agent-driven flow is not used. when the agent-driven flow is not used.
Reads IMAP config from .secrets/git-issues/imap-bridge.env (or env vars). 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 .secrets/git-issues/token. Reads Gitea token from GITEA_TOKEN or centralized / legacy token paths (mail_common.load_gitea_config).
""" """
from __future__ import annotations from __future__ import annotations

View File

@ -1,38 +1,49 @@
# Shared config and helpers for git-issues mail scripts (IMAP/SMTP, Gitea). # Shared config and helpers for git-issues mail scripts (Gitea + re-exported IMAP/SMTP from LeCoffre).
# Used by mail-list-unread, mail-send-reply, mail-create-issue-from-email, mail-mark-read. # IMAP/SMTP implementation: lecoffre_ng_test/automation/imap-bridge/imap_common.py
from __future__ import annotations from __future__ import annotations
import json import json
import os import os
import re import re
import ssl import sys
from pathlib import Path from pathlib import Path
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen 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: _pkg = _imap_bridge_python_dir()
"""Return IMAP SINCE date (DD-Mon-YYYY). Messages before this date are ignored by fetch/list scripts.""" if str(_pkg) not in sys.path:
return os.environ.get("MAIL_SINCE_DATE", MAIL_SINCE_DATE_DEFAULT).strip() or MAIL_SINCE_DATE_DEFAULT sys.path.insert(0, str(_pkg))
from imap_common import (
def imap_search_criterion_all() -> str: MAIL_SINCE_DATE_DEFAULT,
"""IMAP search: all messages on or after MAIL_SINCE_DATE.""" imap_search_criterion_all,
return f"SINCE {imap_since_date()}" imap_search_criterion_unseen,
imap_since_date,
imap_ssl_context,
def imap_search_criterion_unseen() -> str: load_imap_config,
"""IMAP search: unread messages on or after MAIL_SINCE_DATE.""" load_smtp_config,
return f"(UNSEEN SINCE {imap_since_date()})" )
def repo_root() -> Path: 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") env_root = os.environ.get("REPO_ROOT")
if env_root: if env_root:
return Path(env_root).resolve() return Path(env_root).resolve()
@ -42,72 +53,28 @@ def repo_root() -> Path:
return Path(__file__).resolve().parent.parent return Path(__file__).resolve().parent.parent
def load_env_file(path: Path) -> None: def _lecoffre_automation_token_path() -> Path | None:
if not path.is_file(): lr = os.environ.get("LECOFFRE_REPO_ROOT", "").strip()
return if lr:
with open(path, encoding="utf-8") as f: p = Path(lr).expanduser().resolve() / ".secrets" / "automation" / "git-issues" / "token"
for line in f: return p if p.is_file() else None
line = line.strip() sib = repo_root().parent / "lecoffre_ng_test" / ".secrets" / "automation" / "git-issues" / "token"
if not line or line.startswith("#"): return sib if sib.is_file() else None
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 load_gitea_config() -> dict[str, str]: def load_gitea_config() -> dict[str, str]:
root = repo_root() root = repo_root()
token = os.environ.get("GITEA_TOKEN") token = os.environ.get("GITEA_TOKEN")
if not token: if not token:
token_path = root / ".secrets" / "git-issues" / "token" lt = _lecoffre_automation_token_path()
if token_path.is_file(): if lt is not None:
token = token_path.read_text(encoding="utf-8").strip() 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 { return {
"api_url": os.environ.get("GITEA_API_URL", "https://git.4nkweb.com/api/v1").rstrip("/"), "api_url": os.environ.get("GITEA_API_URL", "https://git.4nkweb.com/api/v1").rstrip("/"),
"owner": os.environ.get("GITEA_REPO_OWNER", "4nk"), "owner": os.environ.get("GITEA_REPO_OWNER", "4nk"),

View File

@ -34,12 +34,12 @@ Spooler tickets (nouveau) : mails dans **projects/<id>/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). - **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/<id>/`. Pas de fallback. Voir `docs/repo/ia-dev-project-conf-schema.md` (racine monorepo **smart_ide**). - **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/<id>/`. 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/<script>.sh`. Les opérations (issues, mails, déploiement) utilisent les chemins absolus de `projects/<id>/conf.json` pour le projet cible. - **Lancement** : tous les scripts sont invoqués depuis la **racine de ia_dev** : `./git-issues/<script>.sh`. Les opérations (issues, mails, déploiement) utilisent les chemins absolus de `projects/<id>/conf.json` pour le projet cible.
- **Secrets et logs** : `.secrets` est à la racine de ia_dev (`.secrets/git-issues/token`, `agent-loop.env`, `imap-bridge.env`). Les logs et data par projet sont sous `projects/<id>/logs/` et `projects/<id>/data/issues/`. - **Secrets et logs** : les logs et data par projet sont sous `projects/<id>/logs/` et `projects/<id>/data/issues/`. **IMAP/SMTP** : module canonique **`lecoffre_ng_test/automation/imap-bridge/imap_common.py`** (résolution via **`LECOFFRE_REPO_ROOT`** ou clone **`lecoffre_ng_test`** voisin de **`ia_dev`**). Fichiers env préférés : **`<LeCoffre>/.secrets/automation/git-issues/imap-bridge.env`** ; repli : **`ia_dev/.secrets/git-issues/`** ou **`ia_dev/.secrets/gitea-issues/`**. **Token Gitea** : **`GITEA_TOKEN`**, puis **`<LeCoffre>/.secrets/automation/git-issues/token`**, puis **`ia_dev/.secrets/git-issues|gitea-issues/token`**. Copie centralisée avec backup horodaté : depuis la racine LeCoffre, **`bash automation/imap-bridge/centralize-ia-dev-secrets.sh`** (variable **`IADEV_ROOT`** si besoin).
## Prérequis ## Prérequis
- **jq** : `apt install jq` ou `brew install jq` - **jq** : `apt install jq` ou `brew install jq`
- **Token Gitea** : variable d'environnement `GITEA_TOKEN` ou fichier `.secrets/git-issues/token` (contenu = le token, non versionné). Créer le token dans Gitea : Settings → Applications → Generate New Token (scopes `read:issue`, `write:issue` si commentaires). - **Token Gitea** : variable d'environnement **`GITEA_TOKEN`**, ou fichier **`lecoffre_ng_test/.secrets/automation/git-issues/token`** (centralisé), ou **`ia_dev/.secrets/git-issues/token`** / **`gitea-issues/token`** (repli). Créer le token dans Gitea : Settings → Applications → Generate New Token (scopes `read:issue`, `write:issue` si commentaires).
## Scripts (depuis la racine de ia_dev) ## Scripts (depuis la racine de ia_dev)
@ -97,8 +97,8 @@ Le répertoire **projects/<id>/data/issues/** (spooler) est rempli **uniquement*
**Prérequis :** **Prérequis :**
- Python 3 (stdlib : imaplib, email, smtplib, json, urllib). - Python 3 (stdlib : imaplib, email, smtplib, json, urllib).
- Token Gitea : comme les autres scripts (`GITEA_TOKEN` ou `.secrets/git-issues/token`). - Token Gitea : comme les autres scripts (**`GITEA_TOKEN`** ou chemins documentés ci-dessus).
- Config IMAP/SMTP : copier `git-issues/imap-bridge.env.example` vers `.secrets/git-issues/imap-bridge.env`. Pour la boucle agent (optionnel) : `agent-loop.env.example` vers `.secrets/git-issues/agent-loop.env` (voir `.smartIde/agents/agent-loop.md`). Renseigner `IMAP_USER`, `IMAP_PASSWORD` (et optionnellement `SMTP_*` pour l'envoi ; par défaut SMTP reprend les mêmes host/port Bridge 1025). Optionnel : `MAIL_FILTER_TO=ai.support.lecoffreio@4nkweb.com` (seuls les mails envoyés à cette adresse sont listés) ; `MAIL_SINCE_DATE=10-Mar-2026` (seuls les mails à partir de cette date sont récupérés/listés). - Config IMAP/SMTP : gabarit **`lecoffre_ng_test/automation/imap-bridge/imap-bridge.env.example`** → fichier canonique **`<LeCoffre>/.secrets/automation/git-issues/imap-bridge.env`** (ou repli **`ia_dev/.secrets/git-issues|gitea-issues/imap-bridge.env`**). **`bash automation/imap-bridge/centralize-ia-dev-secrets.sh`** copie depuis **`ia_dev`** vers LeCoffre avec backup horodaté sous **`.secrets/automation/git-issues/backup/`**. Pour la boucle agent (optionnel) : `agent-loop.env.example` vers `.secrets/git-issues/agent-loop.env` sous **`ia_dev`** ou la même arborescence **automation** après centralisation (voir `.smartIde/agents/agent-loop.md`). Renseigner `IMAP_USER`, `IMAP_PASSWORD` (et optionnellement `SMTP_*` pour l'envoi ; par défaut SMTP reprend les mêmes host/port Bridge 1025). Optionnel : `MAIL_FILTER_TO=ai.support.lecoffreio@4nkweb.com` (seuls les mails envoyés à cette adresse sont listés) ; `MAIL_SINCE_DATE=10-Mar-2026` (seuls les mails à partir de cette date sont récupérés/listés).
- Proton Mail Bridge (ou serveur IMAP/SMTP) en cours d'exécution. - Proton Mail Bridge (ou serveur IMAP/SMTP) en cours d'exécution.
**Scripts :** `mail-list-unread.sh`, `mail-get-thread.sh`, `mail-thread-log.sh`, `mail-send-reply.sh`, `mail-create-issue-from-email.sh`, `mail-mark-read.sh`. Le script batch `mail-to-issue.sh` reste disponible mais ne doit pas être utilisé dans le cadre du workflow agent (liste → lecture du fil → log du fil → décision → création/ réponse). Le script **`agent-loop.sh`** permet de lancer une boucle de surveillance des mails avec fichier témoin ; voir `.smartIde/agents/agent-loop.md`. **Scripts :** `mail-list-unread.sh`, `mail-get-thread.sh`, `mail-thread-log.sh`, `mail-send-reply.sh`, `mail-create-issue-from-email.sh`, `mail-mark-read.sh`. Le script batch `mail-to-issue.sh` reste disponible mais ne doit pas être utilisé dans le cadre du workflow agent (liste → lecture du fil → log du fil → décision → création/ réponse). Le script **`agent-loop.sh`** permet de lancer une boucle de surveillance des mails avec fichier témoin ; voir `.smartIde/agents/agent-loop.md`.