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
+250
@@ -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">夜 火</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 / 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>
|
||||
Reference in new issue
Block a user