#!/usr/bin/env node 'use strict'; /** * 配置 Email 渠道(163): * 1. 创建/更新 email inbox(IMAP 走 imap-id-proxy,绕开 163 的 "Unsafe Login") * 2. 创建/更新默认外发邮件模板(email_outgoing) * 3. 创建/更新 automation:email inbox 新会话 -> 分配 AI Assistant * * 依赖:docker compose 里的 imap-proxy 服务(services/imap-id-proxy)。 * 用法:node scripts/add-email-inbox.js */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const OLD_MAIL_CFG = '/home/dev/code/crm/crm-ai-demo/bridge/mail.config.json'; const INBOX_NAME = 'Email 163'; const RULE_NAME = 'Assign email conversations to AI Assistant'; const ASSISTANT_NAME = 'Yehwang Assistant'; const TEMPLATE_NAME = 'Default outgoing email'; const TEMPLATE_BODY = `

Hello,

{{ template "content" . }}

Best regards,
Yehwang Support

`; function loadEnv(file) { const out = {}; for (const line of fs.readFileSync(file, 'utf8').split('\n')) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const eq = t.indexOf('='); if (eq < 0) continue; let v = t.slice(eq + 1).trim(); if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1); out[t.slice(0, eq).trim()] = v; } return out; } function createClient(baseURL) { const cookies = new Map(); let csrf = ''; const cookieHeader = () => [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; '); function store(res) { const list = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [res.headers.get('set-cookie')].filter(Boolean); for (const c of list) { const [pair] = c.split(';'); const eq = pair.indexOf('='); if (eq > 0) { const k = pair.slice(0, eq).trim(); const v = pair.slice(eq + 1).trim(); cookies.set(k, v); if (k === 'csrf_token') csrf = v; } } } async function req(method, p, body) { const headers = { Accept: 'application/json' }; if (cookies.size) headers.Cookie = cookieHeader(); if (body !== undefined) headers['Content-Type'] = 'application/json'; if (csrf && method !== 'GET') headers['X-CSRFTOKEN'] = csrf; const res = await fetch(baseURL + p, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); store(res); const text = await res.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { /* ignore */ } if (!res.ok) throw new Error(`${method} ${p} -> HTTP ${res.status}: ${(json && json.message) || text.slice(0, 300)}`); return json; } return { post: (p, b) => req('POST', p, b), get: (p) => req('GET', p), put: (p, b) => req('PUT', p, b) }; } const data = (j) => (j && j.data !== undefined ? j.data : j); async function main() { const env = loadEnv(path.join(ROOT, '.env')); const old = JSON.parse(fs.readFileSync(OLD_MAIL_CFG, 'utf8')); const base = (env.LIBREDESK_BASE_URL || 'http://localhost:9000').replace(/\/+$/, ''); const c = createClient(base); await c.post('/api/v1/auth/login', { email: env.LIBREDESK_SYSTEM_EMAIL || 'System', password: env.LIBREDESK_SYSTEM_USER_PASSWORD }); console.log('登录成功'); // 1. Email inbox const inboxes = data(await c.get('/api/v1/inboxes')) || []; let inbox = inboxes.find((i) => i.channel === 'email' && i.name === INBOX_NAME); const payload = { name: INBOX_NAME, channel: 'email', enabled: true, csat_enabled: false, prompt_tags_on_reply: false, reopen_window_hours: 0, from: old.smtp.auth.user, aliases: [], config: { auth_type: 'password', imap: [{ host: 'imap-proxy', port: 1993, username: old.imap.auth.user, password: old.imap.auth.pass, mailbox: 'INBOX', read_interval: '1m', scan_inbox_since: '48h', tls_type: 'none', tls_skip_verify: false, }], smtp: [{ host: old.smtp.host, port: old.smtp.port, username: old.smtp.auth.user, password: old.smtp.auth.pass, tls_type: 'tls', auth_protocol: 'plain', max_conns: 5, idle_timeout: '30s', pool_wait_timeout: '10s', max_msg_retries: 3, hello_hostname: '', }], reply_to: '', enable_plus_addressing: false, }, }; if (!inbox) { inbox = data(await c.post('/api/v1/inboxes', payload)); console.log(`创建 Email Inbox #${inbox.id}`); } else { inbox = data(await c.put(`/api/v1/inboxes/${inbox.id}`, payload)); console.log(`更新 Email Inbox #${inbox.id}`); } // 2. 默认外发邮件模板 const tpls = data(await c.get('/api/v1/templates?type=email_outgoing')) || []; let tpl = tpls.find((t) => t.is_default); if (!tpl) { tpl = data(await c.post('/api/v1/templates', { name: TEMPLATE_NAME, body: TEMPLATE_BODY, subject: '', is_default: true, type: 'email_outgoing', })); console.log(`创建默认外发模板 #${tpl.id}`); } else if (tpl.body !== TEMPLATE_BODY) { tpl = data(await c.put(`/api/v1/templates/${tpl.id}`, { name: tpl.name, body: TEMPLATE_BODY, subject: tpl.subject || '', is_default: true, type: 'email_outgoing', })); console.log(`更新默认外发模板 #${tpl.id}`); } else { console.log(`复用默认外发模板 #${tpl.id}`); } // 3. Automation:email inbox -> assistant const assistants = data(await c.get('/api/v1/ai/assistants')) || []; const assistant = assistants.find((a) => a.name === ASSISTANT_NAME); if (!assistant) throw new Error(`未找到 Assistant "${ASSISTANT_NAME}",先运行 scripts/setup-defaults.js`); const rulesRaw = data(await c.get('/api/v1/automations/rules?type=new_conversation')) || []; const rules = Array.isArray(rulesRaw) ? rulesRaw : (rulesRaw.rules || []); const rulePayload = { name: RULE_NAME, description: 'Route new email conversations to the AI assistant', type: 'new_conversation', events: [], enabled: true, weight: 0, rules: [{ type: 'new_conversation', execution_mode: 'all', event: [], group_operator: 'AND', groups: [{ logical_op: 'AND', rules: [{ field: 'inbox', field_type: 'conversation', operator: 'equals', value: String(inbox.id), case_sensitive_match: false, }], }], actions: [{ type: 'assign_user', value: [String(assistant.user_id)], display_value: [assistant.name], }], }], }; const existing = rules.find((r) => r.name === RULE_NAME); if (!existing) { const rule = data(await c.post('/api/v1/automations/rules', rulePayload)); console.log(`创建 Automation #${rule.id}(inbox ${inbox.id} → user ${assistant.user_id})`); } else { const rule = data(await c.put(`/api/v1/automations/rules/${existing.id}`, rulePayload)); console.log(`更新 Automation #${rule.id}(inbox ${inbox.id} → user ${assistant.user_id})`); } console.log('\nEmail 渠道配置完成。IMAP 走 imap-proxy(自动注入 ID),SMTP 直连 smtp.163.com:465。'); } main().catch((err) => { console.error('[add-email-inbox]', err.message); process.exit(1); });