Initial commit: WpywMail 桌面客户端:Node 零依赖本地服务 + React 19 / shadcn-ui 界面,支持收发信、注册、找回密码、会话与账号管理

This commit is contained in:
WpyQwq
2026-09-19 11:20:43 +08:00
commit c7fb8f8f68
47 changed files with 11573 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
'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: '[email protected]', 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);
});