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

@@ -0,0 +1,59 @@
"""Generate fig_04_ablation_panels.pdf — 2x2 ablation panel figure."""
import os
import matplotlib.pyplot as plt
import numpy as np
from style_kit import PALETTE, add_simulated_watermark, apply_publication_style, save_publication_bundle
apply_publication_style()
PRIMARY = PALETTE["blue_main"]
ABLATION = PALETTE["red_strong"]
DATASETS = ["ETTm1", "Electricity", "Traffic", "Weather"]
horizons = np.array([96, 192, 336, 720])
full = {
"ETTm1": np.array([0.319, 0.346, 0.385, 0.460]),
"Electricity": np.array([0.131, 0.142, 0.149, 0.171]),
"Traffic": np.array([0.352, 0.378, 0.402, 0.451]),
"Weather": np.array([0.179, 0.198, 0.220, 0.272]),
}
no_patch = {
"ETTm1": np.array([0.354, 0.382, 0.422, 0.499]),
"Electricity": np.array([0.143, 0.155, 0.164, 0.186]),
"Traffic": np.array([0.378, 0.408, 0.434, 0.486]),
"Weather": np.array([0.198, 0.221, 0.247, 0.301]),
}
fig, axes = plt.subplots(2, 2, figsize=(6.6, 4.4), sharex=True)
for ax, name in zip(axes.flat, DATASETS):
ax.plot(horizons, full[name], marker="o", color=PRIMARY, label="Full")
ax.plot(horizons, no_patch[name], marker="s", linestyle="--",
color=ABLATION, label=r"$-$ patching")
ax.set_title(name)
ax.set_xscale("log", base=2)
ax.set_xticks(horizons)
ax.set_xticklabels([str(h) for h in horizons])
ax.grid(True, alpha=0.25, linewidth=0.4)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
for ax in axes[:, 0]:
ax.set_ylabel("MSE")
for ax in axes[-1, :]:
ax.set_xlabel(r"Horizon $H$")
handles, labels = axes[0, 0].get_legend_handles_labels()
fig.legend(handles, labels, loc="upper center", bbox_to_anchor=(0.5, 1.02),
ncol=2, frameon=False)
for ax, label in zip(axes.flat, "abcd"):
ax.text(-0.15, 1.05, f"({label})", transform=ax.transAxes,
fontweight="bold", fontsize=10)
add_simulated_watermark(fig.axes[-1], y=-0.22)
plt.tight_layout(rect=(0.0, 0.01, 1.0, 0.95))
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fig_04_ablation_panels")
save_publication_bundle(fig, out)
plt.close(fig)
print(f"Wrote {out}.pdf/.svg/.tiff")