98 lines
4.0 KiB
TypeScript
98 lines
4.0 KiB
TypeScript
/**
|
|
* 临时邮箱取验证码 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<any> {
|
|
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<any> {
|
|
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<string> {
|
|
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<TempMailbox> {
|
|
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<string> {
|
|
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}`);
|
|
}
|