mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-31 21:21:30 +08:00
fix(main): 优化项目结构
This commit is contained in:
556
src/services/moe_mail.py
Normal file
556
src/services/moe_mail.py
Normal file
@@ -0,0 +1,556 @@
|
||||
"""
|
||||
自定义域名邮箱服务实现
|
||||
基于 email.md 中的 REST API 接口
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, List
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from .base import BaseEmailService, EmailServiceError, EmailServiceType
|
||||
from ..core.http_client import HTTPClient, RequestConfig
|
||||
from ..config.constants import OTP_CODE_PATTERN
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MeoMailEmailService(BaseEmailService):
|
||||
"""
|
||||
自定义域名邮箱服务
|
||||
基于 REST API 接口
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None, name: str = None):
|
||||
"""
|
||||
初始化自定义域名邮箱服务
|
||||
|
||||
Args:
|
||||
config: 配置字典,支持以下键:
|
||||
- base_url: API 基础地址 (必需)
|
||||
- api_key: API 密钥 (必需)
|
||||
- api_key_header: API 密钥请求头名称 (默认: X-API-Key)
|
||||
- timeout: 请求超时时间 (默认: 30)
|
||||
- max_retries: 最大重试次数 (默认: 3)
|
||||
- proxy_url: 代理 URL
|
||||
- default_domain: 默认域名
|
||||
- default_expiry: 默认过期时间(毫秒)
|
||||
name: 服务名称
|
||||
"""
|
||||
super().__init__(EmailServiceType.CUSTOM_DOMAIN, name)
|
||||
|
||||
# 必需配置检查
|
||||
required_keys = ["base_url", "api_key"]
|
||||
missing_keys = [key for key in required_keys if key not in (config or {})]
|
||||
|
||||
if missing_keys:
|
||||
raise ValueError(f"缺少必需配置: {missing_keys}")
|
||||
|
||||
# 默认配置
|
||||
default_config = {
|
||||
"base_url": "",
|
||||
"api_key": "",
|
||||
"api_key_header": "X-API-Key",
|
||||
"timeout": 30,
|
||||
"max_retries": 3,
|
||||
"proxy_url": None,
|
||||
"default_domain": None,
|
||||
"default_expiry": 3600000, # 1小时
|
||||
}
|
||||
|
||||
self.config = {**default_config, **(config or {})}
|
||||
|
||||
# 创建 HTTP 客户端
|
||||
http_config = RequestConfig(
|
||||
timeout=self.config["timeout"],
|
||||
max_retries=self.config["max_retries"],
|
||||
)
|
||||
self.http_client = HTTPClient(
|
||||
proxy_url=self.config.get("proxy_url"),
|
||||
config=http_config
|
||||
)
|
||||
|
||||
# 状态变量
|
||||
self._emails_cache: Dict[str, Dict[str, Any]] = {}
|
||||
self._last_config_check: float = 0
|
||||
self._cached_config: Optional[Dict[str, Any]] = None
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""获取 API 请求头"""
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# 添加 API 密钥
|
||||
api_key_header = self.config.get("api_key_header", "X-API-Key")
|
||||
headers[api_key_header] = self.config["api_key"]
|
||||
|
||||
return headers
|
||||
|
||||
def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
发送 API 请求
|
||||
|
||||
Args:
|
||||
method: HTTP 方法
|
||||
endpoint: API 端点
|
||||
**kwargs: 请求参数
|
||||
|
||||
Returns:
|
||||
响应 JSON 数据
|
||||
|
||||
Raises:
|
||||
EmailServiceError: 请求失败
|
||||
"""
|
||||
url = urljoin(self.config["base_url"], endpoint)
|
||||
|
||||
# 添加默认请求头
|
||||
kwargs.setdefault("headers", {})
|
||||
kwargs["headers"].update(self._get_headers())
|
||||
|
||||
try:
|
||||
# POST 请求禁用自动重定向,手动处理以保持 POST 方法(避免 HTTP→HTTPS 重定向时被转为 GET)
|
||||
if method.upper() == "POST":
|
||||
kwargs["allow_redirects"] = False
|
||||
response = self.http_client.request(method, url, **kwargs)
|
||||
# 处理重定向
|
||||
max_redirects = 5
|
||||
redirect_count = 0
|
||||
while response.status_code in (301, 302, 303, 307, 308) and redirect_count < max_redirects:
|
||||
location = response.headers.get("Location", "")
|
||||
if not location:
|
||||
break
|
||||
import urllib.parse as _urlparse
|
||||
redirect_url = _urlparse.urljoin(url, location)
|
||||
# 307/308 保持 POST,其余(301/302/303)转为 GET
|
||||
if response.status_code in (307, 308):
|
||||
redirect_method = method
|
||||
redirect_kwargs = kwargs
|
||||
else:
|
||||
redirect_method = "GET"
|
||||
# GET 不传 body
|
||||
redirect_kwargs = {k: v for k, v in kwargs.items() if k not in ("json", "data")}
|
||||
response = self.http_client.request(redirect_method, redirect_url, **redirect_kwargs)
|
||||
url = redirect_url
|
||||
redirect_count += 1
|
||||
else:
|
||||
response = self.http_client.request(method, url, **kwargs)
|
||||
|
||||
if response.status_code >= 400:
|
||||
error_msg = f"API 请求失败: {response.status_code}"
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_msg = f"{error_msg} - {error_data}"
|
||||
except:
|
||||
error_msg = f"{error_msg} - {response.text[:200]}"
|
||||
|
||||
self.update_status(False, EmailServiceError(error_msg))
|
||||
raise EmailServiceError(error_msg)
|
||||
|
||||
# 解析响应
|
||||
try:
|
||||
return response.json()
|
||||
except json.JSONDecodeError:
|
||||
return {"raw_response": response.text}
|
||||
|
||||
except Exception as e:
|
||||
self.update_status(False, e)
|
||||
if isinstance(e, EmailServiceError):
|
||||
raise
|
||||
raise EmailServiceError(f"API 请求失败: {method} {endpoint} - {e}")
|
||||
|
||||
def get_config(self, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
获取系统配置
|
||||
|
||||
Args:
|
||||
force_refresh: 是否强制刷新缓存
|
||||
|
||||
Returns:
|
||||
配置信息
|
||||
"""
|
||||
# 检查缓存
|
||||
if not force_refresh and self._cached_config and time.time() - self._last_config_check < 300:
|
||||
return self._cached_config
|
||||
|
||||
try:
|
||||
response = self._make_request("GET", "/api/config")
|
||||
self._cached_config = response
|
||||
self._last_config_check = time.time()
|
||||
self.update_status(True)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.warning(f"获取配置失败: {e}")
|
||||
return {}
|
||||
|
||||
def create_email(self, config: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
创建临时邮箱
|
||||
|
||||
Args:
|
||||
config: 配置参数:
|
||||
- name: 邮箱前缀(可选)
|
||||
- expiryTime: 有效期(毫秒)(可选)
|
||||
- domain: 邮箱域名(可选)
|
||||
|
||||
Returns:
|
||||
包含邮箱信息的字典:
|
||||
- email: 邮箱地址
|
||||
- service_id: 邮箱 ID
|
||||
- id: 邮箱 ID(同 service_id)
|
||||
- expiry: 过期时间信息
|
||||
"""
|
||||
# 获取默认配置
|
||||
sys_config = self.get_config()
|
||||
default_domain = self.config.get("default_domain")
|
||||
if not default_domain and sys_config.get("emailDomains"):
|
||||
# 使用系统配置的第一个域名
|
||||
domains = sys_config["emailDomains"].split(",")
|
||||
default_domain = domains[0].strip() if domains else None
|
||||
|
||||
# 构建请求参数
|
||||
request_config = config or {}
|
||||
create_data = {
|
||||
"name": request_config.get("name", ""),
|
||||
"expiryTime": request_config.get("expiryTime", self.config.get("default_expiry", 3600000)),
|
||||
"domain": request_config.get("domain", default_domain),
|
||||
}
|
||||
|
||||
# 移除空值
|
||||
create_data = {k: v for k, v in create_data.items() if v is not None and v != ""}
|
||||
|
||||
try:
|
||||
response = self._make_request("POST", "/api/emails/generate", json=create_data)
|
||||
|
||||
email = response.get("email", "").strip()
|
||||
email_id = response.get("id", "").strip()
|
||||
|
||||
if not email or not email_id:
|
||||
raise EmailServiceError("API 返回数据不完整")
|
||||
|
||||
email_info = {
|
||||
"email": email,
|
||||
"service_id": email_id,
|
||||
"id": email_id,
|
||||
"created_at": time.time(),
|
||||
"expiry": create_data.get("expiryTime"),
|
||||
"domain": create_data.get("domain"),
|
||||
"raw_response": response,
|
||||
}
|
||||
|
||||
# 缓存邮箱信息
|
||||
self._emails_cache[email_id] = email_info
|
||||
|
||||
logger.info(f"成功创建自定义域名邮箱: {email} (ID: {email_id})")
|
||||
self.update_status(True)
|
||||
return email_info
|
||||
|
||||
except Exception as e:
|
||||
self.update_status(False, e)
|
||||
if isinstance(e, EmailServiceError):
|
||||
raise
|
||||
raise EmailServiceError(f"创建邮箱失败: {e}")
|
||||
|
||||
def get_verification_code(
|
||||
self,
|
||||
email: str,
|
||||
email_id: str = None,
|
||||
timeout: int = 120,
|
||||
pattern: str = OTP_CODE_PATTERN,
|
||||
otp_sent_at: Optional[float] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
从自定义域名邮箱获取验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
email_id: 邮箱 ID(如果不提供,从缓存中查找)
|
||||
timeout: 超时时间(秒)
|
||||
pattern: 验证码正则表达式
|
||||
otp_sent_at: OTP 发送时间戳(自定义域名服务暂不使用此参数)
|
||||
|
||||
Returns:
|
||||
验证码字符串,如果超时或未找到返回 None
|
||||
"""
|
||||
# 查找邮箱 ID
|
||||
target_email_id = email_id
|
||||
if not target_email_id:
|
||||
# 从缓存中查找
|
||||
for eid, info in self._emails_cache.items():
|
||||
if info.get("email") == email:
|
||||
target_email_id = eid
|
||||
break
|
||||
|
||||
if not target_email_id:
|
||||
logger.warning(f"未找到邮箱 {email} 的 ID,无法获取验证码")
|
||||
return None
|
||||
|
||||
logger.info(f"正在从自定义域名邮箱 {email} 获取验证码...")
|
||||
|
||||
start_time = time.time()
|
||||
seen_message_ids = set()
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
# 获取邮件列表
|
||||
response = self._make_request("GET", f"/api/emails/{target_email_id}")
|
||||
|
||||
messages = response.get("messages", [])
|
||||
if not isinstance(messages, list):
|
||||
time.sleep(3)
|
||||
continue
|
||||
|
||||
for message in messages:
|
||||
message_id = message.get("id")
|
||||
if not message_id or message_id in seen_message_ids:
|
||||
continue
|
||||
|
||||
seen_message_ids.add(message_id)
|
||||
|
||||
# 检查是否是目标邮件
|
||||
sender = str(message.get("from_address", "")).lower()
|
||||
subject = str(message.get("subject", ""))
|
||||
|
||||
# 获取邮件内容
|
||||
message_content = self._get_message_content(target_email_id, message_id)
|
||||
if not message_content:
|
||||
continue
|
||||
|
||||
content = f"{sender} {subject} {message_content}"
|
||||
|
||||
# 检查是否是 OpenAI 邮件
|
||||
if "openai" not in sender and "openai" not in content.lower():
|
||||
continue
|
||||
|
||||
# 提取验证码 过滤掉邮箱
|
||||
email_pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
|
||||
match = re.search(pattern, re.sub(email_pattern, "", content))
|
||||
if match:
|
||||
code = match.group(1)
|
||||
logger.info(f"从自定义域名邮箱 {email} 找到验证码: {code}")
|
||||
self.update_status(True)
|
||||
return code
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"检查邮件时出错: {e}")
|
||||
|
||||
# 等待一段时间再检查
|
||||
time.sleep(3)
|
||||
|
||||
logger.warning(f"等待验证码超时: {email}")
|
||||
return None
|
||||
|
||||
def _get_message_content(self, email_id: str, message_id: str) -> Optional[str]:
|
||||
"""获取邮件内容"""
|
||||
try:
|
||||
response = self._make_request("GET", f"/api/emails/{email_id}/{message_id}")
|
||||
message = response.get("message", {})
|
||||
|
||||
# 优先使用纯文本内容,其次使用 HTML 内容
|
||||
content = message.get("content", "")
|
||||
if not content:
|
||||
html = message.get("html", "")
|
||||
if html:
|
||||
# 简单去除 HTML 标签
|
||||
content = re.sub(r"<[^>]+>", " ", html)
|
||||
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.debug(f"获取邮件内容失败: {e}")
|
||||
return None
|
||||
|
||||
def list_emails(self, cursor: str = None, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
列出所有邮箱
|
||||
|
||||
Args:
|
||||
cursor: 分页游标
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
邮箱列表
|
||||
"""
|
||||
params = {}
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
|
||||
try:
|
||||
response = self._make_request("GET", "/api/emails", params=params)
|
||||
emails = response.get("emails", [])
|
||||
|
||||
# 更新缓存
|
||||
for email_info in emails:
|
||||
email_id = email_info.get("id")
|
||||
if email_id:
|
||||
self._emails_cache[email_id] = email_info
|
||||
|
||||
self.update_status(True)
|
||||
return emails
|
||||
except Exception as e:
|
||||
logger.warning(f"列出邮箱失败: {e}")
|
||||
self.update_status(False, e)
|
||||
return []
|
||||
|
||||
def delete_email(self, email_id: str) -> bool:
|
||||
"""
|
||||
删除邮箱
|
||||
|
||||
Args:
|
||||
email_id: 邮箱 ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
response = self._make_request("DELETE", f"/api/emails/{email_id}")
|
||||
success = response.get("success", False)
|
||||
|
||||
if success:
|
||||
# 从缓存中移除
|
||||
self._emails_cache.pop(email_id, None)
|
||||
logger.info(f"成功删除邮箱: {email_id}")
|
||||
else:
|
||||
logger.warning(f"删除邮箱失败: {email_id}")
|
||||
|
||||
self.update_status(success)
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除邮箱失败: {email_id} - {e}")
|
||||
self.update_status(False, e)
|
||||
return False
|
||||
|
||||
def check_health(self) -> bool:
|
||||
"""检查自定义域名邮箱服务是否可用"""
|
||||
try:
|
||||
# 尝试获取配置
|
||||
config = self.get_config(force_refresh=True)
|
||||
if config:
|
||||
logger.debug(f"自定义域名邮箱服务健康检查通过,配置: {config.get('defaultRole', 'N/A')}")
|
||||
self.update_status(True)
|
||||
return True
|
||||
else:
|
||||
logger.warning("自定义域名邮箱服务健康检查失败:获取配置为空")
|
||||
self.update_status(False, EmailServiceError("获取配置为空"))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"自定义域名邮箱服务健康检查失败: {e}")
|
||||
self.update_status(False, e)
|
||||
return False
|
||||
|
||||
def get_email_messages(self, email_id: str, cursor: str = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取邮箱中的邮件列表
|
||||
|
||||
Args:
|
||||
email_id: 邮箱 ID
|
||||
cursor: 分页游标
|
||||
|
||||
Returns:
|
||||
邮件列表
|
||||
"""
|
||||
params = {}
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
|
||||
try:
|
||||
response = self._make_request("GET", f"/api/emails/{email_id}", params=params)
|
||||
messages = response.get("messages", [])
|
||||
self.update_status(True)
|
||||
return messages
|
||||
except Exception as e:
|
||||
logger.error(f"获取邮件列表失败: {email_id} - {e}")
|
||||
self.update_status(False, e)
|
||||
return []
|
||||
|
||||
def get_message_detail(self, email_id: str, message_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取邮件详情
|
||||
|
||||
Args:
|
||||
email_id: 邮箱 ID
|
||||
message_id: 邮件 ID
|
||||
|
||||
Returns:
|
||||
邮件详情
|
||||
"""
|
||||
try:
|
||||
response = self._make_request("GET", f"/api/emails/{email_id}/{message_id}")
|
||||
message = response.get("message")
|
||||
self.update_status(True)
|
||||
return message
|
||||
except Exception as e:
|
||||
logger.error(f"获取邮件详情失败: {email_id}/{message_id} - {e}")
|
||||
self.update_status(False, e)
|
||||
return None
|
||||
|
||||
def create_email_share(self, email_id: str, expires_in: int = 86400000) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
创建邮箱分享链接
|
||||
|
||||
Args:
|
||||
email_id: 邮箱 ID
|
||||
expires_in: 有效期(毫秒)
|
||||
|
||||
Returns:
|
||||
分享信息
|
||||
"""
|
||||
try:
|
||||
response = self._make_request(
|
||||
"POST",
|
||||
f"/api/emails/{email_id}/share",
|
||||
json={"expiresIn": expires_in}
|
||||
)
|
||||
self.update_status(True)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"创建邮箱分享链接失败: {email_id} - {e}")
|
||||
self.update_status(False, e)
|
||||
return None
|
||||
|
||||
def create_message_share(
|
||||
self,
|
||||
email_id: str,
|
||||
message_id: str,
|
||||
expires_in: int = 86400000
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
创建邮件分享链接
|
||||
|
||||
Args:
|
||||
email_id: 邮箱 ID
|
||||
message_id: 邮件 ID
|
||||
expires_in: 有效期(毫秒)
|
||||
|
||||
Returns:
|
||||
分享信息
|
||||
"""
|
||||
try:
|
||||
response = self._make_request(
|
||||
"POST",
|
||||
f"/api/emails/{email_id}/messages/{message_id}/share",
|
||||
json={"expiresIn": expires_in}
|
||||
)
|
||||
self.update_status(True)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"创建邮件分享链接失败: {email_id}/{message_id} - {e}")
|
||||
self.update_status(False, e)
|
||||
return None
|
||||
|
||||
def get_service_info(self) -> Dict[str, Any]:
|
||||
"""获取服务信息"""
|
||||
config = self.get_config()
|
||||
return {
|
||||
"service_type": self.service_type.value,
|
||||
"name": self.name,
|
||||
"base_url": self.config["base_url"],
|
||||
"default_domain": self.config.get("default_domain"),
|
||||
"system_config": config,
|
||||
"cached_emails_count": len(self._emails_cache),
|
||||
"status": self.status.value,
|
||||
}
|
||||
Reference in New Issue
Block a user