#!/usr/bin/env node 'use strict'; /** * 配置 LibreDesk 通知邮件(SMTP)。默认使用旧项目的 163 授权码。 * 修改后 LibreDesk 需要重启才能生效(脚本最后会重启容器)。 * * 用法:node scripts/configure-email.js */ const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); const OLD_MAIL_CFG = '/home/dev/code/crm/crm-ai-demo/bridge/mail.config.json'; 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) }; } async function main() { const env = loadEnv(path.join(ROOT, '.env')); const base = (env.LIBREDESK_BASE_URL || 'http://192.168.31.61:9000').replace(/\/+$/, ''); // 163 凭据:优先 .env 覆盖,否则读旧项目 mail.config.json let user = env.SMTP_USER; let pass = env.SMTP_PASS; let host = env.SMTP_HOST || 'smtp.163.com'; let port = Number(env.SMTP_PORT || 465); let from = env.SMTP_FROM; if ((!user || !pass) && fs.existsSync(OLD_MAIL_CFG)) { const old = JSON.parse(fs.readFileSync(OLD_MAIL_CFG, 'utf8')); user = user || old.smtp?.auth?.user; pass = pass || old.smtp?.auth?.pass; host = host || old.smtp?.host; port = port || old.smtp?.port; from = from || old.smtp?.from; } if (!user || !pass) throw new Error('缺少 SMTP 凭据:设置 SMTP_USER/SMTP_PASS 或确认旧 mail.config.json 存在'); 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('登录成功'); const payload = { 'notification.email.username': user, 'notification.email.host': host, 'notification.email.port': port, 'notification.email.password': pass, 'notification.email.email_address': from, 'notification.email.enabled': true, 'notification.email.tls_type': 'tls', 'notification.email.auth_protocol': 'plain', 'notification.email.max_conns': 5, 'notification.email.idle_timeout': '30s', 'notification.email.wait_timeout': '10s', 'notification.email.max_msg_retries': 3, 'notification.email.tls_skip_verify': false, 'notification.email.hello_hostname': '', }; await c.put('/api/v1/settings/notifications/email', payload); console.log(`已保存 SMTP:${host}:${port} user=${user} from=${from}`); console.log('重启 LibreDesk 使配置生效…'); execFileSync('docker', ['compose', 'restart', 'libredesk'], { cwd: ROOT, stdio: 'inherit' }); console.log('完成。'); } main().catch((err) => { console.error('[configure-email]', err.message); process.exit(1); });