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:
1 parent
7acc0c5451
commit
e45b68bb8b
1089 files changed
+260614
No files matched your search
+102
@@ -0,0 +1,102 @@
|
||||
"""Shared publication-style helpers for experiment-suite figures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
PALETTE = {
|
||||
"blue_main": "#0F4D92",
|
||||
"blue_secondary": "#3775BA",
|
||||
"green_1": "#DDF3DE",
|
||||
"green_2": "#AADCA9",
|
||||
"green_3": "#8BCF8B",
|
||||
"red_1": "#F6CFCB",
|
||||
"red_2": "#E9A6A1",
|
||||
"red_strong": "#B64342",
|
||||
"neutral_light": "#CFCECE",
|
||||
"neutral_mid": "#767676",
|
||||
"neutral_dark": "#4D4D4D",
|
||||
"neutral_black": "#272727",
|
||||
"teal": "#42949E",
|
||||
"violet": "#9A4D8E",
|
||||
}
|
||||
|
||||
DEFAULT_COLORS = [
|
||||
PALETTE["blue_main"],
|
||||
PALETTE["green_3"],
|
||||
PALETTE["red_strong"],
|
||||
PALETTE["teal"],
|
||||
PALETTE["violet"],
|
||||
PALETTE["neutral_mid"],
|
||||
]
|
||||
|
||||
PALETTE_NMI_PASTEL = [
|
||||
"#484878",
|
||||
"#7884B4",
|
||||
"#B4C0E4",
|
||||
"#E4E4F0",
|
||||
"#E4CCD8",
|
||||
"#F0C0CC",
|
||||
]
|
||||
|
||||
|
||||
def apply_publication_style(font_size: int = 9, axes_linewidth: float = 0.6) -> None:
|
||||
"""Apply a shared journal-style matplotlib configuration."""
|
||||
plt.rcParams.update({
|
||||
"font.family": "sans-serif",
|
||||
"font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans"],
|
||||
"font.size": font_size,
|
||||
"axes.labelsize": font_size,
|
||||
"axes.titlesize": font_size + 1,
|
||||
"xtick.labelsize": max(font_size - 1, 6),
|
||||
"ytick.labelsize": max(font_size - 1, 6),
|
||||
"legend.fontsize": max(font_size - 1, 6),
|
||||
"axes.spines.right": False,
|
||||
"axes.spines.top": False,
|
||||
"axes.linewidth": axes_linewidth,
|
||||
"axes.grid": True,
|
||||
"grid.alpha": 0.25,
|
||||
"grid.linewidth": 0.4,
|
||||
"lines.linewidth": 1.4,
|
||||
"lines.markersize": 4.5,
|
||||
"savefig.dpi": 300,
|
||||
"savefig.bbox": "tight",
|
||||
"svg.fonttype": "none",
|
||||
"pdf.fonttype": 42,
|
||||
"ps.fonttype": 42,
|
||||
})
|
||||
|
||||
|
||||
def add_panel_label(ax, label: str, x: float = -0.12, y: float = 1.03) -> None:
|
||||
ax.text(
|
||||
x, y, label,
|
||||
transform=ax.transAxes,
|
||||
fontsize=10,
|
||||
fontweight="bold",
|
||||
ha="left",
|
||||
va="bottom",
|
||||
)
|
||||
|
||||
|
||||
def add_simulated_watermark(ax, x: float = 0.99, y: float = 0.02) -> None:
|
||||
ax.text(
|
||||
x, y, "simulated",
|
||||
transform=ax.transAxes,
|
||||
ha="right",
|
||||
va="bottom",
|
||||
fontsize=7,
|
||||
color="gray",
|
||||
alpha=0.7,
|
||||
style="italic",
|
||||
)
|
||||
|
||||
|
||||
def save_publication_bundle(fig, out_base: str, dpi: int = 600) -> None:
|
||||
"""Save PDF/SVG/TIFF together when possible."""
|
||||
out_path = Path(out_base)
|
||||
fig.savefig(out_path.with_suffix(".pdf"))
|
||||
fig.savefig(out_path.with_suffix(".svg"))
|
||||
fig.savefig(out_path.with_suffix(".tiff"), dpi=dpi)
|
||||
Reference in new issue
Block a user