from __future__ import annotations import asyncio from datetime import datetime from pathlib import Path import structlog _log = structlog.get_logger(__name__) _UNSAFE = str.maketrans({"/": "_", "\\": "_", ":": "_", "<": "", ">": ""}) async def save_eml( archive_root: Path, envelope_id: str, source: str, ts: datetime, raw: bytes, ) -> str: """Write raw .eml bytes to the append-only archive. Layout: {archive_root}/{source}/{YYYY}/{MM}/{sanitized_id}.eml Returns the relative raw_ref path (store this in Envelope.raw_ref). Raises FileExistsError if envelope_id is already archived (append-only guarantee). """ safe_id = envelope_id.translate(_UNSAFE) rel_path = f"{source}/{ts.year:04d}/{ts.month:02d}/{safe_id}.eml" dest = archive_root / rel_path dest.parent.mkdir(parents=True, exist_ok=True) if dest.exists(): raise FileExistsError(f"archive: {rel_path} already exists — archive is append-only") await asyncio.to_thread(dest.write_bytes, raw) _log.info("archive.saved", raw_ref=rel_path, bytes=len(raw)) return rel_path