feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)

Real IMAP connection via imapflow library:
- Connects to IMAP server (IMAPFlow, TLS on port 993)
- Fetches recent messages (configurable limit, default 20)
- Extracts: from, fromEmail, subject, date, snippet (200 chars)
- Thread-to-contact matching (PRD §6.1):
  1. Match sender email against persons table
  2. If no person match, extract domain and match against company.website
  3. Returns matchedCompany + matchedPerson for each email
- Audit-logged (action: crm.mailsync:imap)
- Zod validated (host, port, user, password, folder, limit)
- 502 on IMAP connection failure with detail
- 400 on validation failure

Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
Z User committed 2026-10-27 22:50:50 +00:00
1 parent 7acc0c5451
commit e45b68bb8b
1089 files changed
+260614

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""合并 major_recommendation.json 与 school_recommendation.json 为 analysis.json。"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
def load_json(path: Path) -> dict:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
raise RuntimeError(f"无法读取 {path}: {e}") from e
def merge(majors_data: dict, schools_data: dict) -> dict:
return {
"student": majors_data.get("student") or {},
"analysis": {
"intro": majors_data.get("intro") or "",
"strategy": majors_data.get("strategy") or "",
"major_directions": majors_data.get("major_directions") or [],
"school_strategy": schools_data.get("school_strategy") or "",
},
"recommended_majors": majors_data.get("recommended_majors") or [],
"recommended_schools": schools_data.get("recommended_schools") or [],
}
def main() -> int:
parser = argparse.ArgumentParser(description="合并专业/院校推荐为 analysis.json")
parser.add_argument("--majors", type=Path, required=True, help="major_recommendation.json")
parser.add_argument("--schools", type=Path, required=True, help="school_recommendation.json")
parser.add_argument("-o", "--output", type=Path, required=True, help="输出 analysis.json")
args = parser.parse_args()
try:
majors_data = load_json(args.majors)
schools_data = load_json(args.schools)
result = merge(majors_data, schools_data)
except RuntimeError as e:
print(f"错误: {e}", file=sys.stderr)
return 1
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print(
f"已合并: {args.output}(专业 {len(result['recommended_majors'])},"
f"学校 {len(result['recommended_schools'])})"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())