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

+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""
Utility script for managing the blog examples library.
Helps identify old examples to prune when the library exceeds the limit.
"""
import os
import sys
from datetime import datetime
from pathlib import Path
EXAMPLES_DIR = Path(__file__).parent.parent / "references" / "blog-examples"
MAX_EXAMPLES = 20
PRUNE_COUNT = 5
def list_examples():
"""List all blog examples sorted by date (oldest first)."""
examples = []
for f in EXAMPLES_DIR.glob("*.md"):
# Extract date from filename (YYYY-MM-DD-slug.md)
try:
date_str = f.stem[:10]
date = datetime.strptime(date_str, "%Y-%m-%d")
examples.append((date, f.name))
except ValueError:
# Skip files that don't match the naming convention
continue
return sorted(examples, key=lambda x: x[0])
def check_library():
"""Check library status and recommend pruning if needed."""
examples = list_examples()
count = len(examples)
print(f"Blog Examples Library Status")
print(f"=" * 40)
print(f"Total examples: {count}")
print(f"Maximum allowed: {MAX_EXAMPLES}")
print()
if count > MAX_EXAMPLES:
print(f"⚠️ Library exceeds limit by {count - MAX_EXAMPLES} files")
print(f"Recommend removing the {PRUNE_COUNT} oldest examples:")
print()
for i, (date, name) in enumerate(examples[:PRUNE_COUNT]):
print(f" {i+1}. {name} ({date.strftime('%B %d, %Y')})")
else:
print(f"✓ Library is within limits ({MAX_EXAMPLES - count} slots available)")
print()
print("All examples (oldest first):")
print("-" * 40)
for date, name in examples:
print(f" {name}")
def prune_oldest(dry_run=True):
"""Remove the oldest examples to bring library under limit."""
examples = list_examples()
count = len(examples)
if count <= MAX_EXAMPLES:
print("Library is within limits. No pruning needed.")
return
to_remove = examples[:PRUNE_COUNT]
if dry_run:
print(f"DRY RUN - Would remove {len(to_remove)} files:")
else:
print(f"Removing {len(to_remove)} oldest files:")
for date, name in to_remove:
filepath = EXAMPLES_DIR / name
if dry_run:
print(f" Would remove: {name}")
else:
filepath.unlink()
print(f" Removed: {name}")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "prune":
dry_run = "--execute" not in sys.argv
prune_oldest(dry_run=dry_run)
else:
check_library()