清理代码
This commit is contained in:
@@ -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: 会话 ID(oai-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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user