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
+77
View File
@@ -0,0 +1,77 @@
'use strict';
/**
* 邮件服务器「账号接口」客户端(对接服务端 v2.2.0 的账号体系)。
*
* 走的是服务器上**只放账号类接口**的公网 HTTPS 入口(默认 https://mail.example.com:9443):
* 注册、邮箱验证码、找回密码、登录换 token、改密码、会话与资料。
* 邮件读写(读信/发件/队列)**不经过这里**,仍然走 IMAP 993 / SMTP 587 ——
* 那个入口对邮件与管理接口一律返回 404(可在服务器上用 /api/messages 验证)。
*
* 零依赖:用 Node 内置的全局 fetch(Node 18+)。
*/
const DEFAULT_API_BASE = 'https://mail.example.com:9443';
/** 服务器给的错误信息本身就是中文说明,直接透传给 UI;只补上网络层失败的可读提示。 */
async function call(base, path, options = {}) {
const { method = 'GET', body, token, timeoutMs = 20000 } = options;
const root = String(base || DEFAULT_API_BASE).replace(/\/+$/, '');
const url = root + path;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const headers = { 'Content-Type': 'application/json; charset=utf-8' };
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(url, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal,
});
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = { error: text };
}
if (!res.ok) {
const err = new Error((data && data.error) || `账号接口返回 HTTP ${res.status}`);
err.status = res.status;
err.data = data;
throw err;
}
return data;
} catch (err) {
if (err && err.status) throw err;
if (err && err.name === 'AbortError') throw new Error(`账号接口超时:${url}`);
throw new Error(`连不上账号接口 ${url}:${err && err.message}`);
} finally {
clearTimeout(timer);
}
}
function createAccountApi(base) {
const apiBase = String(base || DEFAULT_API_BASE).replace(/\/+$/, '');
return {
apiBase,
policy: () => call(apiBase, '/api/auth/policy'),
register: (body) => call(apiBase, '/api/register', { method: 'POST', body }),
verifyRegistration: (body) => call(apiBase, '/api/register/verify', { method: 'POST', body }),
resendCode: (body) => call(apiBase, '/api/register/resend', { method: 'POST', body }),
forgot: (email) => call(apiBase, '/api/auth/forgot', { method: 'POST', body: { email } }),
reset: (body) => call(apiBase, '/api/auth/reset', { method: 'POST', body }),
login: (email, password) => call(apiBase, '/api/login', { method: 'POST', body: { email, password } }),
logout: (token) => call(apiBase, '/api/logout', { method: 'POST', body: {}, token }),
changePassword: (token, body) => call(apiBase, '/api/account/password', { method: 'POST', body, token }),
profile: (token) => call(apiBase, '/api/me', { token }),
updateProfile: (token, displayName) =>
call(apiBase, '/api/account/profile', { method: 'PATCH', body: { displayName }, token }),
sessions: (token) => call(apiBase, '/api/account/sessions', { token }),
revokeSessions: (token, body) => call(apiBase, '/api/account/sessions/revoke', { method: 'POST', body, token }),
audit: (token, limit = 30) => call(apiBase, `/api/account/audit?limit=${limit}`, { token }),
};
}
module.exports = { DEFAULT_API_BASE, createAccountApi };