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:
parent
e5e07741d3
commit
42ff25ccd5
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|||||||
@ -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`.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user