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

+250
View File
@@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>深夜篝火 · a fire at midnight</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" fetchpriority="high"
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Noto+Sans+JP:wght@900&display=swap">
<link rel="stylesheet" media="print" onload="this.media='all'"
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Noto+Sans+JP:wght@900&display=swap">
<noscript><link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Noto+Sans+JP:wght@900&display=swap"></noscript>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{
background:#111;
min-height:100vh;
display:flex;align-items:center;justify-content:center;
padding:24px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue',sans-serif;
-webkit-font-smoothing:antialiased;
}
.frame{
position:relative;
height:min(94vh, 1644px);
aspect-ratio: 1280 / 1644;
background:#000;
border:1px solid #1d1d1d;
box-shadow:0 30px 80px rgba(0,0,0,.6);
overflow:hidden;
color:#fff;
container-type:size;
}
canvas.bg{
position:absolute;inset:0;
width:100%;height:100%;
image-rendering:pixelated;
image-rendering:-moz-crisp-edges;
image-rendering:crisp-edges;
z-index:0;
display:block;
}
img.src{display:none}
.vignette{
position:absolute;inset:0;z-index:1;pointer-events:none;
background:linear-gradient(180deg,
rgba(0,0,0,.55) 0%, rgba(0,0,0,0) 18%,
rgba(0,0,0,0) 70%, rgba(0,0,0,.45) 100%);
}
.layer{
position:absolute;inset:0;z-index:2;
padding:3.6cqh 4.4cqh;
display:flex;flex-direction:column;
pointer-events:none;
}
.layer *{text-shadow:0 0 .8cqh rgba(0,0,0,.85)}
/* === Top corners === */
.top{
display:flex;justify-content:space-between;align-items:center;
font-size:.95cqh;letter-spacing:.32em;font-weight:500;text-transform:uppercase;
}
.top .corner-r .cn{
font-family:'PingFang SC','Heiti SC',sans-serif;
margin-left:1.4em;letter-spacing:.32em;
}
hr.divider{
margin-top:1.1cqh;
border:none;height:1px;background:rgba(255,255,255,.45);
}
/* === Title 2×2:两列独立纵向 stack(chinese-typography #2 按词组结构 /
#5 行距合理 / #7 中文密排不加字距 / #8 字重字号联动) === */
.title-block{margin-top:2.6cqh}
.title-grid{
display:inline-flex; /* 不撑满父级宽度,列贴左 */
align-items:flex-start; /* 两列顶端对齐:夜 与 深 顶部同线 */
gap:0; /* 列间贴合,跟目标图一致 */
font-family:'GFutoGoB101Pro-Bold','Noto Sans JP','PingFang SC','Heiti SC',sans-serif;
font-weight:900;
color:#fff;
line-height:1.1; /* chinese-typography #5:纵向 ≥1.0 才有可读气口;
原 0.95 让相邻字底/顶贴死 */
letter-spacing:0; /* #7:字间距原则上不动,横向交给字体 side-bearing */
}
.title-grid .col{display:flex;flex-direction:column}
.title-grid .sm,
.title-grid .lg{font-size:12cqh} /* 四字同号 */
.title-en{
display:block;margin-top:1cqh;
font-style:italic;font-weight:400;
font-size:1.9cqh;letter-spacing:.04em;
text-transform:lowercase;
}
/* === Center stack === */
.center-stack{
position:absolute;
left:50%;top:58%;
transform:translate(-50%,-50%);
display:flex;flex-direction:column;align-items:center;
gap:1.3cqh;
z-index:3;
pointer-events:none;
}
.brackets{
position:relative;
padding:1cqh 2.6cqh;
font-weight:500;font-size:1.1cqh;letter-spacing:.36em;
text-transform:uppercase;color:#fff;
text-shadow:0 0 .8cqh rgba(0,0,0,.9);
}
.brackets::before,.brackets::after{
content:'';position:absolute;
width:1.3cqh;height:1.3cqh;border:1px solid #fff;
}
.brackets::before{top:0;left:0;border-right:none;border-bottom:none}
.brackets::after{bottom:0;right:0;border-left:none;border-top:none}
/* === Footer === */
.bottom{
margin-top:auto;
display:flex;justify-content:space-between;align-items:flex-end;
}
.wordmark{
font-family:'PingFang SC','Heiti SC',sans-serif;
font-weight:900;font-size:2.6cqh;letter-spacing:.45em;color:#fff;
text-shadow:0 0 1cqh rgba(0,0,0,.9);
}
.credit{
font-size:.85cqh;letter-spacing:.28em;
text-align:right;line-height:1.7;text-transform:uppercase;font-weight:500;
color:#fff;
}
.credit .cn{
display:block;
font-family:'PingFang SC','Heiti SC',sans-serif;
letter-spacing:.22em;
}
</style>
</head>
<body>
<div class="frame">
<canvas class="bg" id="bg" width="600" height="771"></canvas>
<img class="src" id="src" alt="" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/4gIcSUNDX1BST0ZJTEUAAQEAAAIMbGNtcwIQAABtbnRyUkdCIFhZWiAH3AABABkAAwApADlhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApkZXNjAAAA/AAAAF5jcHJ0AAABXAAAAAt3dHB0AAABaAAAABRia3B0AAABfAAAABRyWFlaAAABkAAAABRnWFlaAAABpAAAABRiWFlaAAABuAAAABRyVFJDAAABzAAAAEBnVFJDAAABzAAAAEBiVFJDAAABzAAAAEBkZXNjAAAAAAAAAANjMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAElYAABYWVogAAAAAAAA9tYAAQAAAADTLVhZWiAAAAAAAAADFgAAAzMAAAKkWFlaIAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPY3VydgAAAAAAAAAaAAAAywHJA2MFkghrC/YQPxVRGzQh8SmQMhg7kkYFUXdd7WtwegWJsZp8rGm/fdPD6TD////bAEMACQYGCAYFCQgHCAoJCQoNFg4NDAwNGhMUEBYfHCEgHxweHiMnMiojJS8lHh4rOywvMzU4ODghKj1BPDZBMjc4Nf/bAEMBCQoKDQsNGQ4OGTUkHiQ1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1Nf/CABEICV0GQAMBIgACEQEDEQH/xAAbAAEBAQEBAQEBAAAAAAAAAAAAAQIDBAUGB//EABoBAQEBAQEBAQAAAAAAAAAAAAABAgMEBQb/2gAMAwEAAhEDEQAAAfxA1AQAAAAAAUAAAAAAAAAAAAKJQAAACgAAAQsFAAAAAAALAAAAAAACwAAWUJQQoCUACACWgAAAAIsCiKWKSAAUIsLALICgAAAAAAAAAUAIAAAASiLAAAFAAJQljIUEAAALAolAAAAAAAAAAAAUlAAAAKAAAABFAAAAAAAECgFlJQELALAolACLAUAAAASgACUAAAgKAAAAAASoCgAgKASiACAAoAAAAAAAAIBQAAAIogAAAUCLCCAAFAAAAAAAAAAAAAAUigAAAAKAAABBSUAAAAAAAsFAIBQELKJQAAAAABAUAAAAAAAAAAAAAAAAAICgAIoAAiiKIAAAAAAAAAAAAIBQAAEoiiABQAMiAFlAAAAAAAAAAAABSUAAAAAoAAAEWUAAAAAAAACxZQIEqgAABAAAAAAAAAAAAUAAEBQAAAAgKACAoAAAAAAAAACLAVJRYsFQFIsAACwCAAoIBQAEoASiCAtyIFAAAAAAAAABSUIohQAAAAAKAAAABKAAAAAAAAAEUoBKAAQAAAAAAAAAAAAAsAAAAAUAAAAAAAAAAAAAEABQAJRAAAAAAVKIUSwAsAICggAFAAASiAglAAAAAAAAAAWUIKlAAAAAAAoAAAAChAAAAAAAAQKoAAgKBAAAFlEohSAKIUgFlEolQoEoigCKEpUEKWLAAEBQAAAQAAAAAAAAAAAAAAAAACKWKIsAAAAAlAAAAyJQAAAAAAABSALAoAAAAAAAACgABQEAAAAAAAACihKQAABZUiiWUiqCAoAAIAAAACggAAAAAAABKIolBKIoSlgQUiwsUlBFIsAAAAAAAAAAAAAAAWKIsACiAACUADIlAAAAAAAAoAAAAAAAAAAoAAABQBAAAAAAChFIqgQAAAAVAAAoAAAEBQAAAAAgAAAAAAAAAAAAAACUAAAAEoAihLAogCiKIoiiLAAAAAABKWKJQShKIAJQMiUAAAAAAUlAAAAAAAAAAAKAAAAqUBAAAAAFlAAAsAACAoUikCgAAAQAAVUAEAAAABSiKiLAsAACwAAAAAAAAAAAAAAAAAAACgAAgCLAUgAAAAUAAACLIBciUAAAAolCUAAAAAAoAAAAAAAAsKAEAAAAAoAAsAAAAAAUQAKAFSLAUihFJQiwKIoihKIoAAEFlAUIAlBKEoiiAAAALAAAAAAAAAAKBAAAAAUBKIqIogAAAAUAACLDIzoAAUigAAAAAAAKAAAAAFIUhSWVAAAABSAAUAAsAAAAAAUQAAKoQAAAAAAAAAAAAAAAAAIBQAAAAoIAlAAAABLAsBSAACwAAoiiFIAAAAAJQIogAAAUAADAzoAUAAAAAACgAAABZJSoolAFSkAAAAKJQAhSUAAoEAAAAAoAQAsoVJQAAABAUAAAEAAAFIsAUAAAAAAAAAAAFBAAAlAAASywBZQAAAACUAIUgAAAUIAiiLAAAFAwXOkoAAAAAAACgAABZAoAAAUiiAAUJQAAAAAAACwAAAAEoAFlAsAAABBSFIUiwKAIolAKSiKJSIoilSiLAAsAAABSAAAAAAACUBKslAAAAEAAAABQAIoiwABQAgCKEogUDIzoAAAAAAAKAAFIpAAABSUAAAQLQAgKBAAAlCgAQUgCiKQFBAFKBAAFlQAKAAAAAAAAAAACAAAAUEBYpAUAEBYsABSAFIoQAAAAQoiwALKFIAIABQAIsAAAlAAlCLAFyM6AAAAACgAACkAAAAqUAACwAAAAAAAAUgACwKAAAAABUAACwUBAoAAAAAAAAAAsAAAAAgAAAAAAAAAAAFAlCKIsAABQEAACgAAIoiiLIABQAIsCwABQgCAyM7AAAACgAAFlQAAAUlAAALAgKAAAAAAFJSBKFAAAAAABSKQAKWVAQKAAAKIUihKIpAAEpUpIsUsABSKIsAgAsCwChSLICggAAAAAAAAAAAKAAAAAAAiwLAAJQEogBViyAAMCbAAAAAACBaBAACgAAALAAAAAAAFgsCoLKEoAAAWUSiUIoBAAoVJQCwAUlCUQAAAAAAAAAFiiUSUAEpUqAAqUSUAAAVKIoiiLIAFIKCAAAoAAoiiLAoiwAAAASiCAAUBKIoiyUDAmwAAAAAgVAoAAUlAAALAAAAAAABRKIolAAAAAUlAAAAEWWgQALFgoRKAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAilSiKEpAEoAAAAAASiLFAASgIiwBQEoiiLJcCbAAAAWVAAAAAKAAAKBACwALAsQUBQAAAAAAFQLBQAWUSxAqhAARUpQBAABSFIoiiUJQASwAAAAAAAAFIUlCUIoSiKIAAoiwLAAAAAAAAAAAAAAAAACKVAAASiCAAUAF5jOwkACgWhAAAFgoAAAQKAAWUlAAAAEBQAQFFJQAAAFJQBIAWwAEClAEAAFIqggKCAABAogACiKIsCiKIsCgKABAUIAAAASgAAACAFIUSiKIohSFIUiwKIsAAAAJRYsAAAiLFAA5jGwCqgFFBAABQAEAC0EFIoAABAAAUEBVlAAAAQBZQBLCygAKBAAsUAAAQACgCgAAAgAAAAAAAAEBQAsKIogCiKIAAIAAAABQAAAAAAAoAAABKiKIAAABKIsUBKiABeas7igAAAABQAACgQBYFgoAAAQUgBSKWUABSFRKEogKFBAoAEAAAVLKAAAEAAWUAAAACwAAJQAAAQAKCAqywoAAAAAIoiwLAAAoiiAACAoAAAsAAAAAUACKiAAAlFgAAiLFwM7AAAAAAWUACgAQUAAAABAALKJQAABbLCykAABQAsAABAAARUqgAABAAFlAAAsCAoAAIBQAQAKACAoBYKAAAAAAAAAAAAAAAAAAAAABKEogAAAAVKiAAAixQAjmM9AAAAAKAAKCAsCCqRSUABUllIoSgAAoSiUAAAAAUECiwVEqUikhVShKQAKAALEKJQABAoFBAlCwIAABQQAKALCwFlAAAAQsoqIFFIAAAEBQACrIoiwLAFAAACEUlCKIAAFSoiwAAixQOYx0AAWUiwUAAAoEAoAAAARYKAAAUigAAAKACBagFBKQCUAAAAAAQKKIsLKQAAALAlAAAABAAUAAEBQRYqyiKAAAASigAAJQSoiqASiKiKoAAAAABKIoiiBQgACLAAJUogAAIsXmMdAAFAAAAALACgAAAAUlEAAAoAAAAClirJQABACwAAAAFIoiiLAEFqUQAAAKBAgFAAABAFhQAAAAAsoAAAAASigAAAAAAAAAAAAAAAAEolBLAogUISiACAVKIogAXkMbWVZSggAAAKBFAAACglAEABQQoBQQKAqWAoABZSAFEsBUihKIACwKSqlhKQKBABbIIAACglAABCiBSwKIAAAAAACoSihSUQKAAALAsAAAAACiLCgSkSrYoiyAAAAEsAlASiKIAISliwA5Kz0AAABQgKAFQAAACpQAELAsKlUAAEsqooAAAALACgABAAIohQKBCiAABBbAIsBZYpAqFiKqKiKAWLACoAAAABVABIoBFlsCgCiWVAAAAVKAAQAAAKAAAAElsolQAASiCUACKIsAEqWLDlTPQAAAAACpQAABZRKAAQoigBKUAAEWWgAABSUEoAABAUAECgQACoSgiiLAWyUAAlCwAAAAAJQAIolCUEpQglAolAQBZbkKsChAAAAAAAAAAAoABYAAAAEqAIsUBKIoglAAiiACXkM9AAAABRKJQAAFAAAQUAAABQFlEpAWwoVAAAAAAQAAAAAKKIVAQAAAALAAAABRKsAglACUFALAAAIACgFlIEKsFsABACwUIAUiqiwKIoiiAAUIolgAKEUgAgBLFAASiFIJUoiwA5DHUAAAAUAlAABZRKAQABYKAFABKlAApZQAAAAAEAACggAKFAQAEAACwAUiwAWUABAoQLine truncated
<div class="vignette"></div>
<div class="layer">
<header class="top">
<span class="corner-l">No 04 / 2026</span>
<span class="corner-r">CAMPING<span class="cn">夜火</span></span>
</header>
<hr class="divider">
<div class="title-block">
<div class="title-grid" aria-label="深夜篝火">
<div class="col" aria-hidden="true">
<span class="sm">深</span>
<span class="sm">篝</span>
</div>
<div class="col" aria-hidden="true">
<span class="lg">夜</span>
<span class="lg">火</span>
</div>
</div>
<span class="title-en">a fire at midnight</span>
</div>
<footer class="bottom">
<span class="wordmark">夜&nbsp;火</span>
<span class="credit">
<span>Est 2026</span>
<span class="cn">小红书号 955873829</span>
</span>
</footer>
</div>
<div class="center-stack">
<div class="brackets">VOL IV&nbsp;&nbsp;/&nbsp;&nbsp;2026</div>
</div>
</div>
<script>
(async function(){
if (document.fonts && document.fonts.ready){
try { await document.fonts.ready; } catch(e){}
}
const img = document.getElementById('src');
const canvas = document.getElementById('bg');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const ready = (img.complete && img.naturalWidth > 0)
? Promise.resolve()
: new Promise((res, rej) => { img.onload = res; img.onerror = rej; });
try { await ready; } catch(e){ console.warn('img failed', e); return; }
// cover-fit
const ratio = Math.max(W / img.naturalWidth, H / img.naturalHeight);
const dw = img.naturalWidth * ratio, dh = img.naturalHeight * ratio;
ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh);
adjustBrightness(ctx, W, H, -25);
adjustLevels(ctx, W, H, 70, 195);
bayerDither(ctx, W, H);
function adjustLevels(ctx, W, H, lo, hi){
const d = ctx.getImageData(0,0,W,H), px = d.data, scale = 255 / (hi - lo);
for (let i=0; i<px.length; i+=4)
for (let c=0; c<3; c++){ let v=(px[i+c]-lo)*scale; px[i+c]=v<0?0:v>255?255:v; }
ctx.putImageData(d,0,0);
}
function adjustBrightness(ctx, W, H, delta){
const d = ctx.getImageData(0,0,W,H), px = d.data;
for (let i=0; i<px.length; i+=4)
for (let c=0; c<3; c++){ let v=px[i+c]+delta; px[i+c]=v<0?0:v>255?255:v; }
ctx.putImageData(d,0,0);
}
function bayerDither(ctx, W, H){
const d = ctx.getImageData(0,0,W,H), px = d.data;
const m = [
[ 0,32, 8,40, 2,34,10,42],[48,16,56,24,50,18,58,26],
[12,44, 4,36,14,46, 6,38],[60,28,52,20,62,30,54,22],
[ 3,35,11,43, 1,33, 9,41],[51,19,59,27,49,17,57,25],
[15,47, 7,39,13,45, 5,37],[63,31,55,23,61,29,53,21]
];
for (let y=0; y<H; y++)
for (let x=0; x<W; x++){
const i=(y*W+x)*4;
const luma=(0.299*px[i]+0.587*px[i+1]+0.114*px[i+2])/255;
const t=(m[y%8][x%8]+0.5)/64;
const v=luma>t?255:0;
px[i]=px[i+1]=px[i+2]=v; px[i+3]=255;
}
ctx.putImageData(d,0,0);
}
})();
</script>
</body>
</html>