'use strict'; /** * 针对「界面实际使用的那条 API 链路」做一次验收: * /api/send(含中文主题正文 + 回复头)→ 轮询确认到达 → /api/flags → /api/move → /api/delete * 结束后把测试邮件从收件箱与「已发送」都清掉,不给你留垃圾。 */ const BASE = process.env.WPYWMAIL_BASE || 'http://127.0.0.1:8788'; const token = 'WPYW-API-' + Date.now().toString().slice(-8); const report = []; const results = []; function say(s = '') { report.push(s); } function step(name, ok, detail = '') { results.push({ name, ok, detail }); report.push(`${ok ? '[PASS]' : '[FAIL]'} ${name}${detail ? ' —— ' + detail : ''}`); process.stdout.write(`${ok ? 'PASS' : 'FAIL'} ${name}\n`); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function api(path, { method = 'GET', body } = {}) { const res = await fetch(BASE + path, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, }); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = { error: text }; } if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`); return data; } (async () => { say('═'.repeat(74)); say('WpywMail 客户端 · 界面 API 链路验收'); say(`本地服务:${BASE}`); say(`标记 :${token}`); say(`时间 :${new Date().toLocaleString('zh-CN')}`); say('═'.repeat(74)); say(''); const subject = `界面链路验收 ${token} · 中文主题`; const text = [ '这封是通过客户端界面的发信链路(/api/send)发出的。', `标记:${token}`, '标点:你好,世界。()《》——、;:!?', ].join('\r\n'); // 1) 发送 say('── 1) /api/send 发信(中文主题 + 中文正文)──────────────────'); const sendOut = await api('/api/send', { method: 'POST', body: { to: 'wpy@wpy.email', subject, text }, }); step('/api/send 返回成功', sendOut.ok === true, `收件人 ${sendOut.recipients.join(',')},${sendOut.bytes} 字节`); // 2) 等到达 say(''); say('── 2) 轮询收件箱确认到达 ────────────────────────────────────'); let hit = null; for (let i = 0; i < 12 && !hit; i++) { await sleep(2500); const list = await api('/api/messages?folder=INBOX&limit=20'); hit = (list.messages || []).find((m) => (m.subject || '').includes(token)) || null; } step('发出的信出现在收件箱', !!hit, hit ? `UID ${hit.uid}` : '未找到'); if (hit) { const detail = await api(`/api/messages/${hit.uid}?folder=INBOX`); step('主题往返无损(无乱码、无编码字残留)', detail.subject === subject && !detail.subject.includes('=?'), detail.subject); step('正文往返无损(含全角标点)', /你好,世界。()《》——、;:!?/.test(detail.text), `${detail.text.length} 字符`); // 3) 旗标 say(''); say('── 3) 旗标 / 已读 ──────────────────────────────────────────'); await api('/api/flags', { method: 'POST', body: { uid: hit.uid, folder: 'INBOX', flagged: true } }); const after1 = await api('/api/messages?folder=INBOX&limit=20'); const row1 = after1.messages.find((m) => m.uid === hit.uid); step('加旗标生效', !!(row1 && row1.flagged), `flagged=${row1 && row1.flagged}`); await api('/api/flags', { method: 'POST', body: { uid: hit.uid, folder: 'INBOX', seen: false } }); const after2 = await api('/api/messages?folder=INBOX&limit=20'); const row2 = after2.messages.find((m) => m.uid === hit.uid); step('标为未读生效', !!(row2 && !row2.seen), `seen=${row2 && row2.seen}`); // 4) 移动 → 归档 → 移回 say(''); say('── 4) 移动(COPY + 删除,服务器没有 MOVE 能力)─────────────'); await api('/api/move', { method: 'POST', body: { uid: hit.uid, folder: 'INBOX', target: 'Archive' } }); const arch = await api('/api/messages?folder=Archive&limit=20'); const inArch = (arch.messages || []).some((m) => (m.subject || '').includes(token)); step('移动到 Archive 成功', inArch, `Archive 现有 ${arch.total} 封`); const back = arch.messages.find((m) => (m.subject || '').includes(token)); if (back) { await api('/api/move', { method: 'POST', body: { uid: back.uid, folder: 'Archive', target: 'INBOX' } }); const again = await api('/api/messages?folder=INBOX&limit=20'); step('移回 INBOX 成功', (again.messages || []).some((m) => (m.subject || '').includes(token))); } // 5) 删除(进废纸篓) say(''); say('── 5) 删除(默认移入 Trash)────────────────────────────────'); const moveBack = await api('/api/messages?folder=INBOX&limit=20'); const cur = (moveBack.messages || []).find((m) => (m.subject || '').includes(token)) || hit; const del = await api('/api/delete', { method: 'POST', body: { uid: cur.uid, folder: 'INBOX' } }); const after = await api('/api/messages?folder=INBOX&limit=20'); const gone = !(after.messages || []).some((m) => (m.subject || '').includes(token)); step('从收件箱移除', gone, del.moved ? `已移入 ${del.folder}` : '已永久删除'); } // 6) 清掉「已发送」里的副本 say(''); say('── 6) 清理测试痕迹 ─────────────────────────────────────────'); const sentBox = await api('/api/messages?folder=Sent&limit=30'); const sentHits = (sentBox.messages || []).filter((m) => (m.subject || '').includes('WPYW-CLI-') || (m.subject || '').includes('WPYW-API-')); let cleaned = 0; for (const m of sentHits) { try { await api('/api/delete', { method: 'POST', body: { uid: m.uid, folder: 'Sent', permanent: true } }); cleaned++; } catch { /* 忽略 */ } } step('清理「已发送」中的测试副本', true, `清理 ${cleaned} 封(共发现 ${sentHits.length} 封)`); const pass = results.filter((r) => r.ok).length; const fail = results.length - pass; say(''); say('═'.repeat(74)); say(`汇总:通过 ${pass} / ${results.length},失败 ${fail}`); if (fail) for (const r of results.filter((x) => !x.ok)) say(` - ${r.name} ${r.detail}`); say('═'.repeat(74)); const fs = require('node:fs'); fs.writeFileSync('E:\\deepseek\\artifacts\\client-api-check.txt', report.join('\n'), 'utf8'); process.stdout.write(`\nREPORT E:\\deepseek\\artifacts\\client-api-check.txt\nSUMMARY pass=${pass} fail=${fail}\n`); process.exit(fail ? 1 : 0); })().catch((err) => { process.stderr.write('FATAL ' + (err && err.stack ? err.stack : err) + '\n'); process.exit(2); });