ia_dev/git-issues/mail_common.py
Nicolas Cantu 42ff25ccd5 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
2026-04-23 15:01:40 +02:00

112 lines
3.7 KiB
Python

# 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 sys
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
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)."
)
_pkg = _imap_bridge_python_dir()
if str(_pkg) not in sys.path:
sys.path.insert(0, str(_pkg))
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:
env_root = os.environ.get("REPO_ROOT")
if env_root:
return Path(env_root).resolve()
issues_dir = os.environ.get("GIT_ISSUES_DIR")
if issues_dir:
return Path(issues_dir).resolve().parent
return Path(__file__).resolve().parent.parent
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:
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"),
"repo": os.environ.get("GITEA_REPO_NAME", "lecoffre_ng"),
"token": token or "",
}
def sanitize_title(raw: str, max_len: int = 200) -> str:
one_line = re.sub(r"\s+", " ", raw).strip()
return one_line[:max_len] if one_line else "(no subject)"
def create_gitea_issue(title: str, body: str) -> dict | None:
gitea = load_gitea_config()
if not gitea["token"]:
return None
url = f"{gitea['api_url']}/repos/{gitea['owner']}/{gitea['repo']}/issues"
payload = json.dumps({"title": title, "body": body}).encode("utf-8")
req = Request(
url,
data=payload,
method="POST",
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"token {gitea['token']}",
},
)
try:
with urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except (HTTPError, URLError):
return None