#!/usr/bin/env python3 """ migrate_to_email_md.py — конвертация существующего архива из meta.json + body.md в email.md. Сканирует /mnt/yandex-disk/hermes/email//YYYY/MM/UID/, где лежат meta.json + body.md, и объединяет их в один email.md с YAML-frontmatter. После успешной записи удаляет meta.json и body.md. Запуск: python3 migrate_to_email_md.py # все папки python3 migrate_to_email_md.py --dry-run # только показать, что будет python3 migrate_to_email_md.py --folder INBOX # только одну папку """ import json import sys import argparse from pathlib import Path from datetime import datetime ARCHIVE_ROOT = Path("/mnt/yandex-disk/hermes/email") def make_frontmatter(meta: dict, body: str) -> str: """Собрать YAML frontmatter + body в один email.md.""" # Извлекаем поля из meta.json msg_id = meta.get("id", "0") flags = meta.get("flags", []) subject = meta.get("subject", "") from_ = meta.get("from", {}) to_ = meta.get("to", {}) date = meta.get("date", "") has_attachment = meta.get("has_attachment", False) # Форматируем from/to как "Name " def fmt_contact(c): if not c: return "" name = c.get("name") or "" addr = c.get("addr") or "" if name and addr: return f"{name} <{addr}>" return addr or name from_str = fmt_contact(from_) to_str = fmt_contact(to_) # Собираем YAML-поля вручную, чтобы сохранить порядок и читаемость lines = [] lines.append("---") lines.append(f"id: {msg_id}") lines.append(f"folder: {meta.get('_folder', '')}") lines.append(f"subject: {json.dumps(subject, ensure_ascii=False)}") lines.append(f"from: {json.dumps(from_str, ensure_ascii=False)}") lines.append(f"to: {json.dumps(to_str, ensure_ascii=False)}") lines.append(f"date: {json.dumps(date, ensure_ascii=False)}") lines.append(f"flags: {json.dumps(flags)}") lines.append(f"has_attachment: {str(has_attachment).lower()}") # Дополнительные поля, если есть (Message-ID и т.д.) for extra_field in ["message_id", "in_reply_to", "references", "cc", "content_type"]: val = meta.get(extra_field) if val: lines.append(f"{extra_field}: {json.dumps(val, ensure_ascii=False)}") lines.append("---") lines.append("") # Тело письма — уже в body.md lines.append(body.rstrip("\n")) return "\n".join(lines) def migrate_email(msg_dir: Path, dry_run: bool = False) -> bool: """ Конвертировать один email из meta.json + body.md в email.md. Возвращает True, если было что конвертировать. """ meta_path = msg_dir / "meta.json" body_path = msg_dir / "body.md" email_path = msg_dir / "email.md" if not meta_path.exists() and not body_path.exists(): return False if email_path.exists() and not meta_path.exists() and not body_path.exists(): # Уже сконвертировано return False # Читаем meta.json (если есть, иначе пустой словарь) meta = {} if meta_path.exists(): try: with meta_path.open("r", encoding="utf-8") as f: meta = json.load(f) except Exception as e: print(f" [WARN] {msg_dir}/meta.json: {e}", file=sys.stderr) # Читаем body.md (если есть) body = "" if body_path.exists(): try: body = body_path.read_text(encoding="utf-8") except Exception as e: print(f" [WARN] {msg_dir}/body.md: {e}", file=sys.stderr) # Добавляем имя папки в meta # Извлекаем относительный путь от ARCHIVE_ROOT try: rel = msg_dir.relative_to(ARCHIVE_ROOT) folder = str(rel.parent.parent.parent) # folder/YYYY/MM/UID -> folder except ValueError: folder = "unknown" meta["_folder"] = folder content = make_frontmatter(meta, body) if dry_run: uid = meta.get("id", "?") subj = meta.get("subject", "(no subject)")[:50] print(f" [DRY] {folder}/{uid} ({subj}) → email.md") return True # Пишем email.md email_path.parent.mkdir(parents=True, exist_ok=True) email_path.write_text(content, encoding="utf-8") # Удаляем старые файлы if meta_path.exists(): meta_path.unlink() if body_path.exists(): body_path.unlink() uid = meta.get("id", "?") subj = meta.get("subject", "")[:50] print(f" ✓ {folder}/{uid} ({subj})") return True def scan_folders(root: Path) -> list[Path]: """Найти все директории вида /YYYY/MM/UID/.""" msg_dirs = [] for folder_dir in root.iterdir(): if not folder_dir.is_dir(): continue for year_dir in folder_dir.iterdir(): if not year_dir.is_dir() or not year_dir.name.isdigit(): continue for month_dir in year_dir.iterdir(): if not month_dir.is_dir() or not month_dir.name.isdigit(): continue for uid_dir in month_dir.iterdir(): if not uid_dir.is_dir(): continue msg_dirs.append(uid_dir) return msg_dirs def main(): parser = argparse.ArgumentParser( description="Миграция meta.json + body.md → email.md с YAML-frontmatter" ) parser.add_argument("--dry-run", action="store_true", help="Показать, что будет сделано, без изменений") parser.add_argument("--folder", default=None, help="Конвертировать только одну папку (например, INBOX)") args = parser.parse_args() if not ARCHIVE_ROOT.exists(): print(f"Ошибка: {ARCHIVE_ROOT} не существует", file=sys.stderr) return 1 msg_dirs = scan_folders(ARCHIVE_ROOT) if args.folder: msg_dirs = [d for d in msg_dirs if str(d.relative_to(ARCHIVE_ROOT)).startswith(args.folder + "/")] if not msg_dirs: print("Нет директорий с письмами для конвертации.") return 0 total = 0 skipped = 0 for msg_dir in sorted(msg_dirs): try: if migrate_email(msg_dir, dry_run=args.dry_run): total += 1 else: skipped += 1 except Exception as e: print(f" [ERROR] {msg_dir}: {e}", file=sys.stderr) if args.dry_run: print(f"\nБудет сконвертировано: {total}, пропущено (уже email.md): {skipped}") else: print(f"\nСконвертировано: {total}, пропущено: {skipped}") return 0 if __name__ == "__main__": sys.exit(main())