{ "name": "메일 처리 파이프라인", "nodes": [ { "parameters": { "mailbox": "INBOX", "postProcessAction": "read", "options": { "customEmailConfig": "{ \"host\": \"{{$env.IMAP_HOST || '192.168.1.227'}}\", \"port\": {{$env.IMAP_PORT || 993}}, \"secure\": true, \"auth\": { \"user\": \"{{$env.IMAP_USER}}\", \"pass\": \"{{$env.IMAP_PASSWORD}}\" } }" }, "pollTimes": { "item": [ { "mode": "everyX", "value": 15, "unit": "minutes" } ] } }, "id": "m1000001-0000-0000-0000-000000000001", "name": "IMAP Trigger", "type": "n8n-nodes-base.imapEmail", "typeVersion": 2, "position": [0, 300] }, { "parameters": { "jsCode": "const items = $input.all();\nconst results = [];\nfor (const item of items) {\n const j = item.json;\n const from = j.from?.text || j.from || '';\n const subject = (j.subject || '').substring(0, 500);\n const body = (j.text || j.textPlain || j.html || '').substring(0, 5000)\n .replace(/<[^>]*>/g, '').replace(/\\s+/g, ' ').trim();\n const mailDate = j.date || new Date().toISOString();\n results.push({ json: { from, subject, body, mailDate, messageId: j.messageId || '' } });\n}\nreturn results;" }, "id": "m1000001-0000-0000-0000-000000000002", "name": "Parse Mail", "type": "n8n-nodes-base.code", "typeVersion": 1, "position": [220, 300] }, { "parameters": { "jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' \\u2192 ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' \\u2192 timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nconst item = $input.first().json;\nconst prompt = `메일을 분류하고 요약하세요. JSON만 출력.\n\n{\n \"summary\": \"한국어 2~3문장 요약\",\n \"label\": \"업무|개인|광고|알림\",\n \"has_events\": true/false,\n \"has_tasks\": true/false\n}\n\n보낸 사람: ${item.from}\n제목: ${item.subject}\n본문: ${item.body.substring(0, 3000)}`;\n\ntry {\n const r = await httpPost(`${$env.GPU_OLLAMA_URL}/api/generate`,\n { model: 'qwen3.5:9b-q8_0', prompt, stream: false, format: 'json', think: false },\n { timeout: 15000 }\n );\n const cls = JSON.parse(r.response);\n return [{ json: { ...item, summary: cls.summary || item.subject, label: cls.label || '알림', has_events: cls.has_events || false, has_tasks: cls.has_tasks || false } }];\n} catch(e) {\n return [{ json: { ...item, summary: item.subject, label: '알림', has_events: false, has_tasks: false } }];\n}" }, "id": "m1000001-0000-0000-0000-000000000003", "name": "Summarize & Classify", "type": "n8n-nodes-base.code", "typeVersion": 1, "position": [440, 300] }, { "parameters": { "operation": "executeQuery", "query": "=INSERT INTO mail_logs (from_address,subject,summary,label,has_events,has_tasks,mail_date) VALUES ('{{ ($json.from||'').replace(/'/g,\"''\").substring(0,255) }}','{{ ($json.subject||'').replace(/'/g,\"''\").substring(0,500) }}','{{ ($json.summary||'').replace(/'/g,\"''\").substring(0,2000) }}','{{ $json.label }}',{{ $json.has_events }},{{ $json.has_tasks }},'{{ $json.mailDate }}')", "options": {} }, "id": "m1000001-0000-0000-0000-000000000004", "name": "Save to mail_logs", "type": "n8n-nodes-base.postgres", "typeVersion": 2.5, "position": [660, 300], "credentials": { "postgres": { "id": "KaxU8iKtraFfsrTF", "name": "bot-postgres" } } }, { "parameters": { "jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' \\u2192 ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' \\u2192 timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nfunction httpPut(url, body, { timeout = 10000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'PUT',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' \\u2192 ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' \\u2192 timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nconst item = $input.first().json;\nconst embText = `${item.subject} ${item.summary}`;\n\ntry {\n const emb = await httpPost(`${$env.LOCAL_OLLAMA_URL}/api/embeddings`, { model: 'bge-m3', prompt: embText });\n if (emb.embedding) {\n const qdrantUrl = $env.QDRANT_URL || 'http://host.docker.internal:6333';\n await httpPut(`${qdrantUrl}/collections/documents/points`, { points: [{ id: Date.now(), vector: emb.embedding, payload: {\n text: embText, source: 'mail', from_address: item.from,\n mail_date: item.mailDate, label: item.label,\n created_at: new Date().toISOString()\n }}]});\n }\n} catch(e) {}\n\n// DEVONthink 저장 (graceful)\ntry {\n const dtUrl = $env.DEVONTHINK_BRIDGE_URL || 'http://host.docker.internal:8093';\n await httpPost(`${dtUrl}/save`, {\n title: item.subject, content: item.summary,\n type: 'markdown', tags: ['mail', item.label]\n }, { timeout: 5000 });\n} catch(e) {}\n\nreturn [{ json: { ...item, embedded: true } }];" }, "id": "m1000001-0000-0000-0000-000000000005", "name": "Embed & Save", "type": "n8n-nodes-base.code", "typeVersion": 1, "position": [880, 300] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "important-check", "leftValue": "={{ $json.label }}", "rightValue": "업무", "operator": { "type": "string", "operation": "equals" } }, { "id": "tasks-check", "leftValue": "={{ $json.has_tasks }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true" } } ], "combinator": "and" }, "options": {} }, "id": "m1000001-0000-0000-0000-000000000006", "name": "Is Important?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [1100, 300] }, { "parameters": { "method": "POST", "url": "={{ $env.SYNOLOGY_CHAT_WEBHOOK_URL }}", "sendBody": true, "specifyBody": "json", "jsonBody": "={ \"text\": {{ JSON.stringify('[메일 알림] ' + $json.from + ': ' + $json.subject + '\\n' + $json.summary) }} }", "options": { "timeout": 10000 } }, "id": "m1000001-0000-0000-0000-000000000007", "name": "Notify Chat", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1320, 200] } ], "connections": { "IMAP Trigger": { "main": [ [ { "node": "Parse Mail", "type": "main", "index": 0 } ] ] }, "Parse Mail": { "main": [ [ { "node": "Summarize & Classify", "type": "main", "index": 0 } ] ] }, "Summarize & Classify": { "main": [ [ { "node": "Save to mail_logs", "type": "main", "index": 0 } ] ] }, "Save to mail_logs": { "main": [ [ { "node": "Embed & Save", "type": "main", "index": 0 } ] ] }, "Embed & Save": { "main": [ [ { "node": "Is Important?", "type": "main", "index": 0 } ] ] }, "Is Important?": { "main": [ [ { "node": "Notify Chat", "type": "main", "index": 0 } ], [] ] } }, "settings": { "executionOrder": "v1" } }