feat(mail): add optional Cloud Mail no-recipient provider

This commit is contained in:
github-actions[bot]
2026-07-14 11:12:19 +00:00
parent 0a5df68534
commit 4bf37b78ed
6 changed files with 260 additions and 562 deletions
-457
View File
@@ -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")