清理代码

This commit is contained in:
dela
2026-01-26 16:25:22 +08:00
parent 4813449f9c
commit 70627f09fe
18 changed files with 494 additions and 3074 deletions

View File

@@ -1,81 +1,175 @@
"""
浏览器指纹生成模块
用于生成符合 OpenAI Sentinel 要求的浏览器指纹配置数组
"""
# modules/fingerprint.py
"""浏览器指纹生成"""
import uuid
import random
import time
from typing import List
from datetime import datetime
from typing import Dict, List, Any
from config import load_config
class BrowserFingerprint:
"""
浏览器指纹生成器
生成 Sentinel SDK 所需的配置数组18 个元素)
"""
"""生成符合 SDK 期望的浏览器指纹"""
def __init__(self, session_id: str = None):
"""
初始化浏览器指纹
参数:
session_id: 会话 IDoai-did如果不提供则自动生成
"""
self.session_id = session_id or str(uuid.uuid4())
self.start_time = time.time()
def get_config_array(self) -> List:
# 新增: 使用确定性方法从 session_id 派生 Stripe 指纹
import hashlib
seed = hashlib.sha256(self.session_id.encode()).hexdigest()
# seed 是64个hex字符我们需要确保切片正确
# 从 seed 生成一致的 guid/muid/sid
# UUID需要32个hex字符去掉连字符额外部分直接拼接
self.stripe_guid = seed[:8] + '-' + seed[8:12] + '-' + seed[12:16] + '-' + seed[16:20] + '-' + seed[20:32] + seed[32:40]
self.stripe_muid = seed[:8] + '-' + seed[8:12] + '-' + seed[12:16] + '-' + seed[16:20] + '-' + seed[20:32] + seed[40:46]
self.stripe_sid = seed[:8] + '-' + seed[8:12] + '-' + seed[12:16] + '-' + seed[16:20] + '-' + seed[20:32] + seed[46:52]
# 从配置加载指纹参数
config = load_config()
fingerprint_cfg = config.fingerprint_config
self.user_agent = fingerprint_cfg['user_agent']
self.screen_width = fingerprint_cfg['screen_width']
self.screen_height = fingerprint_cfg['screen_height']
self.languages = fingerprint_cfg['languages']
self.hardware_concurrency = fingerprint_cfg['hardware_concurrency']
def get_config_array(self) -> List[Any]:
"""
获取 Sentinel SDK 配置数组
生成 SDK getConfig() 函数返回的 18 元素数组
返回:
包含 18 个元素的指纹数组
数组结构(从 JS 逆向):
[0] screen dimensions (width*height)
[1] timestamp
[2] memory (hardwareConcurrency)
[3] nonce (动态值PoW 时会修改)
[4] user agent
[5] random element
[6] script src
[7] language
[8] languages (joined)
[9] elapsed time (ms)
[10] random function test
[11] keys
[12] window keys
[13] performance.now()
[14] uuid (session_id)
[15] URL params
[16] hardware concurrency
[17] timeOrigin
对应 SDK 源码:
[0]: screen.width + screen.height
[1]: new Date().toString()
[2]: performance.memory.jsHeapSizeLimit (可选)
[3]: nonce (PoW 填充)
[4]: navigator.userAgent
[5]: 随机 script.src
[6]: build ID
[7]: navigator.language
[8]: navigator.languages.join(',')
[9]: 运行时间 (PoW 填充)
[10]: 随机 navigator 属性
[11]: 随机 document key
[12]: 随机 window key
[13]: performance.now()
[14]: session UUID
[15]: URL search params
[16]: navigator.hardwareConcurrency
[17]: performance.timeOrigin
"""
elapsed_ms = int((time.time() - self.start_time) * 1000)
# 模拟的 script sources
fake_scripts = [
"https://sentinel.openai.com/sentinel/97790f37/sdk.js",
"https://chatgpt.com/static/js/main.abc123.js",
"https://cdn.oaistatic.com/_next/static/chunks/main.js",
]
# 模拟的 navigator 属性名
navigator_props = [
'hardwareConcurrency', 'language', 'languages',
'platform', 'userAgent', 'vendor'
]
# 模拟的 document keys
document_keys = ['body', 'head', 'documentElement', 'scripts']
# 模拟的 window keys
window_keys = ['performance', 'navigator', 'document', 'location']
current_time = time.time() * 1000
return [
1920 * 1080, # [0] screen dimensions
str(int(time.time() * 1000)), # [1] timestamp
8, # [2] hardware concurrency
0, # [3] nonce (placeholder)
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", # [4] UA
str(0.123456789), # [5] random element
"https://chatgpt.com/_next/static/chunks/sentinel.js", # [6] script src
"en-US", # [7] language
"en-US,en", # [8] languages
elapsed_ms, # [9] elapsed time
"", # [10] random function
"", # [11] keys
"", # [12] window keys
elapsed_ms, # [13] performance.now()
self.session_id, # [14] uuid (oai-did)
"", # [15] URL params
8, # [16] hardware concurrency
int(time.time() * 1000) - elapsed_ms, # [17] timeOrigin
self.screen_width + self.screen_height, # [0]
str(datetime.now()), # [1]
None, # [2] memory
None, # [3] nonce (placeholder)
self.user_agent, # [4]
random.choice(fake_scripts), # [5]
"97790f37", # [6] build ID
self.languages[0], # [7]
",".join(self.languages), # [8]
None, # [9] runtime (placeholder)
f"{random.choice(navigator_props)}{random.randint(1, 16)}", # [10]
random.choice(document_keys), # [11]
random.choice(window_keys), # [12]
current_time, # [13]
self.session_id, # [14]
"", # [15] URL params
self.hardware_concurrency, # [16]
current_time - random.uniform(100, 1000), # [17] timeOrigin
]
def get_cookies(self) -> Dict[str, str]:
"""生成初始 cookies"""
return {
'oai-did': self.session_id,
}
def get_stripe_fingerprint(self) -> Dict[str, str]:
"""获取 Stripe 支付指纹(与 session_id 一致派生)"""
return {
'guid': self.stripe_guid,
'muid': self.stripe_muid,
'sid': self.stripe_sid,
}
def get_headers(self, with_sentinel: str = None, host: str = 'auth.openai.com') -> Dict[str, str]:
"""生成 HTTP headers支持多域名"""
# 基础 headers
headers = {
'User-Agent': self.user_agent,
'Accept': 'application/json',
'Accept-Language': f"{self.languages[0]},{self.languages[1]};q=0.5",
# Note: urllib3/requests only auto-decompress brotli/zstd when optional
# deps are installed; avoid advertising unsupported encodings.
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Priority': 'u=1, i',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
}
# 根据域名设置特定 headers
if 'chatgpt.com' in host:
headers.update({
'Origin': 'https://chatgpt.com',
'Referer': 'https://chatgpt.com/',
})
else:
headers.update({
'Origin': 'https://auth.openai.com',
'Referer': 'https://auth.openai.com/create-account/password',
'Content-Type': 'application/json',
})
# Sentinel token
if with_sentinel:
headers['openai-sentinel-token'] = with_sentinel
# Datadog RUM tracing
trace_id = random.randint(10**18, 10**19 - 1)
parent_id = random.randint(10**18, 10**19 - 1)
headers.update({
'traceparent': f'00-0000000000000000{trace_id:016x}-{parent_id:016x}-01',
'tracestate': 'dd=s:1;o:rum',
'x-datadog-origin': 'rum',
'x-datadog-parent-id': str(parent_id),
'x-datadog-sampling-priority': '1',
'x-datadog-trace-id': str(trace_id),
})
return headers
# 导出
__all__ = ["BrowserFingerprint"]

