feat: support 126 mail provider routing and shared NetEase verification flow
- 合并 PR #112 的核心改动:新增 126 邮箱 provider,并复用 163 / 163 VIP 的网页邮箱接码链路 - 本地补充修复:吸收 126 子域名注入与来源识别修正,以及新版 ChatGPT 登录验证码邮件匹配适配 - 影响范围:sidepanel mail provider、background navigation/source routing、content mail polling、相关回归测试
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
一个用于批量跑通 ChatGPT OAuth 注册/登录流程的 Chrome 扩展。
|
||||
|
||||
当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / Inbucket / Hotmail 协助获取验证码。
|
||||
当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / 163 VIP / 126 / Inbucket / Hotmail 协助获取验证码。
|
||||
|
||||
## 插件效果
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
- 自动获取注册验证码与登录验证码
|
||||
- 支持 `Hotmail`:继续使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token)`,并可在远程服务与本地助手两种模式间切换
|
||||
- 支持 `2925`:新增多账号池、自动登录登出、Step 4 / Step 8 命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号
|
||||
- 支持 `QQ Mail`、`163 Mail`、`Inbucket mailbox`
|
||||
- 支持 `QQ Mail`、`163 Mail`、`163 VIP Mail`、`126 Mail`、`Inbucket mailbox`
|
||||
- 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址
|
||||
- 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀
|
||||
- Step 5 同时兼容两种页面:
|
||||
@@ -76,7 +76,7 @@
|
||||
- DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发
|
||||
- Cloudflare 自定义域邮箱前缀 + QQ / 163 / Inbucket 转发
|
||||
- 手动填写一个可收信邮箱
|
||||
- 如果使用 `QQ` / `163` / `Inbucket`,对应页面需要提前能正常打开
|
||||
- 如果使用 `QQ` / `163` / `163 VIP` / `126` / `Inbucket`,对应页面需要提前能正常打开
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -135,17 +135,17 @@
|
||||
|
||||
如果你只是想先跑通一套最稳的组合,建议直接按下面三种方案之一配置。
|
||||
|
||||
### 方案 A:`CPA + QQ / 163 / 163 VIP`
|
||||
### 方案 A:`CPA + QQ / 163 / 163 VIP / 126`
|
||||
|
||||
1. `CPA` 填你的管理面板 OAuth 页面地址
|
||||
2. `Mail` 选择 `QQ Mail`、`163 Mail` 或 `163 VIP Mail`
|
||||
2. `Mail` 选择 `QQ Mail`、`163 Mail`、`163 VIP Mail` 或 `126 Mail`
|
||||
3. `邮箱生成` 选择 `DuckDuckGo`、`Cloudflare` 或 `自定义邮箱池`
|
||||
4. 若你选择 `Cloudflare`,先按下文把 Cloudflare Email Routing 配好
|
||||
5. 若你选择 `自定义邮箱池`,就在 `邮箱池` 中按行填入邮箱;否则点击 `获取` 生成邮箱,或手动粘贴一个你能收信的邮箱
|
||||
6. 先单步验证 `Step 1 ~ Step 4`
|
||||
7. 验证没问题后再点右上角 `Auto`
|
||||
|
||||
### 方案 B:`SUB2API + QQ / 163 / 163 VIP`
|
||||
### 方案 B:`SUB2API + QQ / 163 / 163 VIP / 126`
|
||||
|
||||
1. `来源` 选择 `SUB2API`
|
||||
2. 填好 `SUB2API` 地址、登录邮箱、登录密码、分组名
|
||||
@@ -153,7 +153,7 @@
|
||||
4. Step 1 会直接在 SUB2API 后台生成 OAuth 链接
|
||||
5. Step 10 会把 localhost 回调提交回 SUB2API,并直接创建 OpenAI 账号
|
||||
|
||||
### 方案 C:`Codex2API + QQ / 163 / 163 VIP`
|
||||
### 方案 C:`Codex2API + QQ / 163 / 163 VIP / 126`
|
||||
|
||||
1. `来源` 选择 `Codex2API`
|
||||
2. 填好 `Codex2API` 后台地址、管理密钥
|
||||
@@ -222,12 +222,13 @@ Step 1 和 Step 10 都依赖这个地址。
|
||||
|
||||
### `Mail`
|
||||
|
||||
支持五种验证码来源:
|
||||
支持七种验证码来源:
|
||||
|
||||
- `Hotmail`
|
||||
- `2925`
|
||||
- `163 Mail`
|
||||
- `163 VIP Mail`
|
||||
- `126 Mail`
|
||||
- `QQ Mail`
|
||||
- `Inbucket`
|
||||
|
||||
@@ -235,7 +236,7 @@ Step 1 和 Step 10 都依赖这个地址。
|
||||
|
||||
- `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,可切换为远程服务模式或本地助手模式
|
||||
- `2925` 通过侧边栏里的 2925 账号池选择账号,并在 Step 4 / Step 8 前自动校验网页邮箱登录态
|
||||
- `QQ`、`163`、`163 VIP` 用于直接轮询网页邮箱
|
||||
- `QQ`、`163`、`163 VIP`、`126` 用于直接轮询网页邮箱
|
||||
- `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https://<your-inbucket-host>/m/<mailbox>/`
|
||||
|
||||
### `Hotmail 账号池`
|
||||
@@ -382,7 +383,7 @@ Step 3 使用的注册邮箱。
|
||||
- 当 `Mail = Hotmail` 时,这个输入框由账号池自动同步当前账号邮箱
|
||||
- 当 `Mail = Hotmail` 时,Step 3 会直接使用 Hotmail 账号池里的邮箱;`Duck / Cloudflare` 不参与自动邮箱生成
|
||||
- 当 `Mail = 自定义邮箱` 且启用了 `自定义号池` 时,Auto 会先为当前轮分配一个邮箱;后续普通失败不会换号,只有成功或出现手机号验证才会切到下一个邮箱
|
||||
- 若你准备走 `Cloudflare`,更推荐把 `Mail` 设为 `QQ / 163 / 163 VIP`;`Inbucket` 仅在它能真实接收外部邮件并完成 Cloudflare 验证时再使用
|
||||
- 若你准备走 `Cloudflare`,更推荐把 `Mail` 设为 `QQ / 163 / 163 VIP / 126`;`Inbucket` 仅在它能真实接收外部邮件并完成 Cloudflare 验证时再使用
|
||||
- `Auto` 会按当前“邮箱生成”配置自动获取或分配邮箱;若当前是 `自定义邮箱池`,则会按邮箱池顺序取用
|
||||
- 如果你使用 Inbucket,它只是验证码收件箱,不会自动生成 Inbucket 地址
|
||||
|
||||
@@ -423,6 +424,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
- `Mail = QQ Mail`:Cloudflare 的 `Destination address / Destination addresses` 填你的 QQ 邮箱全地址
|
||||
- `Mail = 163 Mail`:Cloudflare 的 `Destination address / Destination addresses` 填你的 163 邮箱全地址
|
||||
- `Mail = 163 VIP Mail`:Cloudflare 的 `Destination address / Destination addresses` 填你的 163 VIP 邮箱全地址
|
||||
- `Mail = 126 Mail`:Cloudflare 的 `Destination address / Destination addresses` 填你的 126 邮箱全地址
|
||||
- `Mail = Inbucket`:仅当你的 Inbucket 实例本身就是一个真实可收外部邮件、且能收到 Cloudflare 验证邮件的地址时再使用
|
||||
- `Mail = Hotmail`:当前项目的自动流程不推荐和 Cloudflare 同时使用;因为 `Mail = Hotmail` 时,注册邮箱会直接使用 Hotmail 账号池邮箱
|
||||
|
||||
@@ -466,7 +468,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
1. 先在插件里点击 `获取`,拿到一个随机前缀邮箱
|
||||
2. 用另一个邮箱给这个地址发一封测试邮件
|
||||
3. 不要用目标邮箱给自己发测试邮件,否则某些邮箱服务会把它当成重复邮件直接吞掉
|
||||
4. 如果你的 `Mail` 选的是 `QQ / 163 / 163 VIP / Inbucket`,就去对应收件链路里确认这封测试邮件能否到达
|
||||
4. 如果你的 `Mail` 选的是 `QQ / 163 / 163 VIP / 126 / Inbucket`,就去对应收件链路里确认这封测试邮件能否到达
|
||||
|
||||
#### 官方参考
|
||||
|
||||
@@ -591,7 +593,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
|
||||
- `Hotmail`(远程服务 / 本地助手)
|
||||
- `content/qq-mail.js`
|
||||
- `content/mail-163.js`
|
||||
- `content/mail-163.js`(163 / 163 VIP / 126)
|
||||
- `content/inbucket-mail.js`
|
||||
|
||||
邮件匹配规则以以下关键词为主:
|
||||
@@ -771,9 +773,10 @@ content/utils.js 通用工具:等待元素、点击、日志、停
|
||||
content/vps-panel.js CPA 面板步骤:内部 OAuth 刷新 / Step 10
|
||||
content/signup-page.js ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 7 / 9
|
||||
hotmail-utils.js Hotmail 收信相关通用辅助
|
||||
mail-provider-utils.js 网页邮箱 provider 配置辅助
|
||||
content/duck-mail.js Duck 邮箱自动获取
|
||||
content/qq-mail.js QQ 邮箱验证码轮询
|
||||
content/mail-163.js 163 邮箱验证码轮询
|
||||
content/mail-163.js 163 / 163 VIP / 126 邮箱验证码轮询
|
||||
content/inbucket-mail.js Inbucket mailbox 验证码轮询
|
||||
sidepanel/ 侧边栏 UI
|
||||
```
|
||||
|
||||
+7
-1
@@ -754,6 +754,7 @@ function normalizeMailProvider(value = '') {
|
||||
case CLOUDFLARE_TEMP_EMAIL_PROVIDER:
|
||||
case '163':
|
||||
case '163-vip':
|
||||
case '126':
|
||||
case 'qq':
|
||||
case 'inbucket':
|
||||
case '2925':
|
||||
@@ -3856,6 +3857,8 @@ function is163MailHost(hostname = '') {
|
||||
}
|
||||
return hostname === 'mail.163.com'
|
||||
|| hostname.endsWith('.mail.163.com')
|
||||
|| hostname === 'mail.126.com'
|
||||
|| hostname.endsWith('.mail.126.com')
|
||||
|| hostname === 'webmail.vip.163.com';
|
||||
}
|
||||
|
||||
@@ -5956,7 +5959,7 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||||
if (!currentState.emailPrefix) {
|
||||
throw new Error('Gmail 原邮箱未设置,请先在侧边栏填写。');
|
||||
}
|
||||
await addLog(`=== 鐩爣 ${targetRun}/${totalRuns} 杞細Gmail +tag 妯″紡宸插惎鐢紝灏嗗湪姝ラ 3 鑷姩鐢熸垚閭锛堢 ${attemptRuns} 娆″皾璇曪級===`, 'info');
|
||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Gmail +tag 模式已启用,将在步骤 3 自动生成邮箱(第 ${attemptRuns} 次尝试)===`, 'info');
|
||||
return null;
|
||||
}
|
||||
if (!currentState.emailPrefix) {
|
||||
@@ -6904,6 +6907,9 @@ function getMailConfig(state) {
|
||||
if (provider === '163-vip') {
|
||||
return { source: 'mail-163', url: 'https://webmail.vip.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 VIP 邮箱' };
|
||||
}
|
||||
if (provider === '126') {
|
||||
return { source: 'mail-163', url: 'https://mail.126.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '126 邮箱' };
|
||||
}
|
||||
if (provider === 'inbucket') {
|
||||
const host = normalizeInbucketOrigin(state.inbucketHost);
|
||||
const mailbox = (state.inbucketMailbox || '').trim();
|
||||
|
||||
@@ -85,6 +85,8 @@
|
||||
function is163MailHost(hostname = '') {
|
||||
return hostname === 'mail.163.com'
|
||||
|| hostname.endsWith('.mail.163.com')
|
||||
|| hostname === 'mail.126.com'
|
||||
|| hostname.endsWith('.mail.126.com')
|
||||
|| hostname === 'webmail.vip.163.com';
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,9 @@ function extractVerificationCode(text) {
|
||||
const cnMatch = normalized.match(/(?:验证码|代码)[^0-9]{0,16}(\d{6})/i);
|
||||
if (cnMatch) return cnMatch[1];
|
||||
|
||||
const openAiLoginMatch = normalized.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (openAiLoginMatch) return openAiLoginMatch[1];
|
||||
|
||||
const enMatch = normalized.match(/(?:verification\s+code|temporary\s+verification\s+code|your\s+chatgpt\s+code|code(?:\s+is)?)[^0-9]{0,16}(\d{6})/i);
|
||||
if (enMatch) return enMatch[1];
|
||||
|
||||
|
||||
@@ -82,6 +82,9 @@ if (isTopFrame) {
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ function extractVerificationCode(text) {
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
@@ -85,7 +88,7 @@ function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail) {
|
||||
const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
|
||||
const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
|
||||
const code = extractVerificationCode(mail.combinedText);
|
||||
const keywordMatch = /openai|chatgpt|verify|verification|confirm|login|验证码|代码/.test(combined);
|
||||
const keywordMatch = /openai|chatgpt|verify|verification|confirm|log-?in|验证码|代码/.test(combined);
|
||||
|
||||
if (mailboxMatch) return { matched: true, mailboxMatch, code };
|
||||
if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
|
||||
|
||||
+25
-6
@@ -73,6 +73,21 @@ function normalizeText(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getNetEaseMailLabel(hostname) {
|
||||
const currentHostname = String(
|
||||
hostname || (typeof location !== 'undefined' ? location.hostname : '') || ''
|
||||
).toLowerCase();
|
||||
|
||||
if (currentHostname === 'mail.126.com' || currentHostname.endsWith('.mail.126.com')) {
|
||||
return '126 邮箱';
|
||||
}
|
||||
if (currentHostname === 'webmail.vip.163.com') {
|
||||
return '163 VIP 邮箱';
|
||||
}
|
||||
|
||||
return '163 邮箱';
|
||||
}
|
||||
|
||||
function isVisibleNode(node) {
|
||||
if (!node) return false;
|
||||
if (node.hidden) return false;
|
||||
@@ -114,7 +129,7 @@ function isLikelyMailItemNode(node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /发件人|验证码|verification|chatgpt|openai|code/i.test(summaryText);
|
||||
return /发件人|验证码|verification|chatgpt|openai|code|log-?in/i.test(summaryText);
|
||||
}
|
||||
|
||||
function findMailItems() {
|
||||
@@ -391,7 +406,7 @@ function selectOpenedMailTextCandidate(item, candidates = [], options = {}) {
|
||||
if (sender && lower.includes(sender)) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(extractVerificationCode(candidate) && /chatgpt|openai|verification|验证码|login code/i.test(lower));
|
||||
return Boolean(extractVerificationCode(candidate) && /chatgpt|openai|verification|验证码|log-?in\s+code/i.test(lower));
|
||||
}) || source[0] || '';
|
||||
|
||||
const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate)));
|
||||
@@ -476,8 +491,9 @@ async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [], filterAfterTimestamp = 0 } = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||
const mailLabel = getNetEaseMailLabel();
|
||||
|
||||
log(`步骤 ${step}:开始轮询 163 邮箱(最多 ${maxAttempts} 次)`);
|
||||
log(`步骤 ${step}:开始轮询 ${mailLabel}(最多 ${maxAttempts} 次)`);
|
||||
if (filterAfterMinute) {
|
||||
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
|
||||
}
|
||||
@@ -508,7 +524,7 @@ async function handlePollEmail(step, payload) {
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error('163 邮箱列表未加载完成,请确认当前已打开收件箱。');
|
||||
throw new Error(`${mailLabel}列表未加载完成,请确认当前已打开收件箱。`);
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||
@@ -520,7 +536,7 @@ async function handlePollEmail(step, payload) {
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
log(`步骤 ${step}:正在轮询 ${mailLabel},第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
if (attempt > 1) {
|
||||
await refreshInbox();
|
||||
@@ -605,7 +621,7 @@ async function handlePollEmail(step, payload) {
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 163 邮箱中找到新的匹配邮件。` +
|
||||
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 ${mailLabel}中找到新的匹配邮件。` +
|
||||
'请手动检查收件箱。'
|
||||
);
|
||||
}
|
||||
@@ -704,6 +720,9 @@ function extractVerificationCode(text) {
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
|
||||
@@ -670,7 +670,7 @@ function matchesMailFilters(text, senderFilters, subjectFilters) {
|
||||
|
||||
function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
||||
if (strictChatGPTCodeOnly) {
|
||||
const strictMatch = String(text || '').match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
|
||||
const strictMatch = String(text || '').match(/(?:your\s+chatgpt\s+code\s+is|(?:chatgpt\s+log-?in\s+code|suspicious\s+log-?in)[\s\S]{0,120}enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
return strictMatch ? strictMatch[1] : null;
|
||||
}
|
||||
|
||||
@@ -679,6 +679,9 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
||||
const matchCn = normalized.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = normalized.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
const matchChatGPT = normalized.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
|
||||
if (matchChatGPT) return matchChatGPT[1];
|
||||
|
||||
|
||||
@@ -174,6 +174,9 @@ function extractVerificationCode(text) {
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
// Pattern 2: English format "code is 370794" or "code: 370794"
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
+21
-5
@@ -2,13 +2,21 @@
|
||||
|
||||
const getActivationStrategy = self.MultiPageActivationUtils?.getActivationStrategy;
|
||||
|
||||
const SCRIPT_SOURCE = (() => {
|
||||
if (window.__MULTIPAGE_SOURCE) return window.__MULTIPAGE_SOURCE;
|
||||
const url = location.href;
|
||||
const hostname = location.hostname;
|
||||
function detectScriptSource({
|
||||
injectedSource,
|
||||
url = '',
|
||||
hostname = '',
|
||||
} = {}) {
|
||||
if (injectedSource) return injectedSource;
|
||||
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
|
||||
if (hostname === 'mail.qq.com' || hostname === 'wx.mail.qq.com') return 'qq-mail';
|
||||
if (hostname === 'mail.163.com' || hostname.endsWith('.mail.163.com') || hostname === 'webmail.vip.163.com') return 'mail-163';
|
||||
if (
|
||||
hostname === 'mail.163.com'
|
||||
|| hostname.endsWith('.mail.163.com')
|
||||
|| hostname === 'webmail.vip.163.com'
|
||||
|| hostname === 'mail.126.com'
|
||||
|| hostname.endsWith('.mail.126.com')
|
||||
) return 'mail-163';
|
||||
if (hostname === 'mail.google.com') return 'gmail-mail';
|
||||
if (hostname === 'www.icloud.com' || hostname === 'www.icloud.com.cn') return 'icloud-mail';
|
||||
if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
|
||||
@@ -16,6 +24,14 @@ const SCRIPT_SOURCE = (() => {
|
||||
if (url.includes("2925.com")) return "mail-2925";
|
||||
// VPS panel — detected dynamically since URL is configurable
|
||||
return 'vps-panel';
|
||||
}
|
||||
|
||||
const SCRIPT_SOURCE = (() => {
|
||||
return detectScriptSource({
|
||||
injectedSource: window.__MULTIPAGE_SOURCE,
|
||||
url: location.href,
|
||||
hostname: location.hostname,
|
||||
});
|
||||
})();
|
||||
|
||||
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = source.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1];
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = source.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
const matchChatGPT = source.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
|
||||
if (matchChatGPT) return matchChatGPT[1];
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||
const NETEASE_LIST_PATH = '/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D';
|
||||
|
||||
function normalizeMailProvider(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case HOTMAIL_PROVIDER:
|
||||
case '163':
|
||||
case '163-vip':
|
||||
case '126':
|
||||
case 'qq':
|
||||
case 'inbucket':
|
||||
return normalized;
|
||||
default:
|
||||
return '163';
|
||||
}
|
||||
}
|
||||
|
||||
function getMailProviderConfig(state = {}, options = {}) {
|
||||
const provider = normalizeMailProvider(state.mailProvider);
|
||||
const normalizeInbucketOrigin = options.normalizeInbucketOrigin || (() => '');
|
||||
|
||||
if (provider === HOTMAIL_PROVIDER) {
|
||||
return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(微软 Graph)' };
|
||||
}
|
||||
if (provider === '163') {
|
||||
return {
|
||||
source: 'mail-163',
|
||||
url: `https://mail.163.com${NETEASE_LIST_PATH}`,
|
||||
label: '163 邮箱',
|
||||
};
|
||||
}
|
||||
if (provider === '163-vip') {
|
||||
return {
|
||||
source: 'mail-163',
|
||||
url: `https://webmail.vip.163.com${NETEASE_LIST_PATH}`,
|
||||
label: '163 VIP 邮箱',
|
||||
};
|
||||
}
|
||||
if (provider === '126') {
|
||||
return {
|
||||
source: 'mail-163',
|
||||
url: `https://mail.126.com${NETEASE_LIST_PATH}`,
|
||||
label: '126 邮箱',
|
||||
};
|
||||
}
|
||||
if (provider === 'inbucket') {
|
||||
const host = normalizeInbucketOrigin(state.inbucketHost);
|
||||
const mailbox = String(state.inbucketMailbox || '').trim();
|
||||
if (!host) {
|
||||
return { error: 'Inbucket 主机地址为空或无效。' };
|
||||
}
|
||||
if (!mailbox) {
|
||||
return { error: 'Inbucket 邮箱名称为空。' };
|
||||
}
|
||||
return {
|
||||
source: 'inbucket-mail',
|
||||
url: `${host}/m/${encodeURIComponent(mailbox)}/`,
|
||||
label: `Inbucket 邮箱(${mailbox})`,
|
||||
navigateOnReuse: true,
|
||||
inject: ['content/activation-utils.js', 'content/utils.js', 'content/inbucket-mail.js'],
|
||||
injectSource: 'inbucket-mail',
|
||||
};
|
||||
}
|
||||
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' };
|
||||
}
|
||||
|
||||
const api = {
|
||||
HOTMAIL_PROVIDER,
|
||||
getMailProviderConfig,
|
||||
normalizeMailProvider,
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = api;
|
||||
}
|
||||
|
||||
if (typeof self !== 'undefined') {
|
||||
self.MailProviderUtils = api;
|
||||
}
|
||||
+3
-1
@@ -70,7 +70,9 @@
|
||||
"matches": [
|
||||
"https://mail.163.com/*",
|
||||
"https://*.mail.163.com/*",
|
||||
"https://webmail.vip.163.com/*"
|
||||
"https://webmail.vip.163.com/*",
|
||||
"https://mail.126.com/*",
|
||||
"https://*.mail.126.com/*"
|
||||
],
|
||||
"js": [
|
||||
"content/activation-utils.js",
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
<option value="icloud">iCloud 邮箱</option>
|
||||
<option value="163">163 邮箱 (mail.163.com)</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
|
||||
<option value="126">126 邮箱 (mail.126.com)</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||
<option value="2925">2925 邮箱 (2925.com)</option>
|
||||
|
||||
@@ -602,6 +602,11 @@ const MAIL_PROVIDER_LOGIN_CONFIGS = {
|
||||
url: 'https://webmail.vip.163.com/',
|
||||
buttonLabel: '登录',
|
||||
},
|
||||
'126': {
|
||||
label: '126 邮箱',
|
||||
url: 'https://mail.126.com/',
|
||||
buttonLabel: '登录',
|
||||
},
|
||||
qq: {
|
||||
label: 'QQ 邮箱',
|
||||
url: 'https://wx.mail.qq.com/',
|
||||
@@ -2166,7 +2171,7 @@ function applySettingsState(state) {
|
||||
inputCodex2ApiUrl.value = state?.codex2apiUrl || '';
|
||||
inputCodex2ApiAdminKey.value = state?.codex2apiAdminKey || '';
|
||||
const restoredMailProvider = isCustomMailProvider(state?.mailProvider)
|
||||
|| [ICLOUD_PROVIDER, 'hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim())
|
||||
|| [ICLOUD_PROVIDER, 'hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', '126', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim())
|
||||
? String(state?.mailProvider || '163').trim()
|
||||
: (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom'
|
||||
|| String(state?.emailGenerator || '').trim().toLowerCase() === 'manual'
|
||||
|
||||
@@ -16,6 +16,26 @@ test('navigation utils module exposes a factory', () => {
|
||||
assert.equal(typeof api?.createNavigationUtils, 'function');
|
||||
});
|
||||
|
||||
test('navigation utils treat 126 mail hosts as part of the shared NetEase mail family', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const moduleApi = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
|
||||
const navigationUtils = moduleApi.createNavigationUtils({
|
||||
DEFAULT_SUB2API_URL: 'https://example.com/admin/accounts',
|
||||
normalizeLocalCpaStep9Mode: value => value,
|
||||
});
|
||||
|
||||
assert.equal(navigationUtils.is163MailHost('mail.126.com'), true);
|
||||
assert.equal(
|
||||
navigationUtils.matchesSourceUrlFamily(
|
||||
'mail-163',
|
||||
'https://mail.126.com/js6/main.jsp',
|
||||
'https://mail.163.com/js6/main.jsp'
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('navigation utils support codex2api mode and url normalization', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/utils.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const ch = source[index];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('detectScriptSource maps 126 mail hosts to the shared 163 mail source', () => {
|
||||
const bundle = [extractFunction('detectScriptSource')].join('\n');
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { detectScriptSource };
|
||||
`)();
|
||||
|
||||
assert.equal(
|
||||
api.detectScriptSource({
|
||||
url: 'https://mail.126.com/js6/main.jsp',
|
||||
hostname: 'mail.126.com',
|
||||
}),
|
||||
'mail-163'
|
||||
);
|
||||
});
|
||||
|
||||
test('detectScriptSource maps 126 mail subdomains to the shared 163 mail source', () => {
|
||||
const bundle = [extractFunction('detectScriptSource')].join('\n');
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { detectScriptSource };
|
||||
`)();
|
||||
|
||||
assert.equal(
|
||||
api.detectScriptSource({
|
||||
url: 'https://app.mail.126.com/js6/main.jsp',
|
||||
hostname: 'app.mail.126.com',
|
||||
}),
|
||||
'mail-163'
|
||||
);
|
||||
});
|
||||
@@ -181,6 +181,7 @@ test('shouldClearHotmailCurrentSelection returns true only when account becomes
|
||||
test('extractVerificationCode returns first six-digit code from multilingual mail text', () => {
|
||||
assert.equal(extractVerificationCode('你的 ChatGPT 验证码为 370794,请勿泄露。'), '370794');
|
||||
assert.equal(extractVerificationCode('Your verification code is 654321.'), '654321');
|
||||
assert.equal(extractVerificationCode('ChatGPT Log-in Code\nIf that was you, enter this code:\n\n982219'), '982219');
|
||||
assert.equal(extractVerificationCode('No code here'), null);
|
||||
});
|
||||
|
||||
@@ -202,6 +203,15 @@ test('extractVerificationCodeFromMessage reads code from the latest message subj
|
||||
}),
|
||||
'654321'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
extractVerificationCodeFromMessage({
|
||||
subject: 'ChatGPT Log-in Code',
|
||||
bodyPreview: 'We noticed a suspicious log-in on your account. If that was you, enter this code:\n\n982219',
|
||||
from: { emailAddress: { address: 'noreply@openai.com' } },
|
||||
}),
|
||||
'982219'
|
||||
);
|
||||
});
|
||||
|
||||
test('getHotmailListToggleLabel reflects expanded state and account count', () => {
|
||||
|
||||
@@ -81,6 +81,20 @@ return { readOpenedMailBody, extractVerificationCode };
|
||||
assert.equal(api.extractVerificationCode(bodyText), '731091');
|
||||
});
|
||||
|
||||
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
||||
const bundle = [
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { extractVerificationCode };
|
||||
`)();
|
||||
|
||||
const bodyText = 'ChatGPT Log-in Code\nWe noticed a suspicious log-in on your account. If that was you, enter this code:\n\n982219';
|
||||
assert.equal(api.extractVerificationCode(bodyText), '982219');
|
||||
});
|
||||
|
||||
test('readOpenedMailBody ignores conversation list rows when no detail pane is open', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
|
||||
@@ -68,6 +68,20 @@ test('normalizeLuckmailTokenCode and normalizeLuckmailTokenMail extract verifica
|
||||
|
||||
assert.equal(normalizedMail.verification_code, '654321');
|
||||
assert.equal(extractLuckmailVerificationCode('你的验证码为 778899'), '778899');
|
||||
assert.equal(
|
||||
extractLuckmailVerificationCode('ChatGPT Log-in Code\nIf that was you, enter this code:\n\n982219'),
|
||||
'982219'
|
||||
);
|
||||
|
||||
const suspiciousLoginMail = normalizeLuckmailTokenMail({
|
||||
message_id: 'mail-3',
|
||||
from: 'noreply@openai.com',
|
||||
subject: 'ChatGPT Log-in Code',
|
||||
body: 'We noticed a suspicious log-in on your account. If that was you, enter this code:\n\n982219',
|
||||
received_at: '2026-04-14T10:02:00Z',
|
||||
});
|
||||
|
||||
assert.equal(suspiciousLoginMail.verification_code, '982219');
|
||||
});
|
||||
|
||||
test('normalizeLuckmailProjectName and isLuckmailPurchaseForProject match openai case-insensitively', () => {
|
||||
|
||||
@@ -101,6 +101,23 @@ return { findMailItems };
|
||||
assert.equal(rows.length, 1);
|
||||
});
|
||||
|
||||
test('getNetEaseMailLabel returns the active NetEase mailbox brand', () => {
|
||||
const bundle = [
|
||||
extractFunction('getNetEaseMailLabel'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { getNetEaseMailLabel };
|
||||
`)();
|
||||
|
||||
assert.equal(api.getNetEaseMailLabel('mail.126.com'), '126 邮箱');
|
||||
assert.equal(api.getNetEaseMailLabel('app.mail.126.com'), '126 邮箱');
|
||||
assert.equal(api.getNetEaseMailLabel('webmail.vip.163.com'), '163 VIP 邮箱');
|
||||
assert.equal(api.getNetEaseMailLabel('mail.163.com'), '163 邮箱');
|
||||
assert.equal(api.getNetEaseMailLabel('example.com'), '163 邮箱');
|
||||
});
|
||||
|
||||
test('getMailTimestamp parses visible hh:mm text even when no title attribute exists', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
@@ -340,14 +357,30 @@ return { openMailAndGetMessageText, mailItem };
|
||||
assert.doesNotMatch(text, /111111/);
|
||||
});
|
||||
|
||||
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
||||
const bundle = [
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { extractVerificationCode };
|
||||
`)();
|
||||
|
||||
const bodyText = 'ChatGPT Log-in Code\nWe noticed a suspicious log-in on your account. If that was you, enter this code:\n\n982219';
|
||||
assert.equal(api.extractVerificationCode(bodyText), '982219');
|
||||
});
|
||||
|
||||
test('handlePollEmail ignores same-minute old snapshot mail before fallback', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('getNetEaseMailLabel'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const location = { hostname: 'mail.126.com' };
|
||||
let currentItems = [{ id: 'old-mail' }];
|
||||
const seenCodes = new Set();
|
||||
|
||||
@@ -405,7 +438,7 @@ return { handlePollEmail };
|
||||
intervalMs: 1,
|
||||
filterAfterTimestamp: new Date(2026, 3, 22, 22, 22, 40, 0).getTime(),
|
||||
}),
|
||||
/未在 163 邮箱中找到新的匹配邮件/
|
||||
/未在 126 邮箱中找到新的匹配邮件/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -413,6 +446,7 @@ test('handlePollEmail accepts a new same-minute mail that appears after the snap
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('getNetEaseMailLabel'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
@@ -505,6 +539,7 @@ test('handlePollEmail falls back to row text when the subject node is missing',
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('getNetEaseMailLabel'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
@@ -584,6 +619,7 @@ test('handlePollEmail opens matching mail body when preview has no code', async
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('getNetEaseMailLabel'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('manifest 为网易邮箱内容脚本覆盖 126 子域名', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
const mail163Script = manifest.content_scripts.find((script) => (
|
||||
Array.isArray(script.js) && script.js.includes('content/mail-163.js')
|
||||
));
|
||||
|
||||
assert.ok(mail163Script, '应存在 mail-163 内容脚本声明');
|
||||
assert.equal(
|
||||
mail163Script.matches.includes('https://mail.126.com/*'),
|
||||
true,
|
||||
'应覆盖 mail.126.com'
|
||||
);
|
||||
assert.equal(
|
||||
mail163Script.matches.includes('https://*.mail.126.com/*'),
|
||||
true,
|
||||
'应覆盖 *.mail.126.com'
|
||||
);
|
||||
});
|
||||
@@ -546,6 +546,21 @@ return {
|
||||
]);
|
||||
});
|
||||
|
||||
test('extractVerificationCode strict mode matches the new suspicious log-in mail body', () => {
|
||||
const bundle = [
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { extractVerificationCode };
|
||||
`)();
|
||||
|
||||
const bodyText = 'ChatGPT Log-in Code\nWe noticed a suspicious log-in on your account. If that was you, enter this code:\n\n982219';
|
||||
assert.equal(api.extractVerificationCode(bodyText, true), '982219');
|
||||
assert.equal(api.extractVerificationCode(bodyText, false), '982219');
|
||||
});
|
||||
|
||||
test('openMailAndGetMessageText always returns to inbox after opening a 2925 message', async () => {
|
||||
const bundle = [
|
||||
extractFunction('returnToInbox'),
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
HOTMAIL_PROVIDER,
|
||||
getMailProviderConfig,
|
||||
normalizeMailProvider,
|
||||
} = require('../mail-provider-utils.js');
|
||||
|
||||
test('normalizeMailProvider accepts 126 and falls back to 163', () => {
|
||||
assert.equal(normalizeMailProvider('126'), '126');
|
||||
assert.equal(normalizeMailProvider('163-vip'), '163-vip');
|
||||
assert.equal(normalizeMailProvider('unknown-provider'), '163');
|
||||
});
|
||||
|
||||
test('getMailProviderConfig returns the shared NetEase source for 126 mail', () => {
|
||||
assert.deepEqual(
|
||||
getMailProviderConfig({ mailProvider: '126' }),
|
||||
{
|
||||
source: 'mail-163',
|
||||
url: 'https://mail.126.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D',
|
||||
label: '126 邮箱',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('getMailProviderConfig preserves the hotmail provider sentinel', () => {
|
||||
assert.deepEqual(
|
||||
getMailProviderConfig({ mailProvider: HOTMAIL_PROVIDER }),
|
||||
{
|
||||
provider: HOTMAIL_PROVIDER,
|
||||
label: 'Hotmail(微软 Graph)',
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -48,6 +48,7 @@ const helperBundle = [
|
||||
extractFunction(helperSource, 'parseUrlSafely'),
|
||||
extractFunction(helperSource, 'isSignupPageHost'),
|
||||
extractFunction(helperSource, 'isSignupEntryHost'),
|
||||
extractFunction(helperSource, 'is163MailHost'),
|
||||
extractFunction(helperSource, 'matchesSourceUrlFamily'),
|
||||
].join('\n');
|
||||
|
||||
@@ -153,6 +154,11 @@ return {
|
||||
true,
|
||||
'signup-page family should include legacy chat.openai.com'
|
||||
);
|
||||
assert.strictEqual(
|
||||
api.matchesSourceUrlFamily('mail-163', 'https://mail.126.com/js6/main.jsp', 'https://mail.163.com/js6/main.jsp'),
|
||||
true,
|
||||
'mail-163 family should include mail.126.com'
|
||||
);
|
||||
|
||||
api.reset({
|
||||
tabs: [
|
||||
|
||||
+20
-3
@@ -593,7 +593,24 @@ Codex2API 补充:
|
||||
- [luckmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/luckmail-utils.js)
|
||||
- LuckMail 相关后台领域逻辑仍在 [background.js](c:/Users/projectf/Downloads/codex注册扩展/background.js)
|
||||
|
||||
### 7.4 2925 账号池
|
||||
### 7.4 163 / 163 VIP / 126 网页邮箱
|
||||
|
||||
组成:
|
||||
|
||||
- [mail-provider-utils.js](c:/Users/projectf/Downloads/codex注册扩展/mail-provider-utils.js)
|
||||
- [content/mail-163.js](c:/Users/projectf/Downloads/codex注册扩展/content/mail-163.js)
|
||||
|
||||
行为约定:
|
||||
|
||||
- `163`、`163 VIP`、`126` 都走同一条“网易网页邮箱”验证码链路。
|
||||
- sidepanel 只负责切换 provider 与展示登录入口;后台根据 provider 选择对应网页邮箱首页。
|
||||
- 内容脚本来源统一归类到 `mail-163`,这样 Step 4 / Step 8 继续复用同一套验证码读取与邮件清理逻辑。
|
||||
- `manifest.json` 需要同时覆盖:
|
||||
- `https://mail.163.com/*`
|
||||
- `https://webmail.vip.163.com/*`
|
||||
- `https://mail.126.com/*`
|
||||
|
||||
### 7.5 2925 账号池
|
||||
|
||||
组成:
|
||||
|
||||
@@ -625,7 +642,7 @@ Codex2API 补充:
|
||||
8. sidepanel 中 2925 账号池的新增表单也走与 Hotmail 相同的共享交互:默认收起,头部按钮切换“添加账号 / 取消添加”,操作行右侧提供“批量导入”,保存成功后自动收起并清空。
|
||||
9. 当 2925 号池模式开启时,当前选中的号池邮箱会同步回写到同一个 `mail2925BaseEmail` 字段;因此用户切换号池账号后,即使再次关闭号池模式,也会直接沿用刚才选中的邮箱作为手动基邮箱,无需重新输入。
|
||||
|
||||
### 7.4.1 2925 双模式维护约定
|
||||
### 7.5.1 2925 双模式维护约定
|
||||
|
||||
这是后续维护 `2925` 时最容易被重新打散的一段链路,建议按下面的职责边界理解:
|
||||
|
||||
@@ -677,7 +694,7 @@ Codex2API 补充:
|
||||
4. `background/verification-flow.js` 与 `content/mail-2925.js` 的“弱目标邮箱匹配”是否保持同步
|
||||
5. `项目文件结构说明.md` 与当前文件是否已同步更新
|
||||
|
||||
### 7.5 iCloud
|
||||
### 7.6 iCloud
|
||||
|
||||
组成:
|
||||
|
||||
|
||||
+4
-1
@@ -26,6 +26,7 @@
|
||||
- `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。
|
||||
- `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。
|
||||
- `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。
|
||||
- `mail-provider-utils.js`:网页邮箱 provider 配置纯工具,负责 `163 / 163 VIP / 126 / QQ / Inbucket / Hotmail` 的基础归一化与页面入口配置。
|
||||
- `mail2925-utils.js`:2925 账号池相关的纯工具函数,负责账号归一化、冷却期判断、可用账号挑选、批量导入解析与列表更新。
|
||||
- `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案;当前还统一承接 `mail2925Mode` 的归一化与“2925 仅在 provide 模式下参与别名生成”的共享判定。
|
||||
- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。
|
||||
@@ -77,7 +78,7 @@
|
||||
- `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
|
||||
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
|
||||
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
|
||||
- `content/mail-163.js`:163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
|
||||
- `content/mail-163.js`:163 / 163 VIP / 126 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
|
||||
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路;当后台显式开启 `mail2925MatchTargetEmail` 时,会对邮件里显式出现的目标邮箱做弱匹配,避免 `receive` 模式误捞别人的验证码。
|
||||
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
|
||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||
@@ -168,12 +169,14 @@
|
||||
- `tests/phone-verification-flow.test.js`:测试手机号验证共享流程对 HeroSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。
|
||||
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
|
||||
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
|
||||
- `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。
|
||||
- `tests/hotmail-api-mode.test.js`:测试 Hotmail API 模式相关文案和集成方式。
|
||||
- `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。
|
||||
- `tests/hotmail-utils.test.js`:测试 Hotmail 工具层的账号筛选、验证码提取和消息匹配逻辑。
|
||||
- `tests/icloud-mail-content.test.js`:测试 iCloud Mail 内容脚本读取邮件正文和选中状态逻辑。
|
||||
- `tests/icloud-utils.test.js`:测试 iCloud 工具层的 host、别名列表、保留状态与筛选逻辑。
|
||||
- `tests/luckmail-utils.test.js`:测试 LuckMail 工具层的购买记录、邮件、游标和验证码匹配逻辑。
|
||||
- `tests/mail-provider-utils.test.js`:测试网页邮箱 provider 配置工具对 `126` 等 provider 的归一化与入口配置解析。
|
||||
- `tests/mail-2925-content.test.js`:测试 2925 邮箱内容脚本的轮询快照、忽略邮箱对比、按步骤会话隔离已试验证码、单封邮件删除与整箱删除逻辑。
|
||||
- `tests/mail2925-utils.test.js`:测试 2925 账号池工具层的账号归一化、冷却期判断、可用账号挑选与批量导入解析。
|
||||
- `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。
|
||||
|
||||
Reference in New Issue
Block a user