feat(cloudflare): 支持管理员创建临时邮箱

This commit is contained in:
VitaHuang
2026-07-05 21:24:17 +08:00
parent 5bf419f824
commit 1bff080738
6 changed files with 225 additions and 27 deletions
+27 -1
View File
@@ -86,7 +86,9 @@ cp config.example.json config.json
| `proxy` | 代理地址,可留空 |
| `enable_nsfw` | 注册后是否尝试开启 NSFW |
| `cloudflare_api_base` | Cloudflare 临时邮箱 API 地址 |
| `cloudflare_auth_mode` | Cloudflare API 鉴权模式:`none``bearer``x-api-key``query-key` |
| `cloudflare_api_key` | Cloudflare 临时邮箱接口密钥;admin 模式填 `ADMIN_PASSWORD` |
| `cloudflare_auth_mode` | Cloudflare API 鉴权模式:`none``bearer``x-api-key``x-admin-auth``query-key` |
| `cloudflare_path_accounts` | Cloudflare 创建邮箱路径;匿名模式用 `/api/new_address`admin 模式用 `/admin/new_address` |
| `defaultDomains` | Cloudflare 临时邮箱默认域名 |
| `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 |
| `grok2api_local_token_file` | 本地 grok2api token 文件路径 |
@@ -94,6 +96,30 @@ cp config.example.json config.json
| `grok2api_remote_base` | 远端 grok2api 管理 API 地址 |
| `grok2api_remote_app_key` | 远端 grok2api app key |
### Cloudflare 临时邮箱 admin 模式
如果使用 `dreamhunter2333/cloudflare_temp_email` 且匿名 `/api/new_address` 开启了 Turnstile,可以改用 admin 创建邮箱接口:
```json
{
"email_provider": "cloudflare",
"cloudflare_api_base": "https://你的-worker-api-域名",
"cloudflare_api_key": "你的 ADMIN_PASSWORD",
"cloudflare_auth_mode": "x-admin-auth",
"cloudflare_path_accounts": "/admin/new_address",
"cloudflare_path_messages": "/api/mails",
"defaultDomains": "你的收信域名.com"
}
```
创建邮箱会使用 `x-admin-auth` 调用 `/admin/new_address`,后续收件仍使用接口返回的地址 JWT 调用 `/api/mails`
可先用调试脚本验证 admin 创建接口:
```bash
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"
```
`config.json` 包含个人配置和密钥,不要提交到 Git。
## 运行
+72 -7
View File
@@ -3,6 +3,8 @@
import argparse
import re
import secrets
import string
import time
from typing import Any, Dict, List, Optional, Tuple
@@ -36,21 +38,72 @@ def json_or_text(resp: requests.Response) -> Tuple[Optional[Dict[str, Any]], str
return None, (resp.text or "")[:400]
def create_address(api_base: str) -> Tuple[str, str]:
def generate_username(length: int = 10) -> str:
"""生成 cloudflare_temp_email admin API 需要的随机邮箱名称。"""
chars = string.ascii_lowercase + string.digits
return "".join(secrets.choice(chars) for _ in range(length))
def normalize_path(path: str, default_path: str) -> str:
"""标准化 API 路径,避免漏写开头斜杠。"""
raw = (path or default_path).strip() or default_path
return raw if raw.startswith("/") else f"/{raw}"
def build_auth_headers(auth_mode: str, api_key: str, content_type: bool = False) -> Dict[str, str]:
"""按调试参数构造 Cloudflare 临时邮箱接口鉴权请求头。"""
headers = {"Content-Type": "application/json"} if content_type else {}
key = (api_key or "").strip()
mode = (auth_mode or "none").strip().lower()
if not key:
return headers
if mode == "x-admin-auth":
headers["x-admin-auth"] = key
elif mode == "x-api-key":
headers["X-API-Key"] = key
elif mode == "bearer":
headers["Authorization"] = f"Bearer {key}"
return headers
def create_address(
api_base: str,
auth_mode: str = "none",
api_key: str = "",
create_path: str = "/api/new_address",
domain: str = "",
name: str = "",
) -> Tuple[str, str]:
"""创建 Cloudflare 临时邮箱地址,支持匿名 API 和 admin API。"""
path = normalize_path(create_path, "/api/new_address")
is_admin_create = path.rstrip("/").lower() == "/admin/new_address"
if is_admin_create:
payload: Dict[str, Any] = {
"name": name.strip() if name.strip() else generate_username(),
"enablePrefix": True,
}
if domain.strip():
payload["domain"] = domain.strip()
headers = build_auth_headers(auth_mode, api_key, content_type=True)
else:
payload = {}
if domain.strip():
payload["domain"] = domain.strip()
headers = {"Content-Type": "application/json"}
resp = requests.post(
f"{api_base.rstrip('/')}/api/new_address",
json={},
headers={"Content-Type": "application/json"},
f"{api_base.rstrip('/')}{path}",
json=payload,
headers=headers,
timeout=20,
)
resp.raise_for_status()
data, raw = json_or_text(resp)
if not data:
raise RuntimeError(f"/api/new_address 非JSON: {raw}")
raise RuntimeError(f"{path} 非JSON: {raw}")
address = str(data.get("address", "")).strip()
jwt = str(data.get("jwt", "")).strip()
if not address or not jwt:
raise RuntimeError(f"/api/new_address 缺少 address/jwt: {data}")
raise RuntimeError(f"{path} 缺少 address/jwt: {data}")
return address, jwt
@@ -131,6 +184,11 @@ def main():
ap.add_argument("--api-base", required=True)
ap.add_argument("--address", default="")
ap.add_argument("--credential", default="")
ap.add_argument("--auth-mode", default="none", choices=["none", "bearer", "x-api-key", "x-admin-auth"])
ap.add_argument("--api-key", default="")
ap.add_argument("--create-path", default="/api/new_address")
ap.add_argument("--domain", default="")
ap.add_argument("--name", default="")
ap.add_argument("--timeout", type=int, default=180)
ap.add_argument("--interval", type=int, default=3)
args = ap.parse_args()
@@ -138,7 +196,14 @@ def main():
address = args.address.strip()
credential = args.credential.strip()
if not credential:
address, credential = create_address(args.api_base)
address, credential = create_address(
args.api_base,
auth_mode=args.auth_mode,
api_key=args.api_key,
create_path=args.create_path,
domain=args.domain,
name=args.name,
)
print(f"[NEW] address={address}")
print(f"[NEW] credential(jwt)={credential}")
else:
+4 -4
View File
@@ -1,10 +1,10 @@
{
"duckmail_api_key": "",
"cloudflare_api_base": "https://cloudflare.pages.dev",
"cloudflare_api_key": "",
"cloudflare_auth_mode": "none",
"cloudflare_api_base": "https://temp-mail.example.com",
"cloudflare_api_key": "your-admin-password",
"cloudflare_auth_mode": "x-admin-auth",
"cloudflare_path_domains": "/api/domains",
"cloudflare_path_accounts": "/api/new_address",
"cloudflare_path_accounts": "/admin/new_address",
"cloudflare_path_token": "/api/token",
"cloudflare_path_messages": "/api/mails",
"proxy": "",
+34 -14
View File
@@ -174,6 +174,8 @@ def cloudflare_build_headers(content_type=False):
if key:
if mode == "x-api-key":
headers["X-API-Key"] = key
elif mode == "x-admin-auth":
headers["x-admin-auth"] = key
elif mode != "none":
headers["Authorization"] = f"Bearer {key}"
return headers
@@ -188,6 +190,22 @@ def cloudflare_apply_auth_params(params=None):
return merged
def cloudflare_next_default_domain():
"""按配置轮换选择 Cloudflare 临时邮箱域名。"""
global _cf_domain_index
domains = [x.strip() for x in str(config.get("defaultDomains", "") or "").split(",") if x.strip()]
if not domains:
return ""
domain = domains[_cf_domain_index % len(domains)]
_cf_domain_index += 1
return domain
def cloudflare_is_admin_create_path(path):
"""判断当前创建邮箱路径是否为 cloudflare_temp_email 管理员创建接口。"""
return str(path or "").rstrip("/").lower() == "/admin/new_address"
def _pick_list_payload(data):
if isinstance(data, list):
return data
@@ -208,19 +226,22 @@ def _pick_list_payload(data):
def cloudflare_create_temp_address(api_base):
"""适配 cloudflare_temp_email v1.8.x: POST /api/new_address -> {address,jwt}"""
global _cf_domain_index
url = f"{api_base}/api/new_address"
"""适配 cloudflare_temp_email 新建地址接口并兼容 admin 创建模式。"""
path = get_cloudflare_path("cloudflare_path_accounts", "/api/new_address")
url = f"{api_base}{path}"
domain = cloudflare_next_default_domain()
is_admin_create = cloudflare_is_admin_create_path(path)
if is_admin_create:
payload = {"name": generate_username(10), "enablePrefix": True}
if domain:
payload["domain"] = domain
headers = cloudflare_build_headers(content_type=True)
else:
payload = {}
try:
# 在多个域名之间轮换,降低单域偶发不收件导致的失败率
domains = [x.strip() for x in str(config.get("defaultDomains", "") or "").split(",") if x.strip()]
if domains:
payload["domain"] = domains[_cf_domain_index % len(domains)]
_cf_domain_index += 1
except Exception:
pass
resp = http_post(url, json=payload, headers={"Content-Type": "application/json"})
if domain:
payload["domain"] = domain
headers = {"Content-Type": "application/json"}
resp = http_post(url, json=payload, headers=headers)
resp.raise_for_status()
try:
data = resp.json()
@@ -2480,7 +2501,7 @@ class GrokRegisterGUI:
add_label(2, 2, "Cloudflare 鉴权模式:")
self.cloudflare_auth_mode_var = tk.StringVar(value=config.get("cloudflare_auth_mode", "bearer"))
self.cloudflare_auth_mode_combo = tk_option_menu(
config_frame, self.cloudflare_auth_mode_var, ["query-key", "bearer", "x-api-key", "none"], width=12
config_frame, self.cloudflare_auth_mode_var, ["query-key", "bearer", "x-api-key", "x-admin-auth", "none"], width=12
)
add_field(self.cloudflare_auth_mode_combo, 2, 3, sticky=tk.W)
@@ -2989,4 +3010,3 @@ def main():
if __name__ == "__main__":
main()
View File
+87
View File
@@ -0,0 +1,87 @@
import unittest
from unittest.mock import patch
import cf_mail_debug
import grok_register_ttk as app
class DummyResponse:
def __init__(self, payload):
self._payload = payload
self.text = ""
def raise_for_status(self):
return None
def json(self):
return self._payload
class CloudflareAdminCreateTests(unittest.TestCase):
def setUp(self):
self.original_config = app.config.copy()
def tearDown(self):
app.config = self.original_config
def test_app_uses_admin_new_address_with_x_admin_auth(self):
app.config.update({
"cloudflare_api_key": "admin-secret",
"cloudflare_auth_mode": "x-admin-auth",
"cloudflare_path_accounts": "/admin/new_address",
"defaultDomains": "vitassk.com",
})
captured = {}
def fake_post(url, **kwargs):
captured["url"] = url
captured.update(kwargs)
return DummyResponse({"address": "adminuser@vitassk.com", "jwt": "address-jwt"})
with patch.object(app, "generate_username", return_value="adminuser"), \
patch.object(app, "http_post", side_effect=fake_post):
address, jwt = app.cloudflare_create_temp_address("https://temp-mail.ikun.day")
self.assertEqual(address, "adminuser@vitassk.com")
self.assertEqual(jwt, "address-jwt")
self.assertEqual(captured["url"], "https://temp-mail.ikun.day/admin/new_address")
self.assertEqual(captured["json"], {
"name": "adminuser",
"domain": "vitassk.com",
"enablePrefix": True,
})
self.assertEqual(captured["headers"]["Content-Type"], "application/json")
self.assertEqual(captured["headers"]["x-admin-auth"], "admin-secret")
def test_debug_tool_can_create_address_through_admin_api(self):
captured = {}
def fake_post(url, **kwargs):
captured["url"] = url
captured.update(kwargs)
return DummyResponse({"address": "debuguser@vitassk.com", "jwt": "debug-jwt"})
with patch.object(cf_mail_debug.requests, "post", side_effect=fake_post):
address, jwt = cf_mail_debug.create_address(
"https://temp-mail.ikun.day",
auth_mode="x-admin-auth",
api_key="admin-secret",
create_path="/admin/new_address",
domain="vitassk.com",
name="debuguser",
)
self.assertEqual(address, "debuguser@vitassk.com")
self.assertEqual(jwt, "debug-jwt")
self.assertEqual(captured["url"], "https://temp-mail.ikun.day/admin/new_address")
self.assertEqual(captured["json"], {
"name": "debuguser",
"domain": "vitassk.com",
"enablePrefix": True,
})
self.assertEqual(captured["headers"]["Content-Type"], "application/json")
self.assertEqual(captured["headers"]["x-admin-auth"], "admin-secret")
if __name__ == "__main__":
unittest.main()