View File

@@ -1,14 +1,5 @@
"""
邮件接码处理器
用于接收和解析 OpenAI 发送的验证码邮件
⚠️ 本模块提供预留接口,用户需要根据实际情况配置邮箱服务
支持的邮箱方案:
1. IMAP 收件 (Gmail, Outlook, 自建邮箱)
2. 临时邮箱 API (TempMail, Guerrilla Mail, etc.)
3. 邮件转发服务
邮件接码处理器 - 用于接收和解析 OpenAI 发送的验证码邮件
"""
from typing import Optional, Dict, Any, List
@@ -19,58 +10,23 @@ from utils.logger import logger
class MailHandler:
"""
邮件接码处理器
"""邮件接码处理器基类"""
⚠️ 预留接口 - 用户需要配置实际的邮箱服务
使用场景:
- 接收 OpenAI 发送的 6 位数字 OTP 验证码
- 解析邮件内容提取验证码
- 支持超时和重试机制
"""
# OTP 邮件特征
OTP_SUBJECT_KEYWORDS = ["openai", "verification", "verify", "code"]
OTP_SENDER = "noreply@tm.openai.com" # OpenAI 发件人地址
OTP_SENDER = "noreply@tm.openai.com"
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
初始化邮件处理器
参数:
config: 邮箱配置字典,可能包含:
- type: "imap" | "tempmail" | "api" | "cloudmail"
- host: IMAP 服务器地址 (如果使用 IMAP)
- port: IMAP 端口 (默认 993)
- username: 邮箱用户名
- password: 邮箱密码
- api_key: 临时邮箱 API Key (如果使用 API)
"""
self.config = config or {}
self.mail_type = self.config.get("type", "not_configured")
if not config:
logger.warning(
"MailHandler initialized without configuration. "
"OTP retrieval will fail until configured."
)
logger.warning("MailHandler initialized without configuration.")
else:
logger.info(f"MailHandler initialized with type: {self.mail_type}")
@staticmethod
def create(config: Optional[Dict[str, Any]]) -> "MailHandler":
"""
工厂方法:创建合适的邮件处理器
根据配置中的 type 字段自动选择正确的 handler 实现
参数:
config: 邮箱配置字典
返回:
MailHandler 实例IMAPMailHandler 或 CloudMailHandler
"""
"""工厂方法:根据配置创建对应的邮件处理器"""
if not config:
return MailHandler(config)
@@ -81,115 +37,22 @@ class MailHandler:
elif mail_type == "cloudmail":
return CloudMailHandler(config)
else:
# 默认处理器(会抛出 NotImplementedError
return MailHandler(config)
async def wait_for_otp(
self,
email: str,
timeout: int = 300,
check_interval: int = 5
) -> str:
"""
等待并提取 OTP 验证码
⚠️ 预留接口 - 用户需要实现此方法
参数:
email: 注册邮箱地址
timeout: 超时时间(秒),默认 300 秒5 分钟)
check_interval: 检查间隔(秒),默认 5 秒
返回:
6 位数字验证码(例如 "123456"
抛出:
NotImplementedError: 用户需要实现此方法
TimeoutError: 超时未收到邮件
ValueError: 邮件格式错误,无法提取 OTP
集成示例:
```python
# 方案 1: 使用 IMAP (imap-tools 库)
from imap_tools import MailBox
with MailBox(self.config["host"]).login(
self.config["username"],
self.config["password"]
) as mailbox:
for msg in mailbox.fetch(AND(from_=self.OTP_SENDER, seen=False)):
otp = self._extract_otp(msg.text)
if otp:
return otp
# 方案 2: 使用临时邮箱 API
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://tempmail.api/messages?email={email}",
headers={"Authorization": f"Bearer {self.config['api_key']}"}
)
messages = resp.json()
for msg in messages:
otp = self._extract_otp(msg["body"])
if otp:
return otp
# 方案 3: 手动输入(调试用)
print(f"Please enter OTP for {email}:")
return input().strip()
```
"""
logger.info(
f"Waiting for OTP for {email} "
f"(timeout: {timeout}s, check_interval: {check_interval}s)"
)
raise NotImplementedError(
"❌ Mail handler not configured.\n\n"
"User needs to configure email service for OTP retrieval.\n\n"
"Configuration options:\n\n"
"1. IMAP (Gmail, Outlook, custom):\n"
" config = {\n"
" 'type': 'imap',\n"
" 'host': 'imap.gmail.com',\n"
" 'port': 993,\n"
" 'username': 'your@email.com',\n"
" 'password': 'app_password'\n"
" }\n\n"
"2. Temporary email API:\n"
" config = {\n"
" 'type': 'tempmail',\n"
" 'api_key': 'YOUR_API_KEY',\n"
" 'api_endpoint': 'https://api.tempmail.com'\n"
" }\n\n"
"3. Manual input (for debugging):\n"
" config = {'type': 'manual'}\n\n"
"Example implementation location: utils/mail_box.py -> wait_for_otp()"
)
async def wait_for_otp(self, email: str, timeout: int = 300, check_interval: int = 5) -> str:
"""等待并提取 OTP 验证码(需子类实现)"""
logger.info(f"Waiting for OTP for {email} (timeout: {timeout}s)")
raise NotImplementedError("Mail handler not configured.")
def _extract_otp(self, text: str) -> Optional[str]:
"""
从邮件正文中提取 OTP 验证码
OpenAI 邮件格式示例:
"Your OpenAI verification code is: 123456"
"Enter this code: 123456"
参数:
text: 邮件正文(纯文本或 HTML
返回:
6 位数字验证码,未找到则返回 None
"""
# 清理 HTML 标签(如果有)
"""从邮件正文中提取 6 位 OTP 验证码"""
text = re.sub(r'<[^>]+>', ' ', text)
# 常见的 OTP 模式
patterns = [
r'verification code is[:\s]+(\d{6})', # "verification code is: 123456"
r'code[:\s]+(\d{6})', # "code: 123456"
r'enter[:\s]+(\d{6})', # "enter: 123456"
r'(\d{6})', # 任意 6 位数字(最后尝试)
r'verification code is[:\s]+(\d{6})',
r'code[:\s]+(\d{6})',
r'enter[:\s]+(\d{6})',
r'(\d{6})',
]
for pattern in patterns:
@@ -202,118 +65,27 @@ class MailHandler:
logger.warning("Failed to extract OTP from email text")
return None
def _check_imap(self, email: str, timeout: int) -> Optional[str]:
"""
使用 IMAP 检查邮件(预留方法)
参数:
email: 注册邮箱
timeout: 超时时间(秒)
返回:
OTP 验证码,未找到则返回 None
"""
# TODO: 用户实现 IMAP 检查逻辑
# 需要安装: pip install imap-tools
raise NotImplementedError("IMAP checking not implemented")
def _check_tempmail_api(self, email: str, timeout: int) -> Optional[str]:
"""
使用临时邮箱 API 检查邮件(预留方法)
参数:
email: 注册邮箱
timeout: 超时时间(秒)
返回:
OTP 验证码,未找到则返回 None
"""
# TODO: 用户实现临时邮箱 API 调用逻辑
raise NotImplementedError("Temp mail API not implemented")
def generate_temp_email(self) -> str:
"""
生成临时邮箱地址(可选功能)
⚠️ 预留接口 - 如果使用临时邮箱服务,需要实现此方法
返回:
临时邮箱地址(例如 "random123@tempmail.com"
抛出:
NotImplementedError: 用户需要实现此方法
"""
raise NotImplementedError(
"Temp email generation not implemented. "
"Integrate a temp mail service API if needed."
)
def verify_email_deliverability(self, email: str) -> bool:
"""
验证邮箱地址是否可以接收邮件(可选功能)
参数:
email: 邮箱地址
返回:
True 如果邮箱有效且可接收邮件,否则 False
"""
# 基本格式验证
"""验证邮箱地址格式是否有效"""
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
logger.warning(f"Invalid email format: {email}")
return False
# TODO: 用户可以添加更严格的验证逻辑
# 例如DNS MX 记录查询、SMTP 验证等
logger.info(f"Email format valid: {email}")
return True
class IMAPMailHandler(MailHandler):
"""
基于 IMAP 的邮件处理器(完整实现示例)
"""基于 IMAP 的邮件处理器"""
⚠️ 这是一个参考实现,用户可以根据需要修改
依赖:
pip install imap-tools
"""
async def wait_for_otp(
self,
email: str,
timeout: int = 300,
check_interval: int = 5
) -> str:
"""
使用 IMAP 等待 OTP 邮件
参数:
email: 注册邮箱
timeout: 超时时间(秒)
check_interval: 检查间隔(秒)
返回:
6 位数字验证码
抛出:
ImportError: 未安装 imap-tools
TimeoutError: 超时未收到邮件
ValueError: 配置错误或邮件格式错误
"""
async def wait_for_otp(self, email: str, timeout: int = 300, check_interval: int = 5) -> str:
"""使用 IMAP 等待 OTP 邮件"""
try:
from imap_tools import MailBox, AND
except ImportError:
raise ImportError(
"imap-tools not installed. Install with: pip install imap-tools"
)
raise ImportError("imap-tools not installed. Install with: pip install imap-tools")
if not all(k in self.config for k in ["host", "username", "password"]):
raise ValueError(
"IMAP configuration incomplete. Required: host, username, password"
)
raise ValueError("IMAP configuration incomplete. Required: host, username, password")
start_time = time.time()
logger.info(f"Connecting to IMAP server: {self.config['host']}")
@@ -324,86 +96,44 @@ class IMAPMailHandler(MailHandler):
self.config["username"],
self.config["password"]
) as mailbox:
# 查找未读邮件,来自 OpenAI
for msg in mailbox.fetch(
AND(from_=self.OTP_SENDER, seen=False),
reverse=True, # 最新的邮件优先
reverse=True,
limit=10
):
# 检查主题是否包含 OTP 关键词
if any(kw in msg.subject.lower() for kw in self.OTP_SUBJECT_KEYWORDS):
otp = self._extract_otp(msg.text or msg.html)
if otp:
logger.success(f"OTP received: {otp}")
# 标记为已读
mailbox.flag([msg.uid], ['\\Seen'], True)
return otp
except Exception as e:
logger.warning(f"IMAP check failed: {e}")
# 等待下一次检查
elapsed = time.time() - start_time
remaining = timeout - elapsed
logger.debug(
f"No OTP found, waiting {check_interval}s "
f"(remaining: {int(remaining)}s)"
)
logger.debug(f"No OTP found, waiting {check_interval}s")
time.sleep(check_interval)
raise TimeoutError(
f"Timeout waiting for OTP email (timeout: {timeout}s). "
f"Email: {email}, Sender: {self.OTP_SENDER}"
)
raise TimeoutError(f"Timeout waiting for OTP email ({timeout}s)")
class CloudMailHandler(MailHandler):
"""
Cloud Mail API handler with external token management
使用外部预生成的 Token 管理邮件,不调用 genToken API
依赖:
pip install httpx
配置示例:
config = {
"type": "cloudmail",
"api_base_url": "https://your-cloudmail-domain.com",
"token": "9f4e298e-7431-4c76-bc15-4931c3a73984",
"target_email": "user@example.com" # 可选
}
"""
"""Cloud Mail API 邮件处理器"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
初始化 Cloud Mail handler
参数:
config: 配置字典,必填项:
- api_base_url: Cloud Mail API 基础 URL
- token: 预生成的身份令牌
- target_email: (可选) 指定监控的邮箱地址
"""
super().__init__(config)
# 验证必填配置
required = ["api_base_url", "token"]
missing = [key for key in required if not self.config.get(key)]
if missing:
raise ValueError(
f"CloudMail configuration incomplete. Missing: {', '.join(missing)}\n"
f"Required: api_base_url, token"
)
raise ValueError(f"CloudMail config incomplete. Missing: {', '.join(missing)}")
self.api_base_url = self.config["api_base_url"].rstrip("/")
self.token = self.config["token"]
self.target_email = self.config.get("target_email")
self.domain = self.config.get("domain") # 邮箱域名
self.domain = self.config.get("domain")
self._client: Optional[Any] = None
logger.info(f"CloudMailHandler initialized (API: {self.api_base_url}, Domain: {self.domain or 'N/A'})")
logger.info(f"CloudMailHandler initialized (API: {self.api_base_url})")
async def _get_client(self):
"""懒加载 HTTP 客户端"""
@@ -411,9 +141,7 @@ class CloudMailHandler(MailHandler):
try:
import httpx
except ImportError:
raise ImportError(
"httpx not installed. Install with: pip install httpx"
)
raise ImportError("httpx not installed. Install with: pip install httpx")
self._client = httpx.AsyncClient(
timeout=30.0,
@@ -433,33 +161,19 @@ class CloudMailHandler(MailHandler):
num: int = 1,
size: int = 20
) -> List[Dict[str, Any]]:
"""
查询邮件列表 (POST /api/public/emailList)
参数:
to_email: 收件人邮箱
send_email: 发件人邮箱(可选,支持 % 通配符)
subject: 主题关键词(可选)
time_sort: 时间排序 (desc/asc)
num: 页码(从 1 开始)
size: 每页数量
返回:
邮件列表
"""
"""查询邮件列表 (POST /api/public/emailList)"""
client = await self._get_client()
url = f"{self.api_base_url}/api/public/emailList"
payload = {
"toEmail": to_email,
"type": 0, # 0=收件箱
"isDel": 0, # 0=未删除
"type": 0,
"isDel": 0,
"timeSort": time_sort,
"num": num,
"size": size
}
# 可选参数
if send_email:
payload["sendEmail"] = send_email
if subject:
@@ -468,38 +182,17 @@ class CloudMailHandler(MailHandler):
try:
resp = await client.post(url, json=payload)
# 检查认证错误
if resp.status_code in [401, 403]:
raise RuntimeError(
"CloudMail token expired or invalid.\n"
"Please regenerate token and update MAIL_CLOUDMAIL_TOKEN in .env\n"
"Steps:\n"
"1. Login to Cloud Mail with admin account\n"
"2. Call POST /api/public/genToken to generate new token\n"
"3. Update MAIL_CLOUDMAIL_TOKEN in .env\n"
"4. Restart the program"
)
raise RuntimeError("CloudMail token expired or invalid.")
if resp.status_code != 200:
raise RuntimeError(
f"CloudMail API error: {resp.status_code} - {resp.text[:200]}"
)
raise RuntimeError(f"CloudMail API error: {resp.status_code}")
data = resp.json()
# 检查业务逻辑错误
if data.get("code") != 200:
error_msg = data.get("message", "Unknown error")
raise RuntimeError(
f"CloudMail API error: {error_msg} (code: {data.get('code')})"
)
raise RuntimeError(f"CloudMail API error: {data.get('message')}")
# 返回邮件列表
result = data.get("data", {})
# CloudMail API 可能返回两种格式:
# 1. {"data": {"list": [...]}} - 标准格式
# 2. {"data": [...]} - 直接列表格式
if isinstance(result, list):
emails = result
elif isinstance(result, dict):
@@ -512,174 +205,102 @@ class CloudMailHandler(MailHandler):
except Exception as e:
if "httpx" in str(type(e).__module__):
# httpx 网络错误
raise RuntimeError(f"CloudMail API network error: {e}")
else:
# 重新抛出其他错误
raise
raise
async def ensure_email_exists(self, email: str) -> bool:
"""
确保邮箱账户存在(如果不存在则创建)
"""确保邮箱账户存在(如果不存在则创建)"""
logger.info(f"CloudMail: Creating email account {email}...")
用于在注册流程开始前自动创建 Cloud Mail 邮箱账户
参数:
email: 邮箱地址
返回:
True 如果邮箱已存在或成功创建
"""
try:
# 先尝试查询邮箱(检查是否存在)
logger.debug(f"CloudMail: Checking if {email} exists...")
emails = await self._query_emails(
to_email=email,
size=1
)
# 如果能查询到,说明邮箱存在
logger.debug(f"CloudMail: Email {email} already exists")
result = await self.add_users([{"email": email}])
logger.success(f"CloudMail: Email {email} created successfully")
logger.debug(f"CloudMail: API response: {result}")
return True
except Exception as e:
# 查询失败可能是邮箱不存在,尝试创建
logger.info(f"CloudMail: Creating email account {email}...")
try:
await self.add_users([{"email": email}])
logger.success(f"CloudMail: Email {email} created successfully")
except RuntimeError as e:
error_msg = str(e).lower()
if "already" in error_msg or "exist" in error_msg or "duplicate" in error_msg:
logger.debug(f"CloudMail: Email {email} already exists")
return True
except Exception as create_error:
logger.error(f"CloudMail: Failed to create email {email}: {create_error}")
raise
logger.error(f"CloudMail: Failed to create email {email}: {e}")
raise
async def wait_for_otp(
self,
email: str,
timeout: int = 300,
check_interval: int = 5
) -> str:
"""
等待 OTP 邮件(轮询实现)
except Exception as e:
logger.error(f"CloudMail: Failed to create email {email}: {e}")
raise
参数:
email: 注册邮箱
timeout: 超时时间(秒)
check_interval: 检查间隔(秒)
返回:
6 位数字验证码
抛出:
TimeoutError: 超时未收到邮件
ValueError: 邮件格式错误,无法提取 OTP
"""
async def wait_for_otp(self, email: str, timeout: int = 300, check_interval: int = 5) -> str:
"""等待 OTP 邮件(轮询实现)"""
start_time = time.time()
logger.info(
f"CloudMail: Waiting for OTP for {email} "
f"(timeout: {timeout}s, interval: {check_interval}s)"
)
logger.info(f"CloudMail: Waiting for OTP for {email} (timeout: {timeout}s)")
while time.time() - start_time < timeout:
try:
# 查询最近的邮件
# 模糊匹配 openai 发件人
emails = await self._query_emails(
to_email=email,
send_email=self.OTP_SENDER,
send_email="%openai%",
time_sort="desc",
size=10
)
# 检查每封邮件
# 备选:不过滤发件人
if not emails:
emails = await self._query_emails(
to_email=email,
time_sort="desc",
size=10
)
for msg in emails:
subject = msg.get("subject", "").lower()
sender = msg.get("sendEmail", "").lower()
is_from_openai = "openai" in sender or "noreply" in sender
# 检查主题是否包含 OTP 关键词
if any(kw in subject for kw in self.OTP_SUBJECT_KEYWORDS):
# 尝试从邮件内容提取 OTP
if any(kw in subject for kw in self.OTP_SUBJECT_KEYWORDS) or is_from_openai:
content = msg.get("text") or msg.get("content") or ""
otp = self._extract_otp(content)
if otp:
logger.success(f"CloudMail: OTP received: {otp}")
logger.success(f"CloudMail: OTP received: {otp} (from: {sender})")
return otp
# 等待下一次检查
elapsed = time.time() - start_time
remaining = timeout - elapsed
logger.debug(
f"CloudMail: No OTP found, waiting {check_interval}s "
f"(remaining: {int(remaining)}s)"
)
remaining = timeout - (time.time() - start_time)
logger.debug(f"CloudMail: No OTP found, waiting {check_interval}s (remaining: {int(remaining)}s)")
await asyncio.sleep(check_interval)
except Exception as e:
logger.warning(f"CloudMail: Query error: {e}")
await asyncio.sleep(check_interval)
raise TimeoutError(
f"Timeout waiting for OTP email (timeout: {timeout}s). "
f"Email: {email}, Sender: {self.OTP_SENDER}"
)
raise TimeoutError(f"Timeout waiting for OTP email ({timeout}s)")
async def add_users(
self,
users: List[Dict[str, str]]
) -> Dict[str, Any]:
"""
添加用户 (POST /api/public/addUser)
参数:
users: 用户列表,格式:
[
{
"email": "test@example.com",
"password": "optional", # 可选
"roleName": "optional" # 可选
}
]
返回:
API 响应数据
"""
async def add_users(self, users: List[Dict[str, str]]) -> Dict[str, Any]:
"""添加用户 (POST /api/public/addUser)"""
client = await self._get_client()
url = f"{self.api_base_url}/api/public/addUser"
payload = {"list": users}
try:
resp = await client.post(url, json=payload)
# 检查认证错误
if resp.status_code in [401, 403]:
raise RuntimeError(
"CloudMail token expired or invalid. "
"Please regenerate token and update MAIL_CLOUDMAIL_TOKEN in .env"
)
raise RuntimeError("CloudMail token expired or invalid.")
if resp.status_code != 200:
raise RuntimeError(
f"CloudMail addUser API error: {resp.status_code} - {resp.text[:200]}"
)
raise RuntimeError(f"CloudMail addUser error: {resp.status_code}")
data = resp.json()
# 检查业务逻辑错误
if data.get("code") != 200:
error_msg = data.get("message", "Unknown error")
raise RuntimeError(
f"CloudMail addUser error: {error_msg} (code: {data.get('code')})"
)
raise RuntimeError(f"CloudMail addUser error: {data.get('message')}")
logger.info(f"CloudMail: Users added successfully: {len(users)} users")
logger.info(f"CloudMail: Users added: {len(users)}")
return data
except Exception as e:
if "httpx" in str(type(e).__module__):
raise RuntimeError(f"CloudMail API network error: {e}")
else:
raise
raise
async def close(self):
"""清理资源"""
@@ -688,9 +309,4 @@ class CloudMailHandler(MailHandler):
logger.debug("CloudMail: HTTP client closed")
# 导出主要接口
__all__ = [
"MailHandler",
"IMAPMailHandler",
"CloudMailHandler",
]
__all__ = ["MailHandler", "IMAPMailHandler", "CloudMailHandler"]