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,560 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WeChat Cover Pair · Editorial Broadsheet</title>
<!--
Design direction: Editorial Broadsheet
Typography: Noto Serif SC Black (思源宋) headline + Noto Sans SC (思源黑) meta + Fraunces / Space Grotesk Latin
Color: pure white surface · #0a0a0a ink · #2a4a7c indigo accent · #b32d2d cinnabar mark
Modulus: 8pt grid · margins 5:8 · title broken by-word · 破折号 / 顿号 走中文规则
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700;900&family=Noto+Sans+SC:wght@300;400;500;700&family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ink:#0a0a0a;
--ink-soft:#3a3a38;
--muted:#6b6b66;
--paper:#ffffff;
--field:#ececea;
--line:#1a1a1a;
--hair:rgba(10,10,10,.16);
--indigo:#2a4a7c;
--cinnabar:#b32d2d;
--sans:"Noto Sans SC","PingFang SC","HarmonyOS Sans SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
--serif:"Noto Serif SC","Source Han Serif SC","Songti SC",serif;
--latin-serif:"Fraunces",Georgia,"Times New Roman",serif;
--latin-sans:"Space Grotesk","Helvetica Neue",Arial,sans-serif;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
min-height:100vh;
background:var(--field);
display:grid;
place-items:center;
padding:60px 24px;
font-family:var(--sans);
color:var(--ink);
-webkit-font-smoothing:antialiased;
text-rendering:geometricPrecision;
}
.board{
width:1200px;
background:var(--paper);
padding:56px 56px 48px;
display:grid;
gap:32px;
box-shadow:0 1px 0 rgba(0,0,0,.06);
}
/* ───────────────────── 报头 masthead ───────────────────── */
.masthead{
display:grid;
grid-template-columns:1fr auto 1fr;
align-items:end;
padding-bottom:18px;
border-bottom:2px solid var(--line);
}
.masthead .left{
font-family:var(--latin-sans);
font-size:11px;
letter-spacing:.24em;
text-transform:uppercase;
color:var(--ink-soft);
}
.masthead .center{
font-family:var(--latin-serif);
font-weight:700;
font-style:italic;
font-size:22px;
letter-spacing:-.01em;
color:var(--ink);
line-height:1;
}
.masthead .center small{
display:block;
font-style:normal;
font-family:var(--sans);
font-weight:400;
font-size:11px;
letter-spacing:.32em;
color:var(--muted);
margin-top:6px;
text-align:center;
}
.masthead .right{
text-align:right;
font-family:var(--latin-sans);
font-size:11px;
letter-spacing:.24em;
text-transform:uppercase;
color:var(--ink-soft);
}
.masthead .right b{
font-weight:600;
color:var(--ink);
}
/* ───────────────────── 主封 main cover ───────────────────── */
.main-cover{
position:relative;
display:grid;
grid-template-columns: 1fr 432px;
gap:40px;
padding:8px 0 0;
}
.main-cover .kicker{
grid-column:1 / -1;
display:flex;
justify-content:space-between;
align-items:center;
font-family:var(--latin-sans);
font-size:12px;
letter-spacing:.32em;
text-transform:uppercase;
color:var(--ink);
padding-bottom:18px;
border-bottom:1px solid var(--hair);
}
.main-cover .kicker .tag{
display:inline-flex;
align-items:center;
gap:10px;
}
.main-cover .kicker .tag::before{
content:"";
width:24px;
height:1px;
background:var(--cinnabar);
}
.main-cover .kicker .tag em{
font-style:normal;
font-family:var(--sans);
font-weight:500;
font-size:13px;
letter-spacing:.18em;
color:var(--ink);
}
.main-cover .kicker .meta{
font-family:var(--sans);
font-weight:400;
font-size:12px;
letter-spacing:.18em;
color:var(--muted);
}
.main-cover .text{
padding-top:34px;
}
.main-cover h1{
margin:0;
font-family:var(--serif);
font-weight:900;
font-size:78px;
line-height:1.12;
letter-spacing:-.02em;
color:var(--ink);
text-wrap:balance;
}
.main-cover h1 .em{
color:var(--indigo);
font-style:normal;
}
.main-cover .deck{
margin:34px 0 0;
padding-top:24px;
border-top:1px solid var(--hair);
max-width:520px;
font-family:var(--sans);
font-weight:400;
font-size:18px;
line-height:1.75;
color:var(--ink-soft);
letter-spacing:.01em;
}
.main-cover .byline{
margin-top:36px;
display:flex;
gap:24px;
align-items:baseline;
font-family:var(--sans);
font-size:13px;
color:var(--muted);
letter-spacing:.08em;
}
.main-cover .byline b{
color:var(--ink);
font-weight:500;
}
.main-cover .byline .sep{
width:1px;
height:12px;
background:var(--hair);
}
/* 右侧图块 */
.main-cover .figure{
position:relative;
height:520px;
overflow:hidden;
background:#111;
}
.main-cover .figure img{
width:100%;
height:100%;
object-fit:cover;
display:block;
filter:grayscale(.18) contrast(1.02);
}
.main-cover .figure figcaption{
position:absolute;
left:0;
right:0;
bottom:0;
padding:14px 16px;
background:linear-gradient(to top, rgba(0,0,0,.72), rgba(0,0,0,0));
color:#fff;
font-family:var(--sans);
font-size:11px;
letter-spacing:.18em;
text-transform:uppercase;
display:flex;
justify-content:space-between;
}
.main-cover .figure .stamp{
position:absolute;
top:14px;
left:14px;
width:46px;
height:46px;
border:1px solid rgba(255,255,255,.65);
border-radius:50%;
display:grid;
place-items:center;
font-family:var(--latin-serif);
font-style:italic;
font-size:18px;
color:#fff;
}
/* 折页/页脚 */
.colophon{
display:grid;
grid-template-columns: 1fr 1fr 1fr;
border-top:1px solid var(--line);
padding-top:16px;
margin-top:4px;
font-family:var(--sans);
font-size:12px;
color:var(--muted);
letter-spacing:.06em;
}
.colophon span{display:block;font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);margin-bottom:4px}
.colophon b{color:var(--ink);font-weight:500;font-size:13px;letter-spacing:0}
.colophon .mid{text-align:center}
.colophon .end{text-align:right}
/* ───────────────────── 一对 pair ───────────────────── */
.pair{
display:grid;
grid-template-columns: 1fr 1fr;
gap:32px;
}
/* 分享图 share */
.share{
position:relative;
background:var(--paper);
border:1px solid var(--hair);
padding:36px 32px 28px;
min-height:380px;
display:flex;
flex-direction:column;
justify-content:space-between;
overflow:hidden;
}
.share .corner{
position:absolute;
top:0;
right:0;
width:88px;
height:88px;
background:var(--indigo);
display:grid;
place-items:center;
color:#fff;
font-family:var(--latin-serif);
font-style:italic;
font-weight:300;
font-size:42px;
line-height:1;
letter-spacing:-.02em;
}
.share .head{
font-family:var(--latin-sans);
font-size:11px;
letter-spacing:.32em;
text-transform:uppercase;
color:var(--muted);
}
.share h2{
margin:18px 0 0;
font-family:var(--serif);
font-weight:700;
font-size:40px;
line-height:1.25;
letter-spacing:-.01em;
color:var(--ink);
max-width:380px;
}
.share p{
margin:18px 0 0;
font-family:var(--sans);
font-size:14px;
line-height:1.7;
color:var(--ink-soft);
max-width:360px;
}
.share .foot{
display:flex;
justify-content:space-between;
align-items:center;
margin-top:28px;
padding-top:14px;
border-top:1px solid var(--hair);
font-family:var(--sans);
font-size:12px;
color:var(--muted);
letter-spacing:.06em;
}
.share .foot b{color:var(--ink);font-weight:500}
.share .rule{
display:inline-block;
width:36px;
height:2px;
background:var(--cinnabar);
margin-top:4px;
}
/* Safe area sample */
.safe{
position:relative;
padding:0;
min-height:380px;
display:grid;
grid-template-rows: 180px 1fr;
background:#111;
color:#fff;
overflow:hidden;
}
.safe .img{
position:relative;
overflow:hidden;
}
.safe .img img{
width:100%;
height:100%;
object-fit:cover;
filter:grayscale(1) contrast(1.05);
opacity:.85;
}
.safe .img::after{
content:"";
position:absolute;
inset:0;
background:linear-gradient(to bottom, rgba(0,0,0,.15), rgba(0,0,0,.55));
}
.safe .img .label{
position:absolute;
top:14px;
left:16px;
font-family:var(--latin-sans);
font-size:11px;
letter-spacing:.32em;
text-transform:uppercase;
color:#fff;
display:flex;
align-items:center;
gap:10px;
}
.safe .img .label::before{
content:"";
width:6px;
height:6px;
background:#fff;
border-radius:50%;
}
.safe .body{
padding:24px 28px 22px;
display:flex;
flex-direction:column;
justify-content:space-between;
}
.safe h2{
margin:0;
font-family:var(--serif);
font-weight:700;
font-size:30px;
line-height:1.3;
letter-spacing:-.005em;
color:#fff;
}
.safe h2 .accent{color:#e2b35a;font-style:normal}
.safe .legend{
margin-top:14px;
display:grid;
grid-template-columns:auto 1fr;
gap:6px 14px;
font-family:var(--sans);
font-size:12px;
color:#bcbcb8;
letter-spacing:.04em;
}
.safe .legend b{color:#fff;font-weight:500}
/* ───────────────────── 脚注 footer note ───────────────────── */
.note{
margin-top:8px;
padding-top:18px;
border-top:1px solid var(--hair);
display:grid;
grid-template-columns: 1fr auto;
gap:24px;
align-items:start;
font-family:var(--sans);
font-size:12px;
color:var(--muted);
line-height:1.7;
letter-spacing:.02em;
}
.note .body p{margin:0}
.note .body p + p{margin-top:4px}
.note .body b{color:var(--ink);font-weight:500}
.note .iso{
font-family:var(--latin-sans);
font-size:11px;
letter-spacing:.24em;
text-transform:uppercase;
color:var(--ink-soft);
align-self:center;
}
</style>
</head>
<body>
<main class="board" data-template="wechat-cover-pair" data-direction="editorial-broadsheet">
<!-- masthead -->
<header class="masthead">
<div class="left">Vol.&nbsp;04 &nbsp;·&nbsp; 2026 Spring</div>
<div class="center">
The Cover Pair
<small>微 信 公 众 号 · 封 面 范 式</small>
</div>
<div class="right"><b>Main</b> + Share &nbsp;Safe</div>
</header>
<!-- main cover -->
<section class="main-cover" aria-label="main cover">
<div class="kicker">
<span class="tag"><em>公 众 号 首 图</em></span>
<span class="meta">封面专题 / Cover Story · 1080 × 600</span>
</div>
<div class="text">
<h1>
AI Agent 落地<span class="em">,</span><br>
不是多接<br>
一个<span class="em">聊天框</span>
</h1>
<p class="deck">
微信封面要在缩略图里依然读清标题——<br>
更稳、更克制,不按小红书 3∶4 的逻辑硬套。
</p>
<div class="byline">
<span>撰文 / <b>编辑部</b></span>
<span class="sep"></span>
<span>视觉 / <b>Studio Pair</b></span>
<span class="sep"></span>
<span>2026 · 04</span>
</div>
</div>
<figure class="figure">
<span class="stamp">P</span>
<img
src="https://images.pexels.com/photos/1160831/pexels-photo-1160831.jpeg?auto=compress&cs=tinysrgb&w=900"
alt="Black and White Escalators · Pexels #1160831">
<figcaption>
<span>Photograph · Pexels</span>
<span>No. 1160831</span>
</figcaption>
</figure>
</section>
<!-- colophon strip -->
<div class="colophon">
<div class="start"><span>账号</span><b>The Pair Studio</b></div>
<div class="mid"><span>栏目</span><b>编辑笔记 · Editor's Note</b></div>
<div class="end"><span>期号</span><b>2026 / ISSUE 04</b></div>
</div>
<!-- pair: share + safe -->
<section class="pair">
<article class="share">
<div class="corner">04</div>
<div>
<div class="head">Share · 分 享 图</div>
<h2>标题更短,<br>主旨不丢。</h2>
<span class="rule"></span>
<p>
用于二级封面、对外转发与缩略图场景:去掉副标,<br>
让书名号《》与人物名等关键信息留在安全区。
</p>
</div>
<div class="foot">
<span>SHARE COVER · 800 × 800</span>
<b>The Pair Studio</b>
</div>
</article>
<article class="safe">
<div class="img">
<span class="label">Safe Area</span>
<img
src="https://images.pexels.com/photos/911738/pexels-photo-911738.jpeg?auto=compress&cs=tinysrgb&w=900"
alt="Oculus, NYC — Pexels / Santiago Calatrava">
</div>
<div class="body">
<h2>
更大字号<span class="accent">·</span><br>
更少装饰<span class="accent">·</span><br>
更克制的版心。
</h2>
<dl class="legend">
<b>版心</b><span>左右各留 1 字位,标题不顶天</span>
<b>断行</b><span>按词断;"的 / 和"不入行首行末</span>
<b>层级</b><span>字号 ≫ 字重 ≫ 颜色</span>
</dl>
</div>
</article>
</section>
<!-- footer note -->
<footer class="note">
<div class="body">
<p><b>排版说明</b> 主封面参考"杂志报头 + 大字标题 + 副标分隔线"的 Editorial Broadsheet 范式:思源宋 Black 担当大字标题,思源黑 Regular 收尾副文,西文用 Fraunces 与 Space Grotesk 共同呼吸。</p>
<p>标题按词手工断行,"的 / 地 / 得 / 之 / 所"不入行首,"和 / 与 / 及 / 或"不入行末;破折号占两字、连写不拆。</p>
</div>
<div class="iso">ISO &nbsp;Pattern&nbsp; 04</div>
</footer>
</main>
</body>
</html>