Initial commit: WpywMail 桌面客户端:Node 零依赖本地服务 + React 19 / shadcn-ui 界面,支持收发信、注册、找回密码、会话与账号管理
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user