diff --git a/.github/workflows/temp-cloudmail-issue-trigger.yml b/.github/workflows/temp-cloudmail-issue-trigger.yml deleted file mode 100644 index 58e2731..0000000 --- a/.github/workflows/temp-cloudmail-issue-trigger.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Temporary Cloud Mail issue trigger - -on: - issues: - types: - - edited - schedule: - - cron: "*/5 * * * *" - -permissions: - contents: write - -jobs: - apply-upgrade: - if: github.event_name == 'schedule' || (github.event_name == 'issues' && github.event.issue.number == 7) - runs-on: ubuntu-latest - steps: - - name: Checkout main - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - - name: Apply Cloud Mail upgrade - run: python tools/apply_cloudmail_upgrade.py - - - name: Commit upgrade and clean temporary files - shell: bash - run: | - rm -f tools/apply_cloudmail_upgrade.py - rm -f .github/workflows/temp-cloudmail-upgrade.yml - rm -f .github/workflows/temp-cloudmail-issue-trigger.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add grok_register_ttk.py config.example.json README.md tools/apply_cloudmail_upgrade.py .github/workflows/temp-cloudmail-upgrade.yml .github/workflows/temp-cloudmail-issue-trigger.yml - git commit -m "feat(mail): add optional Cloud Mail no-recipient provider" - git push origin HEAD:main diff --git a/.github/workflows/temp-cloudmail-upgrade.yml b/.github/workflows/temp-cloudmail-upgrade.yml deleted file mode 100644 index 60d7e5c..0000000 --- a/.github/workflows/temp-cloudmail-upgrade.yml +++ /dev/null @@ -1,53 +0,0 @@ -# One-shot workflow to finish the Cloud Mail upgrade and remove temporary files. -name: Temporary Cloud Mail upgrade - -on: - push: - branches: - - main - paths: - - .github/workflows/temp-cloudmail-upgrade.yml - -permissions: - contents: write - -jobs: - apply-upgrade: - runs-on: ubuntu-latest - steps: - - name: Checkout main - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - - name: Fix Cloud Mail patch generator escaping - shell: bash - run: | - python <<'PY' - from pathlib import Path - - path = Path("tools/apply_cloudmail_upgrade.py") - text = path.read_text(encoding="utf-8") - old = ' combined = "\\n".join(parts)' - new = ' combined = "\\\\n".join(parts)' - count = text.count(old) - if count != 1: - raise RuntimeError(f"newline escape fix: expected exactly one match, found {count}") - path.write_text(text.replace(old, new, 1), encoding="utf-8") - PY - - - name: Apply Cloud Mail provider patch - run: python tools/apply_cloudmail_upgrade.py - - - name: Commit upgrade and clean temporary files - shell: bash - run: | - rm -f tools/apply_cloudmail_upgrade.py - rm -f .github/workflows/temp-cloudmail-upgrade.yml - rm -f .github/workflows/temp-cloudmail-issue-trigger.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add grok_register_ttk.py config.example.json README.md tools/apply_cloudmail_upgrade.py .github/workflows/temp-cloudmail-upgrade.yml .github/workflows/temp-cloudmail-issue-trigger.yml - git commit -m "feat(mail): add optional Cloud Mail no-recipient provider" - git push origin HEAD:main diff --git a/README.md b/README.md index 7354fb4..9104d01 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Grok Register 是一个面向自动化流程研究、测试环境验证和个人 - 支持 GUI 图形界面运行。 - 支持 CLI 终端运行,不启动 Tk GUI。 - 注册流程使用 Chromium/Chrome 浏览器页面完成。 -- 支持 DuckMail、YYDS、Cloudflare 临时邮箱接口。 +- 支持 DuckMail、YYDS、Cloudflare,以及 Cloud Mail 无人收件模式。 - 支持验证码邮件轮询和解析。 - 支持成功账号实时写入 `accounts_*.txt`。 - 支持将 SSO token 写入 grok2api 本地或远端池。 @@ -91,7 +91,7 @@ cp config.example.json config.json | 配置项 | 说明 | | --- | --- | -| `email_provider` | 邮箱服务商:`duckmail`、`yyds`、`cloudflare` | +| `email_provider` | 邮箱服务商:`duckmail`、`yyds`、`cloudflare`、`cloudmail` | | `register_count` | 本次目标注册数量 | | `proxy` | 代理地址,可留空 | | `enable_nsfw` | 注册后是否尝试开启 NSFW | @@ -103,6 +103,10 @@ cp config.example.json config.json | `cloudflare_path_token` | Cloudflare token 路径;默认 `/api/token` | | `cloudflare_path_messages` | Cloudflare 收件列表路径;默认 `/api/mails` | | `defaultDomains` | Cloudflare 临时邮箱默认域名 | +| `cloudmail_api_base` | Cloud Mail 站点地址 | +| `cloudmail_public_token` | Cloud Mail 公共 API Token,不会写入邮箱凭证文件 | +| `cloudmail_domains` | Cloud Mail 无人收件域名,多个域名用英文逗号分隔 | +| `cloudmail_path_messages` | Cloud Mail 公共收件接口路径,默认 `/api/public/emailList` | | `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 | | `grok2api_local_token_file` | 本地 grok2api token 文件路径 | | `grok2api_auto_add_remote` | 是否写入远端 grok2api | @@ -158,6 +162,35 @@ cp config.example.json config.json python cf_mail_debug.py --api-base "https://你的-worker-api-域名" --auth-mode x-admin-auth --api-key "你的 ADMIN_PASSWORD" --create-path /admin/new_address --domain "你的收信域名.com" ``` +### Cloud Mail 无人收件模式(可选) + +该模式对接 `maillab/cloud-mail`,使用其“无人收件”功能:程序直接生成随机邮箱地址,不需要先在 Cloud Mail 中创建邮箱账号。 + +使用前需要: + +1. 在 Cloud Mail 系统设置中开启“无人收件”。 +2. 使用管理员账号生成公共 API Token: + +```bash +curl -X POST "https://你的-Cloud-Mail-域名/api/public/genToken" \ + -H "Content-Type: application/json" \ + -d '{"email":"管理员邮箱","password":"管理员密码"}' +``` + +3. 配置本项目: + +```json +{ + "email_provider": "cloudmail", + "cloudmail_api_base": "https://你的-Cloud-Mail-域名", + "cloudmail_public_token": "返回结果 data 中的 token", + "cloudmail_domains": "你的收信域名.com", + "cloudmail_path_messages": "/api/public/emailList" +} +``` + +程序会通过 `POST /api/public/emailList` 按目标地址查询邮件。公共 Token 只从 `config.json` 读取,不会作为邮箱 credential 输出到日志或 `mail_credentials.txt`。 + ### grok2api 远端入池配置 如果开启 `grok2api_auto_add_remote`,`grok2api_remote_base` 可以填写站点根地址,也可以直接填写管理 API 地址: diff --git a/config.example.json b/config.example.json index aea6078..956e22b 100755 --- a/config.example.json +++ b/config.example.json @@ -7,6 +7,10 @@ "cloudflare_path_accounts": "/api/new_address", "cloudflare_path_token": "/api/token", "cloudflare_path_messages": "/api/mails", + "cloudmail_api_base": "https://mail.example.com", + "cloudmail_public_token": "", + "cloudmail_domains": "example.com", + "cloudmail_path_messages": "/api/public/emailList", "proxy": "", "enable_nsfw": true, "register_count": 1, diff --git a/grok_register_ttk.py b/grok_register_ttk.py index 91a7a32..ef2b2ae 100755 --- a/grok_register_ttk.py +++ b/grok_register_ttk.py @@ -54,6 +54,10 @@ DEFAULT_CONFIG = { "cloudflare_path_accounts": "/api/new_address", "cloudflare_path_token": "/api/token", "cloudflare_path_messages": "/api/mails", + "cloudmail_api_base": "", + "cloudmail_public_token": "", + "cloudmail_domains": "", + "cloudmail_path_messages": "/api/public/emailList", "proxy": "http://127.0.0.1:7890", "enable_nsfw": True, "register_count": 1, @@ -68,6 +72,7 @@ DEFAULT_CONFIG = { config = DEFAULT_CONFIG.copy() _cf_domain_index = 0 +_cloudmail_domain_index = 0 class RegistrationCancelled(Exception): @@ -519,6 +524,93 @@ def cloudflare_create_temp_address(api_base): return address, jwt +def get_cloudmail_api_base(): + return str(config.get("cloudmail_api_base", "") or "").strip().rstrip("/") + + +def get_cloudmail_public_token(): + return str(config.get("cloudmail_public_token", "") or "").strip() + + +def get_cloudmail_path(): + raw = str( + config.get("cloudmail_path_messages", "/api/public/emailList") + or "/api/public/emailList" + ).strip() + return raw if raw.startswith("/") else "/" + raw + + +def cloudmail_next_domain(): + """按配置轮换选择 Cloud Mail 无人收件域名。""" + global _cloudmail_domain_index + domains = [ + item.strip().lstrip("@") + for item in str(config.get("cloudmail_domains", "") or "").split(",") + if item.strip().lstrip("@") + ] + if not domains: + return "" + domain = domains[_cloudmail_domain_index % len(domains)] + _cloudmail_domain_index += 1 + return domain + + +def cloudmail_get_email_and_token(): + """生成无需预创建账号的 Cloud Mail 收件地址。""" + if not get_cloudmail_api_base(): + raise Exception("Cloud Mail API Base 未配置") + if not get_cloudmail_public_token(): + raise Exception("Cloud Mail Public Token 未配置") + domain = cloudmail_next_domain() + if not domain: + raise Exception("Cloud Mail 收件域名未配置") + address = f"{generate_username(12)}@{domain}" + # 仅返回非敏感占位凭证;公共 Token 始终只从 config.json 读取。 + return address, f"cloudmail:{address}" + + +def cloudmail_get_messages(address): + api_base = get_cloudmail_api_base() + public_token = get_cloudmail_public_token() + if not api_base: + raise Exception("Cloud Mail API Base 未配置") + if not public_token: + raise Exception("Cloud Mail Public Token 未配置") + payload = { + "toEmail": address, + "type": 0, + "isDel": 0, + "timeSort": "desc", + "num": 1, + "size": 20, + } + resp = http_post( + f"{api_base}{get_cloudmail_path()}", + headers={ + "Authorization": public_token, + "Content-Type": "application/json", + }, + json=payload, + timeout=20, + ) + resp.raise_for_status() + try: + data = resp.json() + except Exception: + raise Exception(f"Cloud Mail 邮件接口返回非JSON: {resp.text[:300]}") + if not isinstance(data, dict): + raise Exception(f"Cloud Mail 邮件接口返回格式错误: {data}") + result_code = data.get("code") + if result_code not in (None, 200, "200"): + raise Exception( + f"Cloud Mail 邮件接口失败: code={result_code}, message={data.get('message', '')}" + ) + messages = data.get("data") + if isinstance(messages, list): + return messages + return _pick_list_payload(data) + + def get_user_agent(): return config.get( "user_agent", @@ -1139,6 +1231,8 @@ def get_email_and_token(api_key=None): provider = get_email_provider() if provider == "yyds": return yyds_get_email_and_token(api_key=api_key, jwt=get_yyds_jwt()) + if provider == "cloudmail": + return cloudmail_get_email_and_token() if provider == "cloudflare": api_base = get_cloudflare_api_base() if not api_base: @@ -1199,6 +1293,16 @@ def get_oai_code( jwt=get_yyds_jwt(), cancel_callback=cancel_callback, ) + if provider == "cloudmail": + return cloudmail_get_oai_code( + dev_token, + email, + timeout=timeout, + poll_interval=poll_interval, + log_callback=log_callback, + cancel_callback=cancel_callback, + resend_callback=resend_callback, + ) if provider == "cloudflare": return cloudflare_get_oai_code( dev_token, @@ -1239,6 +1343,81 @@ def extract_verification_code(text, subject=""): return None +def cloudmail_get_oai_code( + dev_token, + email, + timeout=180, + poll_interval=3, + log_callback=None, + cancel_callback=None, + resend_callback=None, +): + # dev_token 是为了保持现有邮箱 Provider 调用契约;Cloud Mail 使用配置中的公共 Token。 + _ = dev_token + deadline = time.time() + timeout + seen_attempts = {} + next_resend_at = time.time() + 35 + while time.time() < deadline: + raise_if_cancelled(cancel_callback) + if resend_callback and time.time() >= next_resend_at: + try: + resend_callback() + if log_callback: + log_callback("[*] 已触发重新发送验证码") + except Exception as exc: + if log_callback: + log_callback(f"[Debug] 触发重发验证码失败: {exc}") + next_resend_at = time.time() + 35 + try: + messages = cloudmail_get_messages(email) + except Exception as exc: + if log_callback: + log_callback(f"[Debug] Cloud Mail 拉取邮件列表失败: {exc}") + sleep_with_cancel(poll_interval, cancel_callback) + continue + if log_callback: + log_callback(f"[Debug] Cloud Mail 本轮邮件数量: {len(messages)}") + for msg in messages: + msg_id = msg.get("emailId") or msg.get("email_id") or msg.get("id") + if not msg_id: + continue + attempt = int(seen_attempts.get(msg_id, 0)) + if attempt >= 5: + continue + seen_attempts[msg_id] = attempt + 1 + target_address = str( + msg.get("toEmail") or msg.get("to_email") or "" + ).strip().lower() + if target_address and target_address != email.lower(): + continue + parts = [] + code_value = str(msg.get("code", "") or "").strip() + if code_value: + parts.append(f"verification code: {code_value}") + for field in ("text", "content", "html", "body", "snippet"): + value = msg.get(field) + values = value if isinstance(value, list) else [value] + for item in values: + if isinstance(item, str) and item.strip(): + parts.append(re.sub(r"<[^>]+>", " ", item)) + subject = str(msg.get("subject", "") or "") + combined = "\n".join(parts) + if log_callback: + log_callback(f"[Debug] Cloud Mail 收到邮件: {subject}") + code = extract_verification_code(combined, subject) + if code: + if log_callback: + log_callback(f"[*] Cloud Mail 从邮件中提取到验证码: {code}") + return code + if log_callback: + log_callback( + f"[Debug] Cloud Mail 邮件已解析但未提取到验证码 " + f"id={msg_id} attempt={seen_attempts[msg_id]}" + ) + sleep_with_cancel(poll_interval, cancel_callback) + raise Exception(f"Cloud Mail 在 {timeout}s 内未收到验证码邮件") + + def duckmail_get_oai_code( dev_token, email, @@ -2826,7 +3005,7 @@ class GrokRegisterGUI: add_label(0, 0, "邮箱服务商:") self.email_provider_var = tk.StringVar(value=config.get("email_provider", "duckmail")) - self.email_provider_combo = tk_option_menu(config_frame, self.email_provider_var, ["duckmail", "yyds", "cloudflare"], width=12) + self.email_provider_combo = tk_option_menu(config_frame, self.email_provider_var, ["duckmail", "yyds", "cloudflare", "cloudmail"], width=12) add_field(self.email_provider_combo, 0, 1, sticky=tk.W) add_label(0, 2, "注册数量:") @@ -2893,37 +3072,52 @@ class GrokRegisterGUI: self.cloudflare_paths_entry = tk_entry(config_frame, textvariable=self.cloudflare_paths_var, width=34) add_field(self.cloudflare_paths_entry, 4, 3) - add_label(5, 0, "grok2api 本地入池:") + add_label(5, 0, "Cloud Mail API Base:") + self.cloudmail_api_base_var = tk.StringVar(value=config.get("cloudmail_api_base", "")) + self.cloudmail_api_base_entry = tk_entry(config_frame, textvariable=self.cloudmail_api_base_var, width=34) + add_field(self.cloudmail_api_base_entry, 5, 1) + + add_label(5, 2, "Cloud Mail 域名:") + self.cloudmail_domains_var = tk.StringVar(value=config.get("cloudmail_domains", "")) + self.cloudmail_domains_entry = tk_entry(config_frame, textvariable=self.cloudmail_domains_var, width=34) + add_field(self.cloudmail_domains_entry, 5, 3) + + add_label(6, 0, "Cloud Mail Public Token:") + self.cloudmail_public_token_var = tk.StringVar(value=config.get("cloudmail_public_token", "")) + self.cloudmail_public_token_entry = tk_entry(config_frame, textvariable=self.cloudmail_public_token_var, width=72) + add_field(self.cloudmail_public_token_entry, 6, 1, columnspan=3) + + add_label(7, 0, "grok2api 本地入池:") self.grok2api_local_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_local", True))) self.grok2api_local_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_local_auto_var) - add_field(self.grok2api_local_auto_check, 5, 1, sticky=tk.W) + add_field(self.grok2api_local_auto_check, 7, 1, sticky=tk.W) - add_label(5, 2, "grok2api 池名:") + add_label(7, 2, "grok2api 池名:") self.grok2api_pool_name_var = tk.StringVar(value=str(config.get("grok2api_pool_name", "ssoBasic"))) self.grok2api_pool_name_combo = tk_option_menu( config_frame, self.grok2api_pool_name_var, ["ssoBasic", "ssoSuper"], width=12 ) - add_field(self.grok2api_pool_name_combo, 5, 3, sticky=tk.W) + add_field(self.grok2api_pool_name_combo, 7, 3, sticky=tk.W) - add_label(6, 0, "本地 token.json:") + add_label(8, 0, "本地 token.json:") self.grok2api_local_file_var = tk.StringVar(value=str(config.get("grok2api_local_token_file", ""))) self.grok2api_local_file_entry = tk_entry(config_frame, textvariable=self.grok2api_local_file_var, width=72) - add_field(self.grok2api_local_file_entry, 6, 1, columnspan=3) + add_field(self.grok2api_local_file_entry, 8, 1, columnspan=3) - add_label(7, 0, "grok2api 远端入池:") + add_label(9, 0, "grok2api 远端入池:") self.grok2api_remote_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_remote", False))) self.grok2api_remote_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_remote_auto_var) - add_field(self.grok2api_remote_auto_check, 7, 1, sticky=tk.W) + add_field(self.grok2api_remote_auto_check, 9, 1, sticky=tk.W) - add_label(8, 0, "grok2api 远端 Base:") + add_label(10, 0, "grok2api 远端 Base:") self.grok2api_remote_base_var = tk.StringVar(value=str(config.get("grok2api_remote_base", ""))) self.grok2api_remote_base_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_base_var, width=72) - add_field(self.grok2api_remote_base_entry, 8, 1, columnspan=3) + add_field(self.grok2api_remote_base_entry, 10, 1, columnspan=3) - add_label(9, 0, "grok2api 远端 app_key:") + add_label(11, 0, "grok2api 远端 app_key:") self.grok2api_remote_key_var = tk.StringVar(value=str(config.get("grok2api_remote_app_key", ""))) self.grok2api_remote_key_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_key_var, width=72) - add_field(self.grok2api_remote_key_entry, 9, 1, columnspan=3) + add_field(self.grok2api_remote_key_entry, 11, 1, columnspan=3) btn_frame = tk.Frame(main_frame, bg=UI_BG) btn_frame.grid(row=1, column=0, sticky=tk.EW, pady=(0, 6)) @@ -3008,6 +3202,9 @@ class GrokRegisterGUI: config["cloudflare_api_base"] = self.cloudflare_api_base_var.get().strip() config["cloudflare_api_key"] = self.cloudflare_api_key_var.get().strip() config["cloudflare_auth_mode"] = self.cloudflare_auth_mode_var.get().strip() or "none" + config["cloudmail_api_base"] = self.cloudmail_api_base_var.get().strip() + config["cloudmail_public_token"] = self.cloudmail_public_token_var.get().strip() + config["cloudmail_domains"] = self.cloudmail_domains_var.get().strip() config["grok2api_auto_add_local"] = bool(self.grok2api_local_auto_var.get()) config["grok2api_local_token_file"] = self.grok2api_local_file_var.get().strip() config["grok2api_pool_name"] = self.grok2api_pool_name_var.get().strip() or "ssoBasic" @@ -3024,6 +3221,17 @@ class GrokRegisterGUI: if config["email_provider"] == "cloudflare" and not config["cloudflare_api_base"]: self.log("[!] Cloudflare 模式需要先填写 Cloudflare API Base") return + if config["email_provider"] == "cloudmail": + missing = [] + if not config["cloudmail_api_base"]: + missing.append("API Base") + if not config["cloudmail_public_token"]: + missing.append("Public Token") + if not config["cloudmail_domains"]: + missing.append("域名") + if missing: + self.log(f"[!] Cloud Mail 模式缺少配置: {', '.join(missing)}") + return try: count = int(self.count_var.get()) except Exception: diff --git a/tools/apply_cloudmail_upgrade.py b/tools/apply_cloudmail_upgrade.py deleted file mode 100644 index 3fdbb91..0000000 --- a/tools/apply_cloudmail_upgrade.py +++ /dev/null @@ -1,457 +0,0 @@ -#!/usr/bin/env python3 -from pathlib import Path -import ast -import json - - -def replace_once(text, old, new, label): - count = text.count(old) - if count != 1: - raise RuntimeError(f"{label}: expected exactly one match, found {count}") - return text.replace(old, new, 1) - - -app_path = Path("grok_register_ttk.py") -app = app_path.read_text(encoding="utf-8-sig") - -app = replace_once( - app, - ' "cloudflare_path_messages": "/api/mails",\n "proxy": "http://127.0.0.1:7890",', - ' "cloudflare_path_messages": "/api/mails",\n' - ' "cloudmail_api_base": "",\n' - ' "cloudmail_public_token": "",\n' - ' "cloudmail_domains": "",\n' - ' "cloudmail_path_messages": "/api/public/emailList",\n' - ' "proxy": "http://127.0.0.1:7890",', - "default Cloud Mail config", -) - -app = replace_once( - app, - 'config = DEFAULT_CONFIG.copy()\n_cf_domain_index = 0\n', - 'config = DEFAULT_CONFIG.copy()\n_cf_domain_index = 0\n_cloudmail_domain_index = 0\n', - "Cloud Mail domain index", -) - -cloudmail_helpers = '''def get_cloudmail_api_base(): - return str(config.get("cloudmail_api_base", "") or "").strip().rstrip("/") - - -def get_cloudmail_public_token(): - return str(config.get("cloudmail_public_token", "") or "").strip() - - -def get_cloudmail_path(): - raw = str( - config.get("cloudmail_path_messages", "/api/public/emailList") - or "/api/public/emailList" - ).strip() - return raw if raw.startswith("/") else "/" + raw - - -def cloudmail_next_domain(): - """按配置轮换选择 Cloud Mail 无人收件域名。""" - global _cloudmail_domain_index - domains = [ - item.strip().lstrip("@") - for item in str(config.get("cloudmail_domains", "") or "").split(",") - if item.strip().lstrip("@") - ] - if not domains: - return "" - domain = domains[_cloudmail_domain_index % len(domains)] - _cloudmail_domain_index += 1 - return domain - - -def cloudmail_get_email_and_token(): - """生成无需预创建账号的 Cloud Mail 收件地址。""" - if not get_cloudmail_api_base(): - raise Exception("Cloud Mail API Base 未配置") - if not get_cloudmail_public_token(): - raise Exception("Cloud Mail Public Token 未配置") - domain = cloudmail_next_domain() - if not domain: - raise Exception("Cloud Mail 收件域名未配置") - address = f"{generate_username(12)}@{domain}" - # 仅返回非敏感占位凭证;公共 Token 始终只从 config.json 读取。 - return address, f"cloudmail:{address}" - - -def cloudmail_get_messages(address): - api_base = get_cloudmail_api_base() - public_token = get_cloudmail_public_token() - if not api_base: - raise Exception("Cloud Mail API Base 未配置") - if not public_token: - raise Exception("Cloud Mail Public Token 未配置") - payload = { - "toEmail": address, - "type": 0, - "isDel": 0, - "timeSort": "desc", - "num": 1, - "size": 20, - } - resp = http_post( - f"{api_base}{get_cloudmail_path()}", - headers={ - "Authorization": public_token, - "Content-Type": "application/json", - }, - json=payload, - timeout=20, - ) - resp.raise_for_status() - try: - data = resp.json() - except Exception: - raise Exception(f"Cloud Mail 邮件接口返回非JSON: {resp.text[:300]}") - if not isinstance(data, dict): - raise Exception(f"Cloud Mail 邮件接口返回格式错误: {data}") - result_code = data.get("code") - if result_code not in (None, 200, "200"): - raise Exception( - f"Cloud Mail 邮件接口失败: code={result_code}, message={data.get('message', '')}" - ) - messages = data.get("data") - if isinstance(messages, list): - return messages - return _pick_list_payload(data) - - -''' - -app = replace_once( - app, - ' return address, jwt\n\n\ndef get_user_agent():', - ' return address, jwt\n\n\n' + cloudmail_helpers + 'def get_user_agent():', - "Cloud Mail helper functions", -) - -app = replace_once( - app, - ' if provider == "yyds":\n' - ' return yyds_get_email_and_token(api_key=api_key, jwt=get_yyds_jwt())\n' - ' if provider == "cloudflare":', - ' if provider == "yyds":\n' - ' return yyds_get_email_and_token(api_key=api_key, jwt=get_yyds_jwt())\n' - ' if provider == "cloudmail":\n' - ' return cloudmail_get_email_and_token()\n' - ' if provider == "cloudflare":', - "Cloud Mail address dispatch", -) - -app = replace_once( - app, - ' if provider == "cloudflare":\n' - ' return cloudflare_get_oai_code(\n', - ' if provider == "cloudmail":\n' - ' return cloudmail_get_oai_code(\n' - ' dev_token,\n' - ' email,\n' - ' timeout=timeout,\n' - ' poll_interval=poll_interval,\n' - ' log_callback=log_callback,\n' - ' cancel_callback=cancel_callback,\n' - ' resend_callback=resend_callback,\n' - ' )\n' - ' if provider == "cloudflare":\n' - ' return cloudflare_get_oai_code(\n', - "Cloud Mail verification dispatch", -) - -cloudmail_poll = '''def cloudmail_get_oai_code( - dev_token, - email, - timeout=180, - poll_interval=3, - log_callback=None, - cancel_callback=None, - resend_callback=None, -): - # dev_token 是为了保持现有邮箱 Provider 调用契约;Cloud Mail 使用配置中的公共 Token。 - _ = dev_token - deadline = time.time() + timeout - seen_attempts = {} - next_resend_at = time.time() + 35 - while time.time() < deadline: - raise_if_cancelled(cancel_callback) - if resend_callback and time.time() >= next_resend_at: - try: - resend_callback() - if log_callback: - log_callback("[*] 已触发重新发送验证码") - except Exception as exc: - if log_callback: - log_callback(f"[Debug] 触发重发验证码失败: {exc}") - next_resend_at = time.time() + 35 - try: - messages = cloudmail_get_messages(email) - except Exception as exc: - if log_callback: - log_callback(f"[Debug] Cloud Mail 拉取邮件列表失败: {exc}") - sleep_with_cancel(poll_interval, cancel_callback) - continue - if log_callback: - log_callback(f"[Debug] Cloud Mail 本轮邮件数量: {len(messages)}") - for msg in messages: - msg_id = msg.get("emailId") or msg.get("email_id") or msg.get("id") - if not msg_id: - continue - attempt = int(seen_attempts.get(msg_id, 0)) - if attempt >= 5: - continue - seen_attempts[msg_id] = attempt + 1 - target_address = str( - msg.get("toEmail") or msg.get("to_email") or "" - ).strip().lower() - if target_address and target_address != email.lower(): - continue - parts = [] - code_value = str(msg.get("code", "") or "").strip() - if code_value: - parts.append(f"verification code: {code_value}") - for field in ("text", "content", "html", "body", "snippet"): - value = msg.get(field) - values = value if isinstance(value, list) else [value] - for item in values: - if isinstance(item, str) and item.strip(): - parts.append(re.sub(r"<[^>]+>", " ", item)) - subject = str(msg.get("subject", "") or "") - combined = "\n".join(parts) - if log_callback: - log_callback(f"[Debug] Cloud Mail 收到邮件: {subject}") - code = extract_verification_code(combined, subject) - if code: - if log_callback: - log_callback(f"[*] Cloud Mail 从邮件中提取到验证码: {code}") - return code - if log_callback: - log_callback( - f"[Debug] Cloud Mail 邮件已解析但未提取到验证码 " - f"id={msg_id} attempt={seen_attempts[msg_id]}" - ) - sleep_with_cancel(poll_interval, cancel_callback) - raise Exception(f"Cloud Mail 在 {timeout}s 内未收到验证码邮件") - - -''' - -app = replace_once( - app, - ' return None\n\n\ndef duckmail_get_oai_code(', - ' return None\n\n\n' + cloudmail_poll + 'def duckmail_get_oai_code(', - "Cloud Mail verification polling", -) - -app = replace_once( - app, - 'self.email_provider_var, ["duckmail", "yyds", "cloudflare"], width=12', - 'self.email_provider_var, ["duckmail", "yyds", "cloudflare", "cloudmail"], width=12', - "Cloud Mail GUI provider option", -) - -old_gui_block = ''' add_label(5, 0, "grok2api 本地入池:") - self.grok2api_local_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_local", True))) - self.grok2api_local_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_local_auto_var) - add_field(self.grok2api_local_auto_check, 5, 1, sticky=tk.W) - - add_label(5, 2, "grok2api 池名:") - self.grok2api_pool_name_var = tk.StringVar(value=str(config.get("grok2api_pool_name", "ssoBasic"))) - self.grok2api_pool_name_combo = tk_option_menu( - config_frame, self.grok2api_pool_name_var, ["ssoBasic", "ssoSuper"], width=12 - ) - add_field(self.grok2api_pool_name_combo, 5, 3, sticky=tk.W) - - add_label(6, 0, "本地 token.json:") - self.grok2api_local_file_var = tk.StringVar(value=str(config.get("grok2api_local_token_file", ""))) - self.grok2api_local_file_entry = tk_entry(config_frame, textvariable=self.grok2api_local_file_var, width=72) - add_field(self.grok2api_local_file_entry, 6, 1, columnspan=3) - - add_label(7, 0, "grok2api 远端入池:") - self.grok2api_remote_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_remote", False))) - self.grok2api_remote_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_remote_auto_var) - add_field(self.grok2api_remote_auto_check, 7, 1, sticky=tk.W) - - add_label(8, 0, "grok2api 远端 Base:") - self.grok2api_remote_base_var = tk.StringVar(value=str(config.get("grok2api_remote_base", ""))) - self.grok2api_remote_base_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_base_var, width=72) - add_field(self.grok2api_remote_base_entry, 8, 1, columnspan=3) - - add_label(9, 0, "grok2api 远端 app_key:") - self.grok2api_remote_key_var = tk.StringVar(value=str(config.get("grok2api_remote_app_key", ""))) - self.grok2api_remote_key_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_key_var, width=72) - add_field(self.grok2api_remote_key_entry, 9, 1, columnspan=3) -''' - -new_gui_block = ''' add_label(5, 0, "Cloud Mail API Base:") - self.cloudmail_api_base_var = tk.StringVar(value=config.get("cloudmail_api_base", "")) - self.cloudmail_api_base_entry = tk_entry(config_frame, textvariable=self.cloudmail_api_base_var, width=34) - add_field(self.cloudmail_api_base_entry, 5, 1) - - add_label(5, 2, "Cloud Mail 域名:") - self.cloudmail_domains_var = tk.StringVar(value=config.get("cloudmail_domains", "")) - self.cloudmail_domains_entry = tk_entry(config_frame, textvariable=self.cloudmail_domains_var, width=34) - add_field(self.cloudmail_domains_entry, 5, 3) - - add_label(6, 0, "Cloud Mail Public Token:") - self.cloudmail_public_token_var = tk.StringVar(value=config.get("cloudmail_public_token", "")) - self.cloudmail_public_token_entry = tk_entry(config_frame, textvariable=self.cloudmail_public_token_var, width=72) - add_field(self.cloudmail_public_token_entry, 6, 1, columnspan=3) - - add_label(7, 0, "grok2api 本地入池:") - self.grok2api_local_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_local", True))) - self.grok2api_local_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_local_auto_var) - add_field(self.grok2api_local_auto_check, 7, 1, sticky=tk.W) - - add_label(7, 2, "grok2api 池名:") - self.grok2api_pool_name_var = tk.StringVar(value=str(config.get("grok2api_pool_name", "ssoBasic"))) - self.grok2api_pool_name_combo = tk_option_menu( - config_frame, self.grok2api_pool_name_var, ["ssoBasic", "ssoSuper"], width=12 - ) - add_field(self.grok2api_pool_name_combo, 7, 3, sticky=tk.W) - - add_label(8, 0, "本地 token.json:") - self.grok2api_local_file_var = tk.StringVar(value=str(config.get("grok2api_local_token_file", ""))) - self.grok2api_local_file_entry = tk_entry(config_frame, textvariable=self.grok2api_local_file_var, width=72) - add_field(self.grok2api_local_file_entry, 8, 1, columnspan=3) - - add_label(9, 0, "grok2api 远端入池:") - self.grok2api_remote_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_remote", False))) - self.grok2api_remote_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_remote_auto_var) - add_field(self.grok2api_remote_auto_check, 9, 1, sticky=tk.W) - - add_label(10, 0, "grok2api 远端 Base:") - self.grok2api_remote_base_var = tk.StringVar(value=str(config.get("grok2api_remote_base", ""))) - self.grok2api_remote_base_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_base_var, width=72) - add_field(self.grok2api_remote_base_entry, 10, 1, columnspan=3) - - add_label(11, 0, "grok2api 远端 app_key:") - self.grok2api_remote_key_var = tk.StringVar(value=str(config.get("grok2api_remote_app_key", ""))) - self.grok2api_remote_key_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_key_var, width=72) - add_field(self.grok2api_remote_key_entry, 11, 1, columnspan=3) -''' - -app = replace_once(app, old_gui_block, new_gui_block, "Cloud Mail GUI fields") - -app = replace_once( - app, - ' config["cloudflare_auth_mode"] = self.cloudflare_auth_mode_var.get().strip() or "none"\n' - ' config["grok2api_auto_add_local"] = bool(self.grok2api_local_auto_var.get())', - ' config["cloudflare_auth_mode"] = self.cloudflare_auth_mode_var.get().strip() or "none"\n' - ' config["cloudmail_api_base"] = self.cloudmail_api_base_var.get().strip()\n' - ' config["cloudmail_public_token"] = self.cloudmail_public_token_var.get().strip()\n' - ' config["cloudmail_domains"] = self.cloudmail_domains_var.get().strip()\n' - ' config["grok2api_auto_add_local"] = bool(self.grok2api_local_auto_var.get())', - "save Cloud Mail GUI config", -) - -app = replace_once( - app, - ' if config["email_provider"] == "cloudflare" and not config["cloudflare_api_base"]:\n' - ' self.log("[!] Cloudflare 模式需要先填写 Cloudflare API Base")\n' - ' return\n' - ' try:', - ' if config["email_provider"] == "cloudflare" and not config["cloudflare_api_base"]:\n' - ' self.log("[!] Cloudflare 模式需要先填写 Cloudflare API Base")\n' - ' return\n' - ' if config["email_provider"] == "cloudmail":\n' - ' missing = []\n' - ' if not config["cloudmail_api_base"]:\n' - ' missing.append("API Base")\n' - ' if not config["cloudmail_public_token"]:\n' - ' missing.append("Public Token")\n' - ' if not config["cloudmail_domains"]:\n' - ' missing.append("域名")\n' - ' if missing:\n' - ' self.log(f"[!] Cloud Mail 模式缺少配置: {\', \'.join(missing)}")\n' - ' return\n' - ' try:', - "validate Cloud Mail GUI config", -) - -ast.parse(app) -app_path.write_text(app, encoding="utf-8-sig") - -config_path = Path("config.example.json") -config_text = config_path.read_text(encoding="utf-8") -config_text = replace_once( - config_text, - ' "cloudflare_path_messages": "/api/mails",\n "proxy": "",', - ' "cloudflare_path_messages": "/api/mails",\n' - ' "cloudmail_api_base": "https://mail.example.com",\n' - ' "cloudmail_public_token": "",\n' - ' "cloudmail_domains": "example.com",\n' - ' "cloudmail_path_messages": "/api/public/emailList",\n' - ' "proxy": "",', - "config.example Cloud Mail fields", -) -json.loads(config_text) -config_path.write_text(config_text, encoding="utf-8") - -readme_path = Path("README.md") -readme = readme_path.read_text(encoding="utf-8") -readme = replace_once( - readme, - '- 支持 DuckMail、YYDS、Cloudflare 临时邮箱接口。', - '- 支持 DuckMail、YYDS、Cloudflare,以及 Cloud Mail 无人收件模式。', - "README feature list", -) -readme = replace_once( - readme, - '| `email_provider` | 邮箱服务商:`duckmail`、`yyds`、`cloudflare` |', - '| `email_provider` | 邮箱服务商:`duckmail`、`yyds`、`cloudflare`、`cloudmail` |', - "README provider list", -) -readme = replace_once( - readme, - '| `defaultDomains` | Cloudflare 临时邮箱默认域名 |\n' - '| `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 |', - '| `defaultDomains` | Cloudflare 临时邮箱默认域名 |\n' - '| `cloudmail_api_base` | Cloud Mail 站点地址 |\n' - '| `cloudmail_public_token` | Cloud Mail 公共 API Token,不会写入邮箱凭证文件 |\n' - '| `cloudmail_domains` | Cloud Mail 无人收件域名,多个域名用英文逗号分隔 |\n' - '| `cloudmail_path_messages` | Cloud Mail 公共收件接口路径,默认 `/api/public/emailList` |\n' - '| `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 |', - "README Cloud Mail config rows", -) - -cloudmail_readme = '''### Cloud Mail 无人收件模式(可选) - -该模式对接 `maillab/cloud-mail`,使用其“无人收件”功能:程序直接生成随机邮箱地址,不需要先在 Cloud Mail 中创建邮箱账号。 - -使用前需要: - -1. 在 Cloud Mail 系统设置中开启“无人收件”。 -2. 使用管理员账号生成公共 API Token: - -```bash -curl -X POST "https://你的-Cloud-Mail-域名/api/public/genToken" \\ - -H "Content-Type: application/json" \\ - -d '{"email":"管理员邮箱","password":"管理员密码"}' -``` - -3. 配置本项目: - -```json -{ - "email_provider": "cloudmail", - "cloudmail_api_base": "https://你的-Cloud-Mail-域名", - "cloudmail_public_token": "返回结果 data 中的 token", - "cloudmail_domains": "你的收信域名.com", - "cloudmail_path_messages": "/api/public/emailList" -} -``` - -程序会通过 `POST /api/public/emailList` 按目标地址查询邮件。公共 Token 只从 `config.json` 读取,不会作为邮箱 credential 输出到日志或 `mail_credentials.txt`。 - -''' - -readme = replace_once( - readme, - '### grok2api 远端入池配置\n', - cloudmail_readme + '### grok2api 远端入池配置\n', - "README Cloud Mail section", -) -readme_path.write_text(readme, encoding="utf-8")