Initial commit: GPT 账号注册机 (open source)

只开源「GPT 账号注册机 / 管理」(account-manager):账号列表/导入导出、检测/批量检测、
拿 RT、协议/浏览器注册等注册机能力。批量直开 / 协议支付 / 提取链接 / 直卡直开 等其它功能
不含实现代码,仅通过 iframe 内嵌外链访问。真实机密均由 .gitignore 忽略,仅提供脱敏示例配置。
This commit is contained in:
432539
2026-08-05 14:54:32 +08:00
commit fef45134d5
29 changed files with 9829 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
// PLUS 邮箱收信 —— 微软 Outlook/Hotmail OAuthrefresh_token + client_id
// 导入格式:email----password----clientId----refreshToken
// 收信流程:refresh_token 换 access_token → 优先 Microsoft Graph 读收件箱,失败回退 IMAP(XOAUTH2)
import tls from 'node:tls';
const TOKEN_URL = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';
// 这些批量 Outlook 账号的 refresh_token 是按「资源级同意」(.default) 颁发的,
// 直接请求细粒度 scope(如 https://graph.microsoft.com/Mail.Read)会被拒:
// AADSTS70000 invalid_grant: one or more scopes requested are unauthorized or expired
// 因此每种传输都按候选 scope 依次尝试,取第一个成功者(.default 优先,细粒度兜底)。
const GRAPH_SCOPES = [
'https://graph.microsoft.com/.default',
'https://graph.microsoft.com/Mail.Read offline_access',
];
const IMAP_SCOPES = [
'https://outlook.office.com/IMAP.AccessAsUser.All offline_access',
'https://outlook.office.com/.default',
];
const IMAP_HOST = 'outlook.office365.com';
const IMAP_PORT = 993;
const tokenCache = new Map(); // key: clientId|refreshToken|scope -> { token, ts }
const TOKEN_TTL = 45 * 60 * 1000;
// ---------- 解析导入格式 ----------
// 每行:email----password----clientId----refreshToken(也兼容用 | 或制表符分隔的多字段)
export function parsePlus(text) {
const out = [];
const seen = new Set();
for (const raw of String(text || '').split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
let parts;
if (line.includes('----')) parts = line.split('----');
else if (line.includes('\t')) parts = line.split('\t');
else if (line.includes('|')) parts = line.split('|');
else parts = line.split(/\s+/);
parts = parts.map((s) => s.trim()).filter(Boolean);
if (parts.length < 4) continue;
const [email, password, clientId, refreshToken] = parts;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) continue;
const key = email.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
out.push({ email, password, clientId, refreshToken });
}
return out;
}
// ---------- 验证码提取(与 buyer 收信口径一致:4-8 位数字优先)----------
export function extractCode(text) {
if (!text) return '';
const s = String(text);
const labeled = s.match(/(?:code|验证码|verification|otp|passcode)[^\d]{0,12}(\d{4,8})/i);
if (labeled) return labeled[1];
const m = s.match(/\b(\d{4,8})\b/);
return m ? m[1] : '';
}
// ---------- 换取 access_token(单个 scope----------
async function exchangeToken(clientId, refreshToken, scope) {
const ck = `${clientId}|${refreshToken}|${scope}`;
const cached = tokenCache.get(ck);
if (cached && Date.now() - cached.ts < TOKEN_TTL) return cached.token;
const params = new URLSearchParams();
params.set('client_id', clientId);
params.set('grant_type', 'refresh_token');
params.set('refresh_token', refreshToken);
params.set('scope', scope);
const res = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.access_token) {
const e = new Error(`换取令牌失败:${data.error || res.status} ${data.error_description ? '· ' + String(data.error_description).slice(0, 120) : ''}`);
e.status = res.status;
e.oauthError = data.error || '';
throw e;
}
tokenCache.set(ck, { token: data.access_token, ts: Date.now() });
return data.access_token;
}
// ---------- 换取 access_token(按候选 scope 回退)----------
// scopes 为候选 scope 数组,依次尝试,返回第一个成功者的 token。
// 遇到 invalid_grant/scope 类错误继续尝试下一个 scope;全部失败则抛出最后错误。
// 成功时打印命中的 scope,便于观察是哪种同意口径生效。
async function getAccessToken(clientId, refreshToken, scopes) {
const candidates = Array.isArray(scopes) ? scopes : [scopes];
let lastErr = null;
for (const scope of candidates) {
try {
const token = await exchangeToken(clientId, refreshToken, scope);
if (candidates.length > 1) {
console.log(`[plus-mailbox] 令牌换取成功 scope=${scope}`);
}
return token;
} catch (e) {
lastErr = e;
// scope 未授权/过期 → 尝试下一个候选;其它错误(RT 失效等)同样继续,
// 但若全部候选都失败则把最后的错误抛出(通常即真正原因)。
continue;
}
}
throw lastErr || new Error('换取令牌失败:无可用 scope');
}
// ---------- MIME 解码:编码字(RFC2047/ quoted-printable / base64 ----------
function decodeQP(str, isWord = false) {
let s = String(str || '');
if (isWord) s = s.replace(/_/g, ' ');
return s
.replace(/=\r?\n/g, '')
.replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
}
function utf8(bytesStr) {
// bytesStr 为 latin1 字节串,按 UTF-8 还原
try { return Buffer.from(bytesStr, 'latin1').toString('utf8'); } catch { return bytesStr; }
}
function decodeEncodedWords(str) {
return String(str || '').replace(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g, (_, charset, enc, data) => {
try {
let bytes;
if (enc.toUpperCase() === 'B') bytes = Buffer.from(data, 'base64').toString('latin1');
else bytes = decodeQP(data, true);
return /utf-?8/i.test(charset) ? utf8(bytes) : bytes;
} catch { return data; }
}).replace(/\?=\s*=\?/g, ''); // 相邻编码字之间的折行分隔
}
// ---------- HTML → 纯文本 ----------
function htmlToText(html) {
return String(html || '')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/&#(\d+);/g, (_, d) => String.fromCharCode(Number(d)))
.replace(/\s+/g, ' ')
.trim();
}
// ---------- Graph 读收件箱 ----------
async function fetchViaGraph(clientId, refreshToken, limit) {
const token = await getAccessToken(clientId, refreshToken, GRAPH_SCOPES);
const url = `https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?$top=${limit}` +
`&$select=subject,from,receivedDateTime,bodyPreview,body&$orderby=receivedDateTime desc`;
const res = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const e = new Error(`Graph 读信失败:${(data.error && data.error.message) || res.status}`);
e.status = res.status;
throw e;
}
return (data.value || []).map((m) => {
const bodyText = m.body && m.body.contentType === 'html' ? htmlToText(m.body.content) : (m.body && m.body.content) || m.bodyPreview || '';
return {
from_addr: (m.from && m.from.emailAddress && m.from.emailAddress.address) || '',
subject: m.subject || '',
received_at: m.receivedDateTime || '',
body: bodyText.slice(0, 4000),
snippet: m.bodyPreview || '',
extracted_code: extractCode(`${m.subject || ''} ${m.bodyPreview || ''} ${bodyText}`),
};
});
}
// ---------- IMAP(XOAUTH2) 读收件箱(Graph 失败时回退)----------
function imapRead(email, token, limit) {
return new Promise((resolve, reject) => {
const sock = tls.connect(IMAP_PORT, IMAP_HOST, { servername: IMAP_HOST }, () => {});
let buf = '';
let stage = 'greeting';
let tag = 0;
let exists = 0;
let raw = '';
let settled = false;
const timer = setTimeout(() => finish(new Error('IMAP 超时')), 20000);
function finish(err, val) {
if (settled) return;
settled = true;
clearTimeout(timer);
try { sock.end(); sock.destroy(); } catch {}
err ? reject(err) : resolve(val);
}
const send = (cmd) => { tag++; sock.write(`a${tag} ${cmd}\r\n`); return `a${tag}`; };
sock.setEncoding('utf8');
sock.on('error', (e) => finish(new Error('IMAP ' + (e.code || e.message))));
sock.on('data', (chunk) => {
buf += chunk;
if (stage === 'greeting') {
if (!/\r\n/.test(buf)) return;
buf = '';
stage = 'auth';
const b64 = Buffer.from(`user=${email}\x01auth=Bearer ${token}\x01\x01`).toString('base64');
sock.write(`a1 AUTHENTICATE XOAUTH2 ${b64}\r\n`);
tag = 1;
return;
}
if (stage === 'auth') {
if (/^\+/m.test(buf)) { sock.write('\r\n'); } // 服务器要求继续 → 发空行以取回错误
if (/^a1 OK/mi.test(buf)) { buf = ''; stage = 'select'; send('SELECT INBOX'); return; }
if (/^a1 (NO|BAD)/mi.test(buf)) return finish(new Error('IMAP 认证失败(XOAUTH2'));
return;
}
if (stage === 'select') {
const m = buf.match(/\*\s+(\d+)\s+EXISTS/i);
if (m) exists = Number(m[1]);
if (/^a2 OK/mi.test(buf)) {
buf = '';
if (!exists) return finish(null, []);
const from = Math.max(1, exists - limit + 1);
stage = 'fetch';
send(`FETCH ${from}:${exists} (BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)] BODY.PEEK[TEXT])`);
return;
}
if (/^a2 (NO|BAD)/mi.test(buf)) return finish(new Error('IMAP SELECT 失败'));
return;
}
if (stage === 'fetch') {
raw += chunk;
if (/^a3 OK/mi.test(raw)) return finish(null, parseImapFetch(raw));
if (/^a3 (NO|BAD)/mi.test(raw)) return finish(new Error('IMAP FETCH 失败'));
}
});
// buf 只在非 fetch 阶段用;fetch 阶段用 raw
});
}
// 从 BODY[TEXT] 原文中挑出可读正文段:multipart 优先 text/plain,其次 text/html
function pickBodyPart(rawBody) {
let segment = rawBody;
const boundaryM = rawBody.match(/boundary="?([^"\r\n;]+)"?/i);
if (boundaryM) {
const bnd = boundaryM[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const parts = rawBody.split(new RegExp('--' + bnd));
let plain = null, html = null;
for (const p of parts) {
if (!plain && /Content-Type:\s*text\/plain/i.test(p)) plain = p;
else if (!html && /Content-Type:\s*text\/html/i.test(p)) html = p;
}
segment = plain || html || rawBody;
}
const cte = (segment.match(/Content-Transfer-Encoding:\s*([\w-]+)/i) || [])[1] || '';
const charset = (segment.match(/charset=?["']?\s*([\w-]+)/i) || [])[1] || '';
const headEnd = segment.search(/\r?\n\r?\n/);
const content = headEnd >= 0 ? segment.slice(headEnd).replace(/^\s+/, '') : segment;
return { content, cte, isUtf8: /utf-?8/i.test(charset) };
}
// 极简解析:按邮件块切分,抓取 From/Subject/Date 和正文文本,并做 MIME 解码
function parseImapFetch(raw) {
const out = [];
const blocks = raw.split(/\*\s+\d+\s+FETCH/i).slice(1);
for (const b of blocks) {
const from = decodeEncodedWords((b.match(/From:\s*(.*)/i) || [])[1] || '');
const subject = decodeEncodedWords((b.match(/Subject:\s*(.*)/i) || [])[1] || '').trim();
const date = ((b.match(/Date:\s*(.*)/i) || [])[1] || '').trim();
const textIdx = b.search(/BODY\[TEXT\]/i);
let body = '';
if (textIdx >= 0) {
const after = b.slice(textIdx);
const lit = after.match(/\{(\d+)\}\r?\n/);
if (lit) {
const start = after.indexOf(lit[0]) + lit[0].length;
body = after.slice(start, start + Number(lit[1]));
}
}
// 挑选正文段(multipart 优先 text/plain),按其自身编码解码
const part = pickBodyPart(body);
let content = part.content;
if (/base64/i.test(part.cte)) {
try { content = Buffer.from(content.replace(/\s+/g, ''), 'base64').toString(part.isUtf8 ? 'utf8' : 'latin1'); } catch {}
} else if (/quoted-printable/i.test(part.cte)) {
content = decodeQP(content);
if (part.isUtf8) content = utf8(content);
} else {
content = decodeQP(content); // 兜底:常见 =XX 也一并还原
if (part.isUtf8) content = utf8(content);
}
body = htmlToText(content).slice(0, 4000);
out.push({
from_addr: from.trim().replace(/^.*<|>.*$/g, '').trim() || from.trim(),
subject,
received_at: date,
body,
snippet: body.slice(0, 160),
extracted_code: extractCode(`${subject} ${body}`),
});
}
return out.reverse();
}
// ---------- 对外统一入口 ----------
export async function fetchPlusInbox({ email, clientId, refreshToken }, limit = 15) {
if (!clientId || !refreshToken) {
const e = new Error('缺少 clientId / refreshToken');
e.status = 400;
throw e;
}
try {
const messages = await fetchViaGraph(clientId, refreshToken, limit);
return { address: email, via: 'graph', messages };
} catch (graphErr) {
// Graph 不通(scope 未授权等)→ 回退 IMAP
try {
const token = await getAccessToken(clientId, refreshToken, IMAP_SCOPES);
const messages = await imapRead(email, token, limit);
return { address: email, via: 'imap', messages };
} catch (imapErr) {
const e = new Error(`收信失败(Graph: ${graphErr.message}IMAP: ${imapErr.message}`);
e.status = graphErr.status || 502;
throw e;
}
}
}