/** * 临时邮箱取验证码 helper —— 用 mail.tm(免费、免预注册 API key,按需建邮箱拿 JWT)。 * 替代已失效的旧 accountCode.py(mail.td 匿名接口已下线)。 * * 典型用法(注册/忘记密码/注销): * const box = await createTempMailbox(); // { address, token } * // …在 App 里用 box.address 注册、触发发码… * const code = await getVerificationCode(box.token); // 轮询收件箱,正则取 6 位码 */ const API = 'https://api.mail.tm'; export interface TempMailbox { address: string; password: string; token: string; } async function jget(url: string, token?: string): Promise { const res = await fetch(url, { headers: { accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, }); if (!res.ok) throw new Error(`GET ${url} → HTTP ${res.status}`); return res.json(); } async function jpost(url: string, body: any): Promise { const res = await fetch(url, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`POST ${url} → HTTP ${res.status}`); return res.json(); } /** mail.tm 集合响应:accept: application/json 返回普通数组;application/ld+json 返回 {hydra:member}。两者都兼容。 */ function asList(data: any): any[] { if (Array.isArray(data)) return data; return data?.['hydra:member'] || []; } /** 用已有邮箱地址重新登录 mail.tm 取 token(邮箱持久,可复用已注册账号收码)。 */ export async function getMailboxToken(address: string, password = 'Test123456!'): Promise { const { token } = await jpost(`${API}/token`, { address, password }); if (!token) throw new Error(`mail.tm token 获取失败: ${address}`); return token; } /** 生成短随机用户名(≤10,字母数字),拼当前可用域名,建临时邮箱并返回登录 token。 */ export async function createTempMailbox(): Promise { const domains = asList(await jget(`${API}/domains`)); const domain = domains.find((d: any) => d.isActive)?.domain || domains[0]?.domain; if (!domain) throw new Error('mail.tm 无可用域名'); // 随机用户名:字母开头 + 随机串(避免 Date.now,用时间不可用时退随机) const rand = Math.random().toString(36).slice(2, 10); const address = `woan${rand}@${domain}`; const password = 'Test123456!'; await jpost(`${API}/accounts`, { address, password }); const { token } = await jpost(`${API}/token`, { address, password }); if (!token) throw new Error('mail.tm 未返回 token'); console.log(`临时邮箱: ${address}`); return { address, password, token }; } /** * 轮询收件箱取验证码。默认匹配 "code is XXXXXX" 或邮件正文里的 6 位数字。 * @param token createTempMailbox 返回的 JWT * @param opts.timeoutMs 总超时(默认 60s);opts.regex 自定义验证码正则(第 1 个捕获组为码) */ export async function getVerificationCode( token: string, opts: { timeoutMs?: number; regex?: RegExp } = {} ): Promise { const timeoutMs = opts.timeoutMs ?? 60000; const deadline = Date.now() + timeoutMs; let lastErr = ''; while (Date.now() < deadline) { try { const list = await jget(`${API}/messages`, token); const msgs = asList(list); if (msgs.length) { const detail = await jget(`${API}/messages/${msgs[0].id}`, token); const text: string = detail.text || (Array.isArray(detail.html) ? detail.html.join(' ') : detail.html || '') || detail.intro || ''; const m = (opts.regex && text.match(opts.regex)) || text.match(/code is[:\s]*([0-9A-Za-z]{4,8})/i) || text.match(/\b(\d{6})\b/); if (m && m[1]) { console.log(`验证码: ${m[1]}`); return m[1]; } lastErr = `收到邮件但未匹配到验证码;正文片段: ${text.slice(0, 120)}`; } } catch (e: any) { lastErr = e.message; } await new Promise((r) => setTimeout(r, 3000)); } throw new Error(`取验证码超时(${timeoutMs}ms)。${lastErr}`); }