Explorar o código

feat: add support for Inbucket mailbox configuration and dynamic host input

Jimmy hai 4 meses
pai
achega
f79166b96d
Modificáronse 7 ficheiros con 449 adicións e 42 borrados
  1. 359 26
      README.md
  2. 69 5
      background.js
  3. 1 1
      content/inbucket-mail.js
  4. 1 1
      content/utils.js
  5. 0 8
      manifest.json
  6. 5 1
      sidepanel/sidepanel.html
  7. 14 0
      sidepanel/sidepanel.js

+ 359 - 26
README.md

@@ -1,40 +1,373 @@
 # Multi-Page Automation
 # Multi-Page Automation
 
 
-ChatGPT OAuth 批量注册自动化 Chrome 扩展。
+一个用于批量跑通 ChatGPT OAuth 注册/登录流程的 Chrome 扩展。
 
 
-## 前提
+当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / Inbucket mailbox 协助获取验证码。
 
 
-- 注册 [DuckDuckGo Email Protection](https://duckduckgo.com/email/),获取 @duck.com 邮箱地址,转发目标设为 163 或 QQ 邮箱
-- Chrome 浏览器,开发者模式加载本扩展
-- 提前登录好 VPS 管理面板和对应的邮箱(163 或 QQ)
+## 当前能力
+
+- 从 VPS 面板自动获取 OpenAI OAuth 授权链接
+- 自动打开 OpenAI 注册页并点击 `Sign up / Register`
+- 自动填写邮箱与密码
+- 支持自定义密码;留空时自动生成强密码
+- 自动显示当前使用中的密码,便于后续保存
+- 自动获取注册验证码与登录验证码
+- 支持 `QQ Mail`、`163 Mail`、`Inbucket mailbox`
+- 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址
+- Step 5 同时兼容两种页面:
+  - 页面要求填写 `birthday`
+  - 页面要求填写 `age`
+- 支持 `Auto` 多轮运行
+- 支持中途 `Stop`
+- Step 8 会自动寻找 OAuth 同意页的“继续”按钮,并通过 Chrome debugger 输入事件发起点击,然后监听本地回调地址
+
+
+## 环境要求
+
+- Chrome 浏览器
+- 打开扩展开发者模式
+- 你自己的 VPS 管理面板,且页面结构与当前脚本适配
+- 至少准备一种验证码接收方式:
+  - DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发
+  - 手动填写一个可收信邮箱
+- 如果使用 `QQ` / `163` / `Inbucket`,对应页面需要提前能正常打开
 
 
 ## 安装
 ## 安装
 
 
-1. 打开 `chrome://extensions/`,开启「开发者模式」
-2. 点击「加载已解压的扩展程序」,选择本项目文件夹
-3. 点击浏览器工具栏的扩展图标,打开侧边面板
+1. 打开 `chrome://extensions/`
+2. 开启“开发者模式”
+3. 点击“加载已解压的扩展程序”
+4. 选择本项目目录
+5. 打开扩展侧边栏
+
+## 侧边栏配置说明
+
+### `VPS`
+
+你的管理面板 OAuth 页面地址,例如:
+
+```txt
+http(s)://<your-host>/management.html#/oauth
+```
+
+Step 1 和 Step 9 都依赖这个地址。
+
+### `Mail`
+
+支持三种验证码来源:
+
+- `163 Mail`
+- `QQ Mail`
+- `Inbucket`
+
+说明:
+
+- `QQ` 和 `163` 用于直接轮询网页邮箱
+- `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https://<your-inbucket-host>/m/<mailbox>/`
+
+### `Mailbox`
+
+仅当 `Mail = Inbucket` 时显示。
+
+填写 Inbucket mailbox 名称,例如:
+
+```txt
+tmp-mailbox
+```
+
+脚本会自动打开:
+
+```txt
+https://<your-inbucket-host>/m/<mailbox>/
+```
+
+并且只检索未读邮件:
+
+- 只匹配 `.message-list-entry.unseen`
+- 第 2 次轮询开始会自动点击 mailbox 页面上的刷新按钮
+- 识别到验证码后会尝试删除当前邮件,减少重复命中
+
+### `Inbucket`
+
+仅当 `Mail = Inbucket` 时显示。
+
+这里填写 Inbucket host,支持两种格式:
+
+- `your-inbucket-host`
+- `https://your-inbucket-host`
+
+脚本会自动规范化成 origin 后再拼接 mailbox URL。
+
+### `Email`
+
+Step 3 使用的注册邮箱。
+
+来源有两种:
+
+- 手动粘贴
+- 点击 `Auto` 从 DuckDuckGo Email Protection 自动获取一个新的 `@duck.com`
+
+注意:
+
+- 当前 `Auto` 按钮只负责 DuckDuckGo 地址获取
+- 如果你使用 Inbucket,它只是验证码收件箱,不会自动生成 Inbucket 地址
+
+### `Password`
+
+- 留空:自动生成强密码
+- 手动输入:使用你自定义的密码
+- 可通过 `Show / Hide` 按钮切换显示
+
+扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。
+
+### `Auto`
+
+整套流程自动跑。
+
+支持多轮运行,运行次数由右上角数字框决定。
+
+## 工作流
+
+### 单步模式
+
+侧边栏共有 9 个步骤按钮,可逐步执行:
+
+1. `Get OAuth Link`
+2. `Open Signup`
+3. `Fill Email / Password`
+4. `Get Signup Code`
+5. `Fill Name / Birthday`
+6. `Login via OAuth`
+7. `Get Login Code`
+8. `Manual OAuth Confirm`
+9. `VPS Verify`
+
+### Auto 模式
+
+点击右上角 `Auto` 后,后台会按顺序跑完整流程。
+
+当前 Auto 逻辑是:
+
+1. Step 1 获取 VPS OAuth 链接
+2. Step 2 打开 OpenAI 注册页
+3. 尝试自动获取 Duck 邮箱
+4. 如果 Duck 自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
+5. 继续执行 Step 3 ~ Step 9
+
+也就是说:
+
+- 如果 Duck 邮箱可自动获取,整套流程更接近全自动
+- 如果不能自动获取,Auto 会在邮箱阶段暂停
+
+## 详细步骤说明
+
+### Step 1: Get OAuth Link
+
+通过 `content/vps-panel.js`:
+
+- 打开 VPS OAuth 面板
+- 等待 `Codex OAuth` 卡片出现
+- 点击“登录”
+- 读取页面里的授权链接
+
+结果会保存到侧边栏的 `OAuth` 字段。
+
+### Step 2: Open Signup
+
+通过 `content/signup-page.js`:
+
+- 打开授权链接
+- 查找 `Sign up / Register / 创建账户` 按钮
+- 自动点击进入注册流程
+
+### Step 3: Fill Email / Password
+
+- 自动填写邮箱
+- 如页面先要求邮箱,再进入密码页,会自动切页继续填写
+- 使用自定义密码或自动生成密码
+- 提交注册表单
+
+实际使用的密码会写入会话状态,并同步到侧边栏显示。
+
+### Step 4: Get Signup Code
+
+根据 `Mail` 配置,轮询邮箱并提取 6 位验证码。
+
+支持:
+
+- `content/qq-mail.js`
+- `content/mail-163.js`
+- `content/inbucket-mail.js`
+
+邮件匹配规则以以下关键词为主:
+
+- 发件人:`openai`、`noreply`、`verify`、`auth`、`duckduckgo`、`forward`
+- 标题:`verify`、`verification`、`code`、`验证`、`confirm`
+
+### Step 5: Fill Name / Birthday
+
+随机生成人名与生日。
+
+当前脚本支持两种页面结构:
+
+- 页面要求 `birthday`
+- 页面要求 `age`
+
+如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。
+
+### Step 6: Login via OAuth
+
+重新打开 OAuth 链接,使用刚注册的账号登录。
+
+支持:
+
+- 邮箱 + 密码登录
+- 提交后进入验证码验证流程
+
+### Step 7: Get Login Code
+
+与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。
+
+### Step 8: Manual OAuth Confirm
+
+虽然按钮名称还是 `Manual OAuth Confirm`,但当前代码已经做了自动尝试:
+
+- 在授权页定位“继续”按钮
+- 等待按钮可点击
+- 获取按钮坐标
+- 通过 Chrome `debugger` 的输入事件点击该按钮
+- 同时监听 `chrome.webNavigation.onBeforeNavigate`
+- 一旦捕获本地回调地址,就把结果保存到 `Callback`
+
+注意:
+
+- 这一步仍然是最容易因页面变化而失效的一步
+- 如果 120 秒内没有捕获到 localhost 回调,会报错超时
+- README 中的按钮名称沿用了旧文案,但代码行为是“自动尝试点击”
+
+### Step 9: VPS Verify
+
+回到 VPS 面板:
+
+- 自动填写 localhost 回调地址
+- 自动点击“提交回调 URL”
+- 等待“认证成功”状态
+
+## Duck 邮箱自动获取
+
+通过 `content/duck-mail.js`:
+
+- 打开 DuckDuckGo Email Protection Autofill 设置页
+- 查找当前私有地址
+- 如需要,点击 `Generate Private Duck Address`
+- 读取新的 `@duck.com` 地址
+
+这个功能会被:
+
+- 侧边栏 `Email` 旁边的 `Auto` 按钮使用
+- `Auto Run` 流程优先尝试使用
+
+## 停止机制
+
+扩展内置了停止当前流程的能力:
+
+- 侧边栏点击 `Stop`
+- Background 会广播 `STOP_FLOW`
+- 各 content script 会在等待、轮询、sleep、元素查找中尽量中断
+
+适合以下场景:
+
+- 卡在某一步
+- 邮件迟迟不来
+- 页面结构变化导致等待超时
+
+## 状态与数据
+
+主要使用 `chrome.storage.session` 保存运行时状态:
+
+- 当前步骤
+- 每一步状态
+- OAuth 链接
+- 当前邮箱
+- 当前密码
+- localhost 回调地址
+- 账号记录
+- tab 注册信息
+- 自定义设置
+
+特点:
+
+- 浏览器会话级存储
+- 扩展运行期间可在多个步骤之间共享
+- 代码里已启用 `storage.session` 对 content script 的访问
+
+## 项目结构
+
+```txt
+background.js              后台主控,编排 1~9 步、Tab 复用、状态管理
+manifest.json              扩展清单
+data/names.js              随机姓名、生日数据
+content/utils.js           通用工具:等待元素、点击、日志、停止控制
+content/vps-panel.js       VPS 面板步骤:Step 1 / Step 9
+content/signup-page.js     OpenAI 注册/登录页步骤:Step 2 / 3 / 5 / 6 / 8
+content/duck-mail.js       Duck 邮箱自动获取
+content/qq-mail.js         QQ 邮箱验证码轮询
+content/mail-163.js        163 邮箱验证码轮询
+content/inbucket-mail.js   Inbucket mailbox 验证码轮询
+sidepanel/                 侧边栏 UI
+```
+
+## 常见使用建议
+
+### 1. 先单步验证,再开 Auto
+
+推荐先手动跑通一次:
+
+1. Step 1
+2. Step 2
+3. Step 3
+4. Step 4
+
+确认邮箱和验证码链路稳定后,再使用 `Auto`。
+
+### 2. Inbucket 建议使用专用 mailbox
+
+当前 Inbucket 逻辑只看未读邮件,但还是建议:
+
+- 给脚本准备一个相对独立的 mailbox
+- 避免收件箱里混入过多无关邮件
+
+### 3. Duck 自动获取失败时直接手填
+
+如果 Duck 页面打不开、未登录或按钮变化:
+
+- 直接在 `Email` 输入框中粘贴邮箱
+- 再继续执行 Step 3 或 Auto Continue
+
+### 4. Step 8 失败时重点检查
+
+- OAuth 同意页 DOM 是否变化
+- “继续”按钮是否变成了别的文案
+- localhost 回调是否真的触发
+- 浏览器是否允许 debugger 附加
 
 
-## 执行流程
+## 已知限制
 
 
-在侧边面板填入 VPS 管理面板地址,选择邮箱类型,设置运行次数,点击 Auto:
+- Step 8 对页面结构较敏感
+- Duck 自动获取依赖 Duck 页面真实 DOM
+- VPS 面板 DOM 也需要和当前脚本选择器匹配
+- `Auto` 按钮名称和 Step 8 的旧文案还未完全统一,但代码行为以实际实现为准
 
 
-1. **获取 OAuth 链接** — 自动打开 VPS 面板,点击登录拿到 OpenAI 授权链接
-2. **打开注册页** — 用授权链接打开 OpenAI 注册页,自动点击 Sign up
-3. ⏸️ **暂停等待** — 面板弹出橙色提示,**手动粘贴一个新的 @duck.com 邮箱地址**,点击 Continue
-4. **填写注册信息** — 自动将 @duck.com 地址填入注册表单,自动生成随机密码,提交
-5. **获取注册验证码** — 自动切到邮箱轮询收件箱,提取 6 位验证码,填回注册页
-6. **完善资料** — 自动随机生成英文姓名和生日,提交完成注册
-7. **登录** — 重新打开 OAuth 链接,用刚注册的账号自动登录
-8. **获取登录验证码** — 再次轮询邮箱拿验证码,自动填入
-9. **完成 OAuth 授权** — 自动点击授权页「继续」;如果页面不接受普通 DOM 点击,会自动切换到更强的点击兜底并继续捕获回调地址
-10. **VPS 回调验证** — 自动将回调地址提交到 VPS 面板,完成认证
+## 调试建议
 
 
-> 全程仅需在第 3 步手动粘贴一次邮箱地址,其余全自动。
-> 支持设置 N 次连续批量运行,每轮在第 2 步后暂停等待粘贴新邮箱。
+- 打开扩展侧边栏看日志
+- 查看 Service Worker 控制台
+- 查看目标页面的 content script 控制台日志
+- 当某一步频繁失败时,优先检查当前页面选择器是否仍然匹配
 
 
 ## 安全说明
 ## 安全说明
 
 
-- VPS 地址在面板中以密码框显示,不会明文泄露
-- 每次注册自动生成随机强密码(14位,含大小写+数字+符号)
-- 所有状态存储在 `chrome.storage.session`,浏览器关闭即清除
-- 代码中无硬编码的密码、IP 地址等敏感信息
+- 所有状态仅保存在浏览器会话中
+- 没有硬编码你的 VPS 地址、密码或账户
+- 自定义密码只存在当前会话存储中
+- 邮箱和密码会被记录到本轮 `accounts` 中,便于追踪本次运行结果

+ 69 - 5
background.js

@@ -32,6 +32,7 @@ const DEFAULT_STATE = {
   vpsUrl: '',
   vpsUrl: '',
   customPassword: '',
   customPassword: '',
   mailProvider: '163', // 'qq' or '163'
   mailProvider: '163', // 'qq' or '163'
+  inbucketHost: '',
   inbucketMailbox: '',
   inbucketMailbox: '',
 };
 };
 
 
@@ -86,6 +87,7 @@ async function resetState() {
     'vpsUrl',
     'vpsUrl',
     'customPassword',
     'customPassword',
     'mailProvider',
     'mailProvider',
+    'inbucketHost',
     'inbucketMailbox',
     'inbucketMailbox',
   ]);
   ]);
   await chrome.storage.session.clear();
   await chrome.storage.session.clear();
@@ -98,6 +100,7 @@ async function resetState() {
     vpsUrl: prev.vpsUrl || '',
     vpsUrl: prev.vpsUrl || '',
     customPassword: prev.customPassword || '',
     customPassword: prev.customPassword || '',
     mailProvider: prev.mailProvider || '163',
     mailProvider: prev.mailProvider || '163',
+    inbucketHost: prev.inbucketHost || '',
     inbucketMailbox: prev.inbucketMailbox || '',
     inbucketMailbox: prev.inbucketMailbox || '',
   });
   });
 }
 }
@@ -241,6 +244,15 @@ async function reuseOrCreateTab(source, url, options = {}) {
       if (options.inject) {
       if (options.inject) {
         if (registry[source]) registry[source].ready = false;
         if (registry[source]) registry[source].ready = false;
         await setState({ tabRegistry: registry });
         await setState({ tabRegistry: registry });
+        if (options.injectSource) {
+          await chrome.scripting.executeScript({
+            target: { tabId },
+            func: (injectedSource) => {
+              window.__MULTIPAGE_SOURCE = injectedSource;
+            },
+            args: [options.injectSource],
+          });
+        }
         await chrome.scripting.executeScript({
         await chrome.scripting.executeScript({
           target: { tabId },
           target: { tabId },
           files: options.inject,
           files: options.inject,
@@ -274,6 +286,15 @@ async function reuseOrCreateTab(source, url, options = {}) {
 
 
     // If dynamic injection needed (VPS panel), re-inject after navigation
     // If dynamic injection needed (VPS panel), re-inject after navigation
     if (options.inject) {
     if (options.inject) {
+      if (options.injectSource) {
+        await chrome.scripting.executeScript({
+          target: { tabId },
+          func: (injectedSource) => {
+            window.__MULTIPAGE_SOURCE = injectedSource;
+          },
+          args: [options.injectSource],
+        });
+      }
       await chrome.scripting.executeScript({
       await chrome.scripting.executeScript({
         target: { tabId },
         target: { tabId },
         files: options.inject,
         files: options.inject,
@@ -303,6 +324,15 @@ async function reuseOrCreateTab(source, url, options = {}) {
       };
       };
       chrome.tabs.onUpdated.addListener(listener);
       chrome.tabs.onUpdated.addListener(listener);
     });
     });
+    if (options.injectSource) {
+      await chrome.scripting.executeScript({
+        target: { tabId: tab.id },
+        func: (injectedSource) => {
+          window.__MULTIPAGE_SOURCE = injectedSource;
+        },
+        args: [options.injectSource],
+      });
+    }
     await chrome.scripting.executeScript({
     await chrome.scripting.executeScript({
       target: { tabId: tab.id },
       target: { tabId: tab.id },
       files: options.inject,
       files: options.inject,
@@ -569,6 +599,7 @@ async function handleMessage(message, sender) {
       if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
       if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
       if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
       if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
       if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
       if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
+      if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
       if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
       if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
       await setState(updates);
       await setState(updates);
       return { ok: true };
       return { ok: true };
@@ -813,6 +844,7 @@ async function autoRunLoop(totalRuns) {
     const keepSettings = {
     const keepSettings = {
       vpsUrl: prevState.vpsUrl,
       vpsUrl: prevState.vpsUrl,
       mailProvider: prevState.mailProvider,
       mailProvider: prevState.mailProvider,
+      inbucketHost: prevState.inbucketHost,
       inbucketMailbox: prevState.inbucketMailbox,
       inbucketMailbox: prevState.inbucketMailbox,
       autoRunning: true,
       autoRunning: true,
     };
     };
@@ -999,20 +1031,40 @@ function getMailConfig(state) {
     return { source: 'mail-163', url: 'https://mail.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 Mail' };
     return { source: 'mail-163', url: 'https://mail.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 Mail' };
   }
   }
   if (provider === 'inbucket') {
   if (provider === 'inbucket') {
+    const host = normalizeInbucketOrigin(state.inbucketHost);
     const mailbox = (state.inbucketMailbox || '').trim();
     const mailbox = (state.inbucketMailbox || '').trim();
+    if (!host) {
+      return { error: 'Inbucket host is empty or invalid.' };
+    }
     if (!mailbox) {
     if (!mailbox) {
       return { error: 'Inbucket mailbox name is empty.' };
       return { error: 'Inbucket mailbox name is empty.' };
     }
     }
     return {
     return {
       source: 'inbucket-mail',
       source: 'inbucket-mail',
-      url: `https://inbucket.j2to.de/m/${encodeURIComponent(mailbox)}/`,
+      url: `${host}/m/${encodeURIComponent(mailbox)}/`,
       label: `Inbucket Mailbox (${mailbox})`,
       label: `Inbucket Mailbox (${mailbox})`,
       navigateOnReuse: true,
       navigateOnReuse: true,
+      inject: ['content/utils.js', 'content/inbucket-mail.js'],
+      injectSource: 'inbucket-mail',
     };
     };
   }
   }
   return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
   return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
 }
 }
 
 
+function normalizeInbucketOrigin(rawValue) {
+  const value = (rawValue || '').trim();
+  if (!value) return '';
+
+  const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
+
+  try {
+    const parsed = new URL(candidate);
+    return parsed.origin;
+  } catch {
+    return '';
+  }
+}
+
 async function executeStep4(state) {
 async function executeStep4(state) {
   const mail = getMailConfig(state);
   const mail = getMailConfig(state);
   if (mail.error) throw new Error(mail.error);
   if (mail.error) throw new Error(mail.error);
@@ -1022,13 +1074,19 @@ async function executeStep4(state) {
   const alive = await isTabAlive(mail.source);
   const alive = await isTabAlive(mail.source);
   if (alive) {
   if (alive) {
     if (mail.navigateOnReuse) {
     if (mail.navigateOnReuse) {
-      await reuseOrCreateTab(mail.source, mail.url);
+      await reuseOrCreateTab(mail.source, mail.url, {
+        inject: mail.inject,
+        injectSource: mail.injectSource,
+      });
     } else {
     } else {
       const tabId = await getTabId(mail.source);
       const tabId = await getTabId(mail.source);
       await chrome.tabs.update(tabId, { active: true });
       await chrome.tabs.update(tabId, { active: true });
     }
     }
   } else {
   } else {
-    await reuseOrCreateTab(mail.source, mail.url);
+    await reuseOrCreateTab(mail.source, mail.url, {
+      inject: mail.inject,
+      injectSource: mail.injectSource,
+    });
   }
   }
 
 
   const result = await sendToContentScript(mail.source, {
   const result = await sendToContentScript(mail.source, {
@@ -1124,13 +1182,19 @@ async function executeStep7(state) {
   const alive = await isTabAlive(mail.source);
   const alive = await isTabAlive(mail.source);
   if (alive) {
   if (alive) {
     if (mail.navigateOnReuse) {
     if (mail.navigateOnReuse) {
-      await reuseOrCreateTab(mail.source, mail.url);
+      await reuseOrCreateTab(mail.source, mail.url, {
+        inject: mail.inject,
+        injectSource: mail.injectSource,
+      });
     } else {
     } else {
       const tabId = await getTabId(mail.source);
       const tabId = await getTabId(mail.source);
       await chrome.tabs.update(tabId, { active: true });
       await chrome.tabs.update(tabId, { active: true });
     }
     }
   } else {
   } else {
-    await reuseOrCreateTab(mail.source, mail.url);
+    await reuseOrCreateTab(mail.source, mail.url, {
+      inject: mail.inject,
+      injectSource: mail.injectSource,
+    });
   }
   }
 
 
   const result = await sendToContentScript(mail.source, {
   const result = await sendToContentScript(mail.source, {

+ 1 - 1
content/inbucket-mail.js

@@ -1,5 +1,5 @@
 // content/inbucket-mail.js — Content script for Inbucket polling (steps 4, 7)
 // content/inbucket-mail.js — Content script for Inbucket polling (steps 4, 7)
-// Injected on: inbucket.j2to.de
+// Injected dynamically on the configured Inbucket host
 //
 //
 // Supported page:
 // Supported page:
 // - /m/<mailbox>/
 // - /m/<mailbox>/

+ 1 - 1
content/utils.js

@@ -1,11 +1,11 @@
 // content/utils.js — Shared utilities for all content scripts
 // content/utils.js — Shared utilities for all content scripts
 
 
 const SCRIPT_SOURCE = (() => {
 const SCRIPT_SOURCE = (() => {
+  if (window.__MULTIPAGE_SOURCE) return window.__MULTIPAGE_SOURCE;
   const url = location.href;
   const url = location.href;
   if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
   if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
   if (url.includes('mail.qq.com')) return 'qq-mail';
   if (url.includes('mail.qq.com')) return 'qq-mail';
   if (url.includes('mail.163.com')) return 'mail-163';
   if (url.includes('mail.163.com')) return 'mail-163';
-  if (url.includes('inbucket.j2to.de')) return 'inbucket-mail';
   if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
   if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
   if (url.includes('chatgpt.com')) return 'chatgpt';
   if (url.includes('chatgpt.com')) return 'chatgpt';
   // VPS panel — detected dynamically since URL is configurable
   // VPS panel — detected dynamically since URL is configurable

+ 0 - 8
manifest.json

@@ -54,14 +54,6 @@
       ],
       ],
       "js": ["content/utils.js", "content/duck-mail.js"],
       "js": ["content/utils.js", "content/duck-mail.js"],
       "run_at": "document_idle"
       "run_at": "document_idle"
-    },
-    {
-      "matches": [
-        "https://inbucket.j2to.de/*"
-      ],
-      "js": ["content/utils.js", "content/inbucket-mail.js"],
-      "all_frames": true,
-      "run_at": "document_idle"
     }
     }
   ],
   ],
   "action": {
   "action": {

+ 5 - 1
sidepanel/sidepanel.html

@@ -53,9 +53,13 @@
         <select id="select-mail-provider" class="data-select">
         <select id="select-mail-provider" class="data-select">
           <option value="163">163 Mail (mail.163.com)</option>
           <option value="163">163 Mail (mail.163.com)</option>
           <option value="qq">QQ Mail (wx.mail.qq.com)</option>
           <option value="qq">QQ Mail (wx.mail.qq.com)</option>
-          <option value="inbucket">Inbucket (inbucket.j2to.de/m/&lt;mailbox&gt;/)</option>
+          <option value="inbucket">Inbucket (custom host)</option>
         </select>
         </select>
       </div>
       </div>
+      <div class="data-row" id="row-inbucket-host" style="display:none;">
+        <span class="data-label">Inbucket</span>
+        <input type="text" id="input-inbucket-host" class="data-input" placeholder="your-inbucket-host or https://your-inbucket-host" />
+      </div>
       <div class="data-row" id="row-inbucket-mailbox" style="display:none;">
       <div class="data-row" id="row-inbucket-mailbox" style="display:none;">
         <span class="data-label">Mailbox</span>
         <span class="data-label">Mailbox</span>
         <input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="e.g. zju2001" />
         <input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="e.g. zju2001" />

+ 14 - 0
sidepanel/sidepanel.js

@@ -26,6 +26,8 @@ const autoContinueBar = document.getElementById('auto-continue-bar');
 const btnClearLog = document.getElementById('btn-clear-log');
 const btnClearLog = document.getElementById('btn-clear-log');
 const inputVpsUrl = document.getElementById('input-vps-url');
 const inputVpsUrl = document.getElementById('input-vps-url');
 const selectMailProvider = document.getElementById('select-mail-provider');
 const selectMailProvider = document.getElementById('select-mail-provider');
+const rowInbucketHost = document.getElementById('row-inbucket-host');
+const inputInbucketHost = document.getElementById('input-inbucket-host');
 const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
 const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
 const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
 const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
 const inputRunCount = document.getElementById('input-run-count');
 const inputRunCount = document.getElementById('input-run-count');
@@ -88,6 +90,9 @@ async function restoreState() {
     if (state.mailProvider) {
     if (state.mailProvider) {
       selectMailProvider.value = state.mailProvider;
       selectMailProvider.value = state.mailProvider;
     }
     }
+    if (state.inbucketHost) {
+      inputInbucketHost.value = state.inbucketHost;
+    }
     if (state.inbucketMailbox) {
     if (state.inbucketMailbox) {
       inputInbucketMailbox.value = state.inbucketMailbox;
       inputInbucketMailbox.value = state.inbucketMailbox;
     }
     }
@@ -118,6 +123,7 @@ function syncPasswordField(state) {
 
 
 function updateMailProviderUI() {
 function updateMailProviderUI() {
   const useInbucket = selectMailProvider.value === 'inbucket';
   const useInbucket = selectMailProvider.value === 'inbucket';
+  rowInbucketHost.style.display = useInbucket ? '' : 'none';
   rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
   rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
 }
 }
 
 
@@ -406,6 +412,14 @@ inputInbucketMailbox.addEventListener('change', async () => {
   });
   });
 });
 });
 
 
+inputInbucketHost.addEventListener('change', async () => {
+  await chrome.runtime.sendMessage({
+    type: 'SAVE_SETTING',
+    source: 'sidepanel',
+    payload: { inbucketHost: inputInbucketHost.value.trim() },
+  });
+});
+
 // ============================================================
 // ============================================================
 // Listen for Background broadcasts
 // Listen for Background broadcasts
 // ============================================================
 // ============================================================