"""Email template loader and renderer for client delivery emails.""" from __future__ import annotations import logging from dataclasses import dataclass from pathlib import Path import yaml log = logging.getLogger(__name__) _TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "skills" / "email_templates" @dataclass class EmailTemplate: """Parsed email template with subject and body.""" task_type: str subject_template: str body_template: str def load_template(task_type: str, templates_dir: Path | str = _TEMPLATES_DIR) -> EmailTemplate | None: """Load an email template matching the given task type. Searches skills/email_templates/ for .md files with matching task_type in frontmatter. """ templates_dir = Path(templates_dir) if not templates_dir.exists(): log.warning("Email templates directory not found: %s", templates_dir) return None for md_file in templates_dir.glob("*.md"): template = _parse_template_file(md_file) if template and template.task_type.lower() == task_type.lower(): log.info("Loaded email template '%s' for task type '%s'", md_file.name, task_type) return template log.warning("No email template found for task type '%s'", task_type) return None def _parse_template_file(path: Path) -> EmailTemplate | None: """Parse a template .md file with YAML frontmatter.""" text = path.read_text(encoding="utf-8") # Split frontmatter from body if not text.startswith("---"): return None parts = text.split("---", 2) if len(parts) < 3: return None try: meta = yaml.safe_load(parts[1]) or {} except yaml.YAMLError: log.warning("Invalid YAML frontmatter in %s", path.name) return None task_type = meta.get("task_type", "") subject = meta.get("subject", "") body = parts[2].strip() if not task_type or not body: return None return EmailTemplate(task_type=task_type, subject_template=subject, body_template=body) def render_template(template: EmailTemplate, context: dict) -> tuple[str, str]: """Render a template with the given context dict. Returns (subject, body). Uses str.format_map() for placeholder substitution. Unknown placeholders are left as-is. """ subject = _safe_format(template.subject_template, context) body = _safe_format(template.body_template, context) return subject, body def _safe_format(template: str, context: dict) -> str: """Format a string template, leaving unknown placeholders intact.""" class SafeDict(dict): def __missing__(self, key: str) -> str: return "{" + key + "}" return template.format_map(SafeDict(context))