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
+160
@@ -0,0 +1,160 @@
|
||||
/* Reusable Guizang-style electronic magazine background.
|
||||
Copy or inline into generated HTML. It renders a deterministic WebGL ink-flow
|
||||
canvas and falls back to 2D if WebGL is unavailable. */
|
||||
(function () {
|
||||
function rgb(input, fallback) {
|
||||
return Array.isArray(input) && input.length === 3 ? input.map((v) => v / 255) : fallback;
|
||||
}
|
||||
|
||||
function fallback2d(canvas, opts) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
const w = rect.width;
|
||||
const h = rect.height;
|
||||
const ink = opts.inkCss || "rgba(26,46,31,.22)";
|
||||
const accent = opts.accentCss || "rgba(46,107,79,.18)";
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x = w * (0.14 + ((i * 0.19 + 0.17) % 0.72));
|
||||
const y = h * (0.10 + ((i * 0.29 + 0.23) % 0.78));
|
||||
const r = Math.max(w, h) * (0.22 + i * 0.045);
|
||||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||||
g.addColorStop(0, i % 2 ? accent : ink);
|
||||
g.addColorStop(1, "rgba(0,0,0,0)");
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
ctx.globalAlpha = 0.09;
|
||||
ctx.strokeStyle = opts.inkCss || "rgba(26,46,31,.22)";
|
||||
for (let i = 0; i < 18; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(w * (0.5 + Math.sin(i) * 0.34), h * (0.5 + Math.cos(i * 1.7) * 0.28), 120 + i * 18, 42 + i * 9, i * 0.37, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function mount(canvas, options) {
|
||||
const opts = options || {};
|
||||
const gl = canvas.getContext("webgl", { alpha: true, antialias: true, preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
fallback2d(canvas, opts);
|
||||
return { mode: "2d" };
|
||||
}
|
||||
|
||||
const vert = `
|
||||
attribute vec2 aPos;
|
||||
void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
`;
|
||||
const frag = `
|
||||
precision mediump float;
|
||||
uniform vec2 uRes;
|
||||
uniform float uTime;
|
||||
uniform vec3 uInk;
|
||||
uniform vec3 uPaper;
|
||||
uniform vec3 uAccent;
|
||||
uniform float uStrength;
|
||||
|
||||
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
|
||||
float noise(vec2 p){
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(
|
||||
mix(hash(i + vec2(0.0,0.0)), hash(i + vec2(1.0,0.0)), u.x),
|
||||
mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), u.x),
|
||||
u.y
|
||||
);
|
||||
}
|
||||
float fbm(vec2 p){
|
||||
float v = 0.0;
|
||||
float a = 0.5;
|
||||
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
||||
for(int i=0; i<5; i++){
|
||||
v += a * noise(p);
|
||||
p = m * p * 1.18;
|
||||
a *= 0.52;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
void main(){
|
||||
vec2 uv = gl_FragCoord.xy / uRes.xy;
|
||||
vec2 p = (gl_FragCoord.xy - 0.5 * uRes.xy) / min(uRes.x, uRes.y);
|
||||
float t = uTime * 0.045;
|
||||
float n = fbm(p * 2.2 + vec2(t, -t * 0.7));
|
||||
float n2 = fbm(p * 5.4 - vec2(t * 1.7, t));
|
||||
float contour = smoothstep(0.015, 0.0, abs(fract((n + n2 * 0.35) * 7.0) - 0.5));
|
||||
float wash = smoothstep(0.22, 0.88, n);
|
||||
float vignette = 1.0 - smoothstep(0.28, 0.92, length(p));
|
||||
vec3 col = mix(uPaper, uInk, wash * 0.42 + contour * 0.18);
|
||||
col = mix(col, uAccent, (0.18 + 0.25 * n2) * contour);
|
||||
float alpha = uStrength * (0.18 + wash * 0.42 + contour * 0.30) * (0.55 + 0.45 * vignette);
|
||||
gl_FragColor = vec4(col, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
function shader(type, source) {
|
||||
const sh = gl.createShader(type);
|
||||
gl.shaderSource(sh, source);
|
||||
gl.compileShader(sh);
|
||||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh));
|
||||
return sh;
|
||||
}
|
||||
|
||||
const program = gl.createProgram();
|
||||
gl.attachShader(program, shader(gl.VERTEX_SHADER, vert));
|
||||
gl.attachShader(program, shader(gl.FRAGMENT_SHADER, frag));
|
||||
gl.linkProgram(program);
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
|
||||
gl.useProgram(program);
|
||||
|
||||
const buffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
|
||||
const loc = gl.getAttribLocation(program, "aPos");
|
||||
gl.enableVertexAttribArray(loc);
|
||||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
const uRes = gl.getUniformLocation(program, "uRes");
|
||||
const uTime = gl.getUniformLocation(program, "uTime");
|
||||
const uInk = gl.getUniformLocation(program, "uInk");
|
||||
const uPaper = gl.getUniformLocation(program, "uPaper");
|
||||
const uAccent = gl.getUniformLocation(program, "uAccent");
|
||||
const uStrength = gl.getUniformLocation(program, "uStrength");
|
||||
const ink = rgb(opts.ink, [0.10, 0.18, 0.12]);
|
||||
const paper = rgb(opts.paper, [0.96, 0.94, 0.90]);
|
||||
const accent = rgb(opts.accent, [0.18, 0.42, 0.31]);
|
||||
|
||||
function resize() {
|
||||
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function draw(time) {
|
||||
resize();
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.uniform2f(uRes, canvas.width, canvas.height);
|
||||
gl.uniform1f(uTime, opts.frozenTime == null ? time * 0.001 : opts.frozenTime);
|
||||
gl.uniform3f(uInk, ink[0], ink[1], ink[2]);
|
||||
gl.uniform3f(uPaper, paper[0], paper[1], paper[2]);
|
||||
gl.uniform3f(uAccent, accent[0], accent[1], accent[2]);
|
||||
gl.uniform1f(uStrength, opts.strength == null ? 0.34 : opts.strength);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
draw(0);
|
||||
if (opts.frozenTime == null) requestAnimationFrame(draw);
|
||||
return { mode: "webgl", redraw: () => draw(0) };
|
||||
}
|
||||
|
||||
window.MagazineBg = { mount };
|
||||
})();
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 377 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
Reference in new issue
Block a user