feat: 新增 LuckMail 平台及自动分配邮箱渠道支持

- 引入 LuckMail 核心 SDK
- 封装 \LuckMailMailbox\ 适配器接入注册工厂
- 支持智能匹配各类项目的子渠道编码
- 完善前端 UI 配置选项
This commit is contained in:
流云
2026-03-28 22:02:22 +08:00
parent b96e8a5bbd
commit cba60a2aa3
12 changed files with 2886 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ CONFIG_KEYS = [
"moemail_api_url",
"mail_provider",
"cfworker_api_url", "cfworker_admin_token", "cfworker_domain", "cfworker_fingerprint",
"luckmail_base_url", "luckmail_api_key", "luckmail_email_type",
"cpa_api_url", "cpa_api_key",
"team_manager_url", "team_manager_key",
"cliproxyapi_management_key",

View File

@@ -104,9 +104,12 @@ def _run_register(task_id: str, req: RegisterTaskRequest):
PlatformCls = get(req.platform)
def _build_mailbox(proxy: Optional[str]):
from core.config_store import config_store
merged_extra = config_store.get_all().copy()
merged_extra.update({k: v for k, v in req.extra.items() if v is not None and v != ""})
return create_mailbox(
provider=req.extra.get("mail_provider", "laoudo"),
extra=req.extra,
provider=merged_extra.get("mail_provider", "laoudo"),
extra=merged_extra,
proxy=proxy,
)
@@ -126,11 +129,15 @@ def _run_register(task_id: str, req: RegisterTaskRequest):
_log(task_id, f"{i+1} 个账号启动前延迟 {wait_seconds:g}")
time.sleep(wait_seconds)
next_start_time = time.time() + req.register_delay_seconds
from core.config_store import config_store
merged_extra = config_store.get_all().copy()
merged_extra.update({k: v for k, v in req.extra.items() if v is not None and v != ""})
_config = RegisterConfig(
executor_type=req.executor_type,
captcha_solver=req.captcha_solver,
proxy=_proxy,
extra=req.extra,
extra=merged_extra,
)
_mailbox = _build_mailbox(_proxy)
_platform = PlatformCls(config=_config, mailbox=_mailbox)
@@ -197,6 +204,21 @@ def create_register_task(
req: RegisterTaskRequest,
background_tasks: BackgroundTasks,
):
mail_provider = req.extra.get("mail_provider")
if mail_provider == "luckmail":
platform = req.platform
if platform in ("tavily", "openblocklabs"):
raise HTTPException(400, f"LuckMail 渠道暂时不支持 {platform} 项目注册")
mapping = {
"trae": "trae",
"cursor": "cursor",
"grok": "grok",
"kiro": "kiro",
"chatgpt": "openai"
}
req.extra["luckmail_project_code"] = mapping.get(platform, platform)
task_id = f"task_{int(time.time()*1000)}"
with _tasks_lock:
_tasks[task_id] = {"id": task_id, "status": "pending",

View File

@@ -118,6 +118,13 @@ def create_mailbox(provider: str, extra: dict = None, proxy: str = None) -> 'Bas
fingerprint=extra.get("cfworker_fingerprint", ""),
proxy=proxy,
)
elif provider == "luckmail":
return LuckMailMailbox(
base_url=extra.get("luckmail_base_url") or "https://mails.luckyous.com/",
api_key=extra.get("luckmail_api_key", ""),
project_code=extra.get("luckmail_project_code", ""),
email_type=extra.get("luckmail_email_type", ""),
)
else: # laoudo
return LaoudoMailbox(
auth_token=extra.get("laoudo_auth", ""),
@@ -610,6 +617,72 @@ class MoeMailMailbox(BaseMailbox):
raise TimeoutError(f"等待验证码超时 ({timeout}s)")
class LuckMailMailbox(BaseMailbox):
"""LuckMail 付费接码平台 - 通过 SDK 创建订单并获取验证码"""
def __init__(self, base_url: str, api_key: str,
project_code: str = "", email_type: str = ""):
if not base_url or not api_key:
raise RuntimeError(
"LuckMail 未配置:请在全局设置中填写 luckmail_base_url 和 luckmail_api_key"
)
from .luckmail import LuckMailClient
self._client = LuckMailClient(
base_url=base_url,
api_key=api_key,
)
self._project_code = project_code
self._email_type = email_type or None
self._order_no = None
def get_email(self) -> MailboxAccount:
try:
body = {"project_code": self._project_code}
if self._email_type:
body["email_type"] = self._email_type
order = self._client.user._sync_create_order(body)
except Exception as e:
raise RuntimeError(f"LuckMail 创建订单失败: {e}") from e
self._order_no = order.order_no
email = order.email_address
self._log(f"[LuckMail] 订单 {order.order_no} 分配邮箱: {email}")
self._log(f"[LuckMail] 超时时间: {order.expired_at}")
return MailboxAccount(email=email, account_id=order.order_no)
def get_current_ids(self, account: MailboxAccount) -> set:
# LuckMail 由服务端管理邮件,本地无需维护 ID 集合
return set()
def wait_for_code(self, account: MailboxAccount, keyword: str = "",
timeout: int = 120, before_ids: set = None,
code_pattern: str = None, **kwargs) -> str:
order_no = account.account_id or self._order_no
if not order_no:
raise RuntimeError("LuckMail 未创建订单,无法等待验证码")
def on_poll(result):
self._log(f"[LuckMail] 轮询中... 状态: {result.status}")
try:
code_result = self._client.user._sync_wait_for_code(
order_no=order_no,
timeout=timeout,
interval=3.0,
on_poll=on_poll,
)
except Exception as e:
raise TimeoutError(f"LuckMail 等待验证码失败: {e}") from e
if code_result.status == "success" and code_result.verification_code:
code = code_result.verification_code
self._log(f"[LuckMail] 收到验证码: {code}")
return code
raise TimeoutError(
f"LuckMail 等待验证码超时 ({timeout}s),最终状态: {code_result.status}"
)
class FreemailMailbox(BaseMailbox):
"""
Freemail 自建邮箱服务(基于 Cloudflare Worker

63
core/luckmail/__init__.py Normal file
View File

@@ -0,0 +1,63 @@
"""
LuckMailSdk - Python SDK for LuckMail Email System
支持同步/异步双模式,智能识别调用上下文自动切换
"""
from .client import LuckMailClient
from .user import UserAPI
from .supplier import SupplierAPI
from .exceptions import (
LuckMailError,
AuthError,
APIError,
NetworkError,
TimeoutError,
)
from .models import (
UserInfo,
EmailItem,
ProjectItem,
OrderInfo,
OrderCode,
PurchaseItem,
TagItem,
TokenCode,
TokenAliveResult,
TokenMailItem,
TokenMailList,
TokenMailDetail,
AppealInfo,
SupplierProfile,
SupplierEmailItem,
AppealItem,
DashboardSummary,
)
__version__ = "1.2.1"
__all__ = [
"LuckMailClient",
"UserAPI",
"SupplierAPI",
"LuckMailError",
"AuthError",
"APIError",
"NetworkError",
"TimeoutError",
"UserInfo",
"EmailItem",
"ProjectItem",
"OrderInfo",
"OrderCode",
"PurchaseItem",
"TagItem",
"TokenCode",
"TokenAliveResult",
"TokenMailItem",
"TokenMailList",
"TokenMailDetail",
"AppealInfo",
"SupplierProfile",
"SupplierEmailItem",
"AppealItem",
"DashboardSummary",
]

224
core/luckmail/client.py Normal file
View File

@@ -0,0 +1,224 @@
"""
LuckMailClient - 主客户端入口
整合用户端和供应商端 API提供统一的访问入口。
支持同步/异步双模式,智能识别调用上下文。
"""
from typing import Optional
from .http_client import LuckMailHttpClient
from .user import UserAPI
from .supplier import SupplierAPI
class LuckMailClient:
"""
LuckMail SDK 主客户端
提供用户端user和供应商端supplier两套 API 访问入口。
所有 API 方法均支持同步/异步双模式,根据调用上下文自动识别,
无需手动区分,大幅降低接入成本。
Args:
base_url: API 基础 URL如 https://your-domain.com
api_key: API Key在平台「个人设置」页面生成
api_secret: API Secret可选用于 HMAC 签名验证,安全性更高)
timeout: 请求超时时间(秒),默认 30
use_hmac: 是否使用 HMAC 签名验证,默认 False
用户端示例(同步)::
from luckmail import LuckMailClient
client = LuckMailClient(
base_url="https://your-domain.com",
api_key="your_api_key_here"
)
# 查询余额
balance = client.user.get_balance()
print(f"余额: {balance}")
# 接码(一行搞定)
code = client.user.create_and_wait('twitter')
print(f"验证码: {code.verification_code}")
用户端示例(异步)::
import asyncio
from luckmail import LuckMailClient
client = LuckMailClient(
base_url="https://your-domain.com",
api_key="your_api_key_here"
)
async def main():
balance = await client.user.get_balance()
print(f"余额: {balance}")
code = await client.user.create_and_wait('twitter')
print(f"验证码: {code.verification_code}")
asyncio.run(main())
供应商端示例::
# 查看数据看板
summary = client.supplier.get_dashboard()
print(f"今日接码: {summary.today_assigned}")
# 处理申述
client.supplier.reply_appeal("APL001", result=1, reply="同意退款")
"""
def __init__(
self,
base_url: str,
api_key: str,
api_secret: Optional[str] = None,
timeout: float = 30.0,
use_hmac: bool = False,
):
self._http = LuckMailHttpClient(
base_url=base_url,
api_key=api_key,
api_secret=api_secret,
timeout=timeout,
use_hmac=use_hmac,
)
# 用户端 API
self.user = UserAPI(self._http)
# 供应商端 API
self.supplier = SupplierAPI(self._http)
# ===== 快捷方法(用户端常用操作)=====
def create_and_wait(
self,
project_code: str,
email_type: Optional[str] = None,
domain: Optional[str] = None,
specified_email: Optional[str] = None,
variant_mode: Optional[str] = None,
timeout: int = 300,
interval: float = 3.0,
on_poll=None,
):
"""
创建接码订单并等待验证码(一站式方法)
自动创建订单并轮询等待验证码,智能识别同步/异步上下文。
Args:
project_code: 项目编码,如 'twitter', 'facebook'
email_type: 邮箱类型(可选)
domain: 指定域名(可选)
specified_email: 指定邮箱(可选)
variant_mode: 谷歌变种模式(可选,仅 email_type=google_variant 时有效): dot / plus / mixed / all
timeout: 最大等待时间(秒),默认 300
interval: 轮询间隔(秒),默认 3.0
on_poll: 每次轮询的回调函数(可选)
Returns:
OrderCode: 验证码结果
同步示例::
result = client.create_and_wait('twitter')
if result.status == 'success':
print(f"✅ 验证码: {result.verification_code}")
print(f"📧 来自: {result.mail_from}")
else:
print(f"❌ 接码失败: {result.status}")
异步示例::
result = await client.create_and_wait('twitter', email_type='ms_graph')
if result.status == 'success':
print(f"✅ 验证码: {result.verification_code}")
带进度回调的示例::
def on_poll(code_result):
print(f"轮询中... 状态: {code_result.status}")
result = client.create_and_wait('twitter', on_poll=on_poll)
"""
from .http_client import _is_async_context
if _is_async_context():
return self._async_create_and_wait(
project_code, email_type, domain, specified_email, variant_mode,
timeout, interval, on_poll
)
return self._sync_create_and_wait(
project_code, email_type, domain, specified_email, variant_mode,
timeout, interval, on_poll
)
async def _async_create_and_wait(
self, project_code, email_type, domain, specified_email, variant_mode,
timeout, interval, on_poll
):
"""异步创建并等待验证码"""
body = {"project_code": project_code}
if email_type:
body["email_type"] = email_type
if domain:
body["domain"] = domain
if specified_email:
body["specified_email"] = specified_email
if variant_mode:
body["variant_mode"] = variant_mode
order = await self.user._async_create_order(body)
return await self.user._async_wait_for_code(
order.order_no, timeout, interval, on_poll
)
def _sync_create_and_wait(
self, project_code, email_type, domain, specified_email, variant_mode,
timeout, interval, on_poll
):
"""同步创建并等待验证码"""
body = {"project_code": project_code}
if email_type:
body["email_type"] = email_type
if domain:
body["domain"] = domain
if specified_email:
body["specified_email"] = specified_email
if variant_mode:
body["variant_mode"] = variant_mode
order = self.user._sync_create_order(body)
return self.user._sync_wait_for_code(
order.order_no, timeout, interval, on_poll
)
def close(self):
"""关闭客户端(同步)"""
self._http.close()
async def aclose(self):
"""关闭客户端(异步)"""
await self._http.aclose()
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.aclose()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def __repr__(self) -> str:
return (
f"LuckMailClient(base_url={self._http.base_url!r}, "
f"api_key={self._http.api_key[:8]}...)"
)

View File

@@ -0,0 +1,35 @@
"""
LuckMailSdk 异常类定义
"""
class LuckMailError(Exception):
"""LuckMail SDK 基础异常"""
pass
class AuthError(LuckMailError):
"""鉴权失败异常"""
def __init__(self, message: str = "Authentication failed"):
super().__init__(message)
class APIError(LuckMailError):
"""API 调用异常"""
def __init__(self, code: int, message: str, data=None):
self.code = code
self.message = message
self.data = data
super().__init__(f"API Error [{code}]: {message}")
class NetworkError(LuckMailError):
"""网络请求异常"""
def __init__(self, message: str = "Network error occurred"):
super().__init__(message)
class TimeoutError(LuckMailError):
"""超时异常"""
def __init__(self, message: str = "Request timed out"):
super().__init__(message)

View File

@@ -0,0 +1,355 @@
"""
核心 HTTP 客户端(基于 curl_cffi
支持同步/异步双模式,智能识别调用上下文自动切换
支持 TLS 指纹模拟,避免被目标网站识别为机器人。
"""
import asyncio
import hashlib
import hmac
import json
import secrets
import threading
import time
from typing import Any, Dict, Optional
from urllib.parse import urlencode
from curl_cffi import requests as curl_requests
from .exceptions import APIError, AuthError, NetworkError
def _is_async_context() -> bool:
"""检测当前是否处于异步上下文(事件循环正在运行)"""
try:
loop = asyncio.get_event_loop()
return loop.is_running()
except RuntimeError:
return False
def _generate_hmac_signature(api_secret: str, api_key: str, timestamp: str, nonce: str) -> str:
"""生成 HMAC-SHA256 签名
签名内容api_key + timestamp + nonce使用 api_secret 作为密钥
"""
message = f"{api_key}{timestamp}{nonce}"
signature = hmac.new(
api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256
).hexdigest()
return signature
class _SyncRunner:
"""同步运行异步函数的工具类"""
_lock = threading.Lock()
_loop: Optional[asyncio.AbstractEventLoop] = None
_thread: Optional[threading.Thread] = None
@classmethod
def _ensure_loop(cls):
"""确保后台事件循环正在运行"""
with cls._lock:
if cls._loop is None or not cls._loop.is_running():
cls._loop = asyncio.new_event_loop()
cls._thread = threading.Thread(
target=cls._loop.run_forever,
daemon=True,
name="LuckMailSdk-EventLoop"
)
cls._thread.start()
@classmethod
def run(cls, coro) -> Any:
"""在后台事件循环中同步运行协程"""
cls._ensure_loop()
future = asyncio.run_coroutine_threadsafe(coro, cls._loop)
return future.result()
class LuckMailHttpClient:
"""
LuckMail HTTP 客户端(基于 curl_cffi
使用 curl_cffi 作为底层 HTTP 库,支持 TLS 指纹模拟。
自动识别调用上下文(同步/异步),提供统一的请求接口。
Args:
base_url: API 基础 URL如 https://your-domain.com
api_key: API Key必填
api_secret: API Secret可选用于 HMAC 签名验证,安全性更高)
timeout: 请求超时时间(秒),默认 30
use_hmac: 是否使用 HMAC 签名验证,默认 False使用时需提供 api_secret
impersonate: 浏览器指纹模拟,默认 "chrome"(可选 "firefox""safari" 等)
"""
def __init__(
self,
base_url: str,
api_key: str,
api_secret: Optional[str] = None,
timeout: float = 30.0,
use_hmac: bool = False,
impersonate: str = "chrome",
):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.api_secret = api_secret
self.timeout = timeout
self.use_hmac = use_hmac and api_secret is not None
self.impersonate = impersonate
# 同步 Session延迟初始化
self._sync_session: Optional[curl_requests.Session] = None
# 异步 Session延迟初始化
self._async_session: Optional[Any] = None
def _get_sync_session(self) -> curl_requests.Session:
"""获取或创建同步 Session"""
if self._sync_session is None:
self._sync_session = curl_requests.Session(
impersonate=self.impersonate,
timeout=self.timeout,
)
return self._sync_session
async def _get_async_session(self):
"""获取或创建异步 Session"""
if self._async_session is None:
self._async_session = curl_requests.AsyncSession(
impersonate=self.impersonate,
timeout=self.timeout,
)
return self._async_session
def _build_headers(self) -> Dict[str, str]:
"""构建请求头(含鉴权信息)"""
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if self.use_hmac and self.api_secret:
# HMAC 签名模式
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
signature = _generate_hmac_signature(
self.api_secret, self.api_key, timestamp, nonce
)
headers["X-API-Key"] = self.api_key
headers["X-Timestamp"] = timestamp
headers["X-Nonce"] = nonce
headers["X-Signature"] = signature
elif self.api_key:
# 普通 API Key 模式(推荐)
headers["X-API-Key"] = self.api_key
return headers
def _build_url(self, path: str, params: Optional[Dict] = None) -> str:
"""构建完整 URL"""
url = f"{self.base_url}{path}"
if params:
# 过滤 None 值
filtered = {k: v for k, v in params.items() if v is not None}
if filtered:
url = f"{url}?{urlencode(filtered)}"
return url
def _parse_response(self, status_code: int, content: bytes) -> Any:
"""解析响应数据"""
try:
data = json.loads(content)
except (json.JSONDecodeError, UnicodeDecodeError):
# 非 JSON 响应(如文件流)直接返回字节内容
return content
if not isinstance(data, dict):
return data
code = data.get("code", -1)
message = data.get("message", "Unknown error")
if code != 0:
if status_code == 401 or code == 401:
raise AuthError(message)
raise APIError(code, message, data.get("data"))
return data.get("data")
# ===================== 异步方法 =====================
async def _async_request(
self,
method: str,
path: str,
params: Optional[Dict] = None,
json_data: Optional[Dict] = None,
) -> Any:
"""异步 HTTP 请求"""
session = await self._get_async_session()
headers = self._build_headers()
url = self._build_url(path, params)
try:
if method.upper() == "GET":
response = await session.get(url, headers=headers)
elif method.upper() == "POST":
response = await session.post(
url, headers=headers, json=json_data or {}
)
elif method.upper() == "PUT":
response = await session.put(
url, headers=headers, json=json_data or {}
)
elif method.upper() == "DELETE":
response = await session.delete(url, headers=headers)
else:
raise ValueError(f"不支持的 HTTP 方法: {method}")
return self._parse_response(response.status_code, response.content)
except (AuthError, APIError):
raise
except Exception as e:
err_msg = str(e).lower()
if "timeout" in err_msg:
from .exceptions import TimeoutError as LuckTimeoutError
raise LuckTimeoutError(f"请求超时: {path}") from e
raise NetworkError(f"请求失败: {e}") from e
async def _async_get_stream(self, path: str, params: Optional[Dict] = None) -> bytes:
"""异步获取流式响应(文件下载等)"""
session = await self._get_async_session()
headers = self._build_headers()
url = self._build_url(path, params)
try:
response = await session.get(url, headers=headers)
return response.content
except Exception as e:
err_msg = str(e).lower()
if "timeout" in err_msg:
from .exceptions import TimeoutError as LuckTimeoutError
raise LuckTimeoutError(f"请求超时: {path}") from e
raise NetworkError(f"网络错误: {e}") from e
async def aclose(self):
"""关闭异步客户端"""
if self._async_session is not None:
await self._async_session.close()
self._async_session = None
# ===================== 同步方法 =====================
def _sync_request(
self,
method: str,
path: str,
params: Optional[Dict] = None,
json_data: Optional[Dict] = None,
) -> Any:
"""同步 HTTP 请求(使用 curl_cffi"""
session = self._get_sync_session()
headers = self._build_headers()
url = self._build_url(path, params)
try:
if method.upper() == "GET":
response = session.get(url, headers=headers)
elif method.upper() == "POST":
response = session.post(
url, headers=headers, json=json_data or {}
)
elif method.upper() == "PUT":
response = session.put(
url, headers=headers, json=json_data or {}
)
elif method.upper() == "DELETE":
response = session.delete(url, headers=headers)
else:
raise ValueError(f"不支持的 HTTP 方法: {method}")
return self._parse_response(response.status_code, response.content)
except (AuthError, APIError):
raise
except Exception as e:
err_msg = str(e).lower()
if "timeout" in err_msg:
from .exceptions import TimeoutError as LuckTimeoutError
raise LuckTimeoutError(f"请求超时: {path}") from e
raise NetworkError(f"请求失败: {e}") from e
def _sync_get_stream(self, path: str, params: Optional[Dict] = None) -> bytes:
"""同步获取流式响应"""
session = self._get_sync_session()
headers = self._build_headers()
url = self._build_url(path, params)
try:
response = session.get(url, headers=headers)
return response.content
except Exception as e:
err_msg = str(e).lower()
if "timeout" in err_msg:
from .exceptions import TimeoutError as LuckTimeoutError
raise LuckTimeoutError(f"请求超时: {path}") from e
raise NetworkError(f"网络错误: {e}") from e
# ===================== 统一接口(智能识别同步/异步)=====================
def request(
self,
method: str,
path: str,
params: Optional[Dict] = None,
json_data: Optional[Dict] = None,
):
"""
统一请求接口,智能识别调用上下文:
- 在 async 函数中调用:自动返回协程,需要 await
- 在普通函数中调用:直接返回结果
使用示例:
# 同步调用
result = client.request("GET", "/api/v1/openapi/user/info")
# 异步调用
result = await client.request("GET", "/api/v1/openapi/user/info")
"""
if _is_async_context():
return self._async_request(method, path, params=params, json_data=json_data)
else:
return self._sync_request(method, path, params=params, json_data=json_data)
def get_stream(self, path: str, params: Optional[Dict] = None):
"""
流式 GET 请求(用于文件下载),智能识别同步/异步上下文
"""
if _is_async_context():
return self._async_get_stream(path, params=params)
else:
return self._sync_get_stream(path, params=params)
def close(self):
"""关闭同步客户端资源"""
if self._sync_session is not None:
self._sync_session.close()
self._sync_session = None
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.aclose()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()

251
core/luckmail/models.py Normal file
View File

@@ -0,0 +1,251 @@
"""
数据模型定义
"""
from dataclasses import dataclass, field
from typing import Optional, List, Any
@dataclass
class UserInfo:
"""用户信息"""
id: int
username: str
email: str
balance: str
status: int
api_email_enabled: int = 0
api_email_price: str = "0.0000"
@dataclass
class EmailItem:
"""邮箱列表项"""
id: int
address: str
type: str
status: int
domain: str
total_used: int = 0
success_count: int = 0
fail_count: int = 0
@dataclass
class ProjectPrice:
"""项目定价"""
email_type: str
code_price: str
buy_price: str
@dataclass
class ProjectItem:
"""项目信息"""
id: int
name: str
code: str
email_types: List[str]
timeout_seconds: int
warranty_hours: int
daily_limit: int
description: str
prices: List[ProjectPrice] = field(default_factory=list)
@dataclass
class OrderInfo:
"""订单信息(创建后)"""
order_no: str
email_address: str
project: str
price: str
timeout_seconds: int
expired_at: str
@dataclass
class OrderCode:
"""订单验证码查询结果"""
order_no: str
status: str # pending / success / timeout / cancelled
verification_code: Optional[str] = None
mail_from: Optional[str] = None
mail_subject: Optional[str] = None
mail_body_html: Optional[str] = None
@dataclass
class PurchaseItem:
"""已购邮箱"""
id: int
email_address: str
token: str
project_name: str
price: str
status: int = 1
tag_id: int = 0
tag_name: str = ""
user_disabled: int = 0
warranty_hours: int = 0
warranty_until: Optional[str] = None
created_at: Optional[str] = None
@dataclass
class TokenCode:
"""Token 查询验证码结果"""
email_address: str
project: str
has_new_mail: bool
verification_code: Optional[str] = None
mail: Optional[dict] = None
@dataclass
class TokenAliveResult:
"""Token 测活结果"""
email_address: str
project: str
alive: bool
status: str
message: str = ""
mail_count: int = 0
@dataclass
class TokenMailItem:
"""Token 邮件列表项"""
message_id: str
from_addr: str = ""
subject: str = ""
body: str = ""
html_body: str = ""
received_at: str = ""
@dataclass
class TokenMailList:
"""Token 邮件列表结果"""
email_address: str
project: str
warranty_until: str = ""
mails: List[TokenMailItem] = field(default_factory=list)
@dataclass
class TokenMailDetail:
"""Token 邮件详情结果"""
message_id: str
from_addr: str = ""
to: str = ""
subject: str = ""
body_text: str = ""
body_html: str = ""
received_at: str = ""
verification_code: str = ""
@dataclass
class AppealInfo:
"""申述信息"""
appeal_no: str
appeal_type: int
reason: str
description: str
status: int
created_at: Optional[str] = None
@dataclass
class TagItem:
"""邮箱标签"""
id: int
name: str
remark: str = ""
limit_type: int = 0 # 0=不下发 1=可下发
purchase_count: int = 0
created_at: Optional[str] = None
@dataclass
class PageResult:
"""分页结果"""
list: List[Any]
total: int
page: int
page_size: int
# ===== 供应商模型 =====
@dataclass
class SupplierProfile:
"""供应商个人信息"""
id: int
username: str
email: str
balance: str
frozen_balance: str
code_commission_rate: str
buy_commission_rate: str
status: int
@dataclass
class SupplierEmailItem:
"""供应商邮箱列表项"""
id: int
address: str
type: str
status: int
domain: str
total_used: int = 0
success_count: int = 0
fail_count: int = 0
is_short_term: int = 0
@dataclass
class AppealItem:
"""申述列表项(供应商端)"""
id: int
appeal_no: str
order_no: str
reason: str
status: int
created_at: str
@dataclass
class AppealDetail:
"""申述详情"""
appeal_no: str
order_no: str
reason: str
status: int
supplier_reply: Optional[str] = None
created_at: Optional[str] = None
@dataclass
class ImportResult:
"""导入邮箱结果"""
success: int
duplicate: int
failed: int
@dataclass
class DashboardSummary:
"""供应商数据看板"""
total_emails: int
active_emails: int
total_assigned: int
total_success: int
success_rate: float
total_commission: str
available_balance: str
today_assigned: int
today_success: int
today_commission: str
email_category: dict = field(default_factory=dict)

463
core/luckmail/supplier.py Normal file
View File

@@ -0,0 +1,463 @@
"""
供应商端 API 接口
Base URL: {base_url}/api/v1/openapi/supplier
所有方法均支持同步/异步双模式,根据调用上下文自动识别。
"""
from typing import Any, Dict, List, Optional
from .http_client import LuckMailHttpClient, _is_async_context
from .models import (
AppealDetail,
AppealItem,
DashboardSummary,
ImportResult,
PageResult,
SupplierEmailItem,
SupplierProfile,
)
_SUPPLIER_PREFIX = "/api/v1/openapi/supplier"
def _parse_supplier_profile(data: dict) -> SupplierProfile:
return SupplierProfile(
id=data.get("id", 0),
username=data.get("username", ""),
email=data.get("email", ""),
balance=data.get("balance", "0.0000"),
frozen_balance=data.get("frozen_balance", "0.0000"),
code_commission_rate=data.get("code_commission_rate", "0.0000"),
buy_commission_rate=data.get("buy_commission_rate", "0.0000"),
status=data.get("status", 1),
)
def _parse_supplier_email(data: dict) -> SupplierEmailItem:
return SupplierEmailItem(
id=data.get("id", 0),
address=data.get("address", ""),
type=data.get("type", ""),
status=data.get("status", 1),
domain=data.get("domain", ""),
total_used=data.get("total_used", 0),
success_count=data.get("success_count", 0),
fail_count=data.get("fail_count", 0),
is_short_term=data.get("is_short_term", 0),
)
def _parse_appeal_item(data: dict) -> AppealItem:
return AppealItem(
id=data.get("id", 0),
appeal_no=data.get("appeal_no", ""),
order_no=data.get("order_no", ""),
reason=data.get("reason", ""),
status=data.get("status", 1),
created_at=data.get("created_at", ""),
)
def _parse_appeal_detail(data: dict) -> AppealDetail:
return AppealDetail(
appeal_no=data.get("appeal_no", ""),
order_no=data.get("order_no", ""),
reason=data.get("reason", ""),
status=data.get("status", 1),
supplier_reply=data.get("supplier_reply"),
created_at=data.get("created_at"),
)
def _parse_page_result(data: dict, item_parser=None) -> PageResult:
items = data.get("list", [])
if item_parser:
items = [item_parser(i) for i in items]
return PageResult(
list=items,
total=data.get("total", 0),
page=data.get("page", 1),
page_size=data.get("page_size", 20),
)
class SupplierAPI:
"""
供应商端 API 接口集合
所有方法智能支持同步/异步调用:
- 在 async 函数中await client.supplier.get_profile()
- 在普通函数中client.supplier.get_profile()
Args:
http_client: LuckMailHttpClient 实例
"""
def __init__(self, http_client: LuckMailHttpClient):
self._client = http_client
def _path(self, path: str) -> str:
"""拼接供应商 API 路径"""
return f"{_SUPPLIER_PREFIX}{path}"
# ===== 供应商信息 =====
def get_profile(self):
"""
获取供应商个人信息
Returns:
SupplierProfile: 供应商信息(余额、佣金率等)
示例::
profile = client.supplier.get_profile()
print(profile.username, profile.balance)
"""
if _is_async_context():
return self._async_get_profile()
return self._sync_get_profile()
async def _async_get_profile(self) -> SupplierProfile:
data = await self._client._async_request("GET", self._path("/profile"))
return _parse_supplier_profile(data)
def _sync_get_profile(self) -> SupplierProfile:
data = self._client._sync_request("GET", self._path("/profile"))
return _parse_supplier_profile(data)
# ===== 邮箱管理 =====
def get_emails(
self,
page: int = 1,
page_size: int = 20,
keyword: Optional[str] = None,
email_type: Optional[str] = None,
is_short_term: Optional[int] = None,
status: Optional[int] = None,
):
"""
获取邮箱列表(分页)
Args:
page: 页码,默认 1
page_size: 每页数量,默认 20
keyword: 邮箱地址关键词搜索
email_type: 邮箱类型ms_graph / ms_imap / google_variant / self_built
is_short_term: 仅微软邮箱有效0=长效 1=短效
status: 状态1=正常 2=异常 4=禁用
Returns:
PageResult: 分页结果list 为 SupplierEmailItem 列表
示例::
result = client.supplier.get_emails(email_type='ms_graph', is_short_term=0)
print(f"长效 MS Graph 邮箱: {result.total}")
"""
params = {
"page": page,
"page_size": page_size,
"keyword": keyword,
"type": email_type,
"is_short_term": is_short_term,
"status": status,
}
if _is_async_context():
return self._async_get_emails(params)
return self._sync_get_emails(params)
async def _async_get_emails(self, params: dict) -> PageResult:
data = await self._client._async_request("GET", self._path("/emails"), params=params)
return _parse_page_result(data, _parse_supplier_email)
def _sync_get_emails(self, params: dict) -> PageResult:
data = self._client._sync_request("GET", self._path("/emails"), params=params)
return _parse_page_result(data, _parse_supplier_email)
def import_emails(
self,
email_type: str,
emails: List[dict],
is_short_term: int = 0,
):
"""
批量导入邮箱到供应商资源池
Args:
email_type: 邮箱类型microsoft / ms_graph / ms_imap / google_variant / self_built
emails: 邮箱列表,每项包含 address、password、client_id、refresh_token 等
is_short_term: 仅微软邮箱有效0=长效默认1=短效
Returns:
ImportResult: 导入结果
示例::
result = client.supplier.import_emails(
email_type='ms_graph',
is_short_term=0,
emails=[
{
'address': 'user1@outlook.com',
'client_id': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'refresh_token': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
]
)
print(f"成功: {result.success}, 重复: {result.duplicate}")
"""
body: Dict[str, Any] = {
"type": email_type,
"is_short_term": is_short_term,
"emails": emails,
}
if _is_async_context():
return self._async_import_emails(body)
return self._sync_import_emails(body)
async def _async_import_emails(self, body: dict) -> ImportResult:
data = await self._client._async_request(
"POST", self._path("/emails/import"), json_data=body
)
return ImportResult(
success=data.get("success", 0),
duplicate=data.get("duplicate", 0),
failed=data.get("failed", 0),
)
def _sync_import_emails(self, body: dict) -> ImportResult:
data = self._client._sync_request(
"POST", self._path("/emails/import"), json_data=body
)
return ImportResult(
success=data.get("success", 0),
duplicate=data.get("duplicate", 0),
failed=data.get("failed", 0),
)
def export_emails(
self,
keyword: Optional[str] = None,
email_type: Optional[str] = None,
is_short_term: Optional[int] = None,
status: Optional[int] = None,
):
"""
导出邮箱txt 文件流)
Args:
keyword: 关键词过滤
email_type: 邮箱类型过滤
is_short_term: 0=长效 1=短效
status: 状态过滤
Returns:
bytes: txt 文件内容
示例::
content = client.supplier.export_emails(email_type='ms_graph')
with open("emails.txt", "wb") as f:
f.write(content)
"""
params = {
"keyword": keyword,
"type": email_type,
"is_short_term": is_short_term,
"status": status,
}
if _is_async_context():
return self._client._async_get_stream(self._path("/emails/export"), params=params)
return self._client._sync_get_stream(self._path("/emails/export"), params=params)
# ===== 申述管理 =====
def get_appeals(
self,
page: int = 1,
page_size: int = 20,
status: Optional[int] = None,
appeal_type: Optional[int] = None,
):
"""
获取申述列表(分页)
Args:
page: 页码
page_size: 每页数量
status: 申述状态1=待处理 2=已同意 3=待仲裁 4=已拒绝
appeal_type: 申述类型过滤
Returns:
PageResult: 分页结果list 为 AppealItem 列表
示例::
result = client.supplier.get_appeals(status=1)
print(f"待处理申述: {result.total}")
"""
params = {
"page": page,
"page_size": page_size,
"status": status,
"type": appeal_type,
}
if _is_async_context():
return self._async_get_appeals(params)
return self._sync_get_appeals(params)
async def _async_get_appeals(self, params: dict) -> PageResult:
data = await self._client._async_request("GET", self._path("/appeals"), params=params)
return _parse_page_result(data, _parse_appeal_item)
def _sync_get_appeals(self, params: dict) -> PageResult:
data = self._client._sync_request("GET", self._path("/appeals"), params=params)
return _parse_page_result(data, _parse_appeal_item)
def get_appeal(self, appeal_no: str):
"""
获取申述详情
Args:
appeal_no: 申述单号
Returns:
AppealDetail: 申述详情
示例::
detail = client.supplier.get_appeal("APL20240310001")
print(detail.reason, detail.status)
"""
if _is_async_context():
return self._async_get_appeal(appeal_no)
return self._sync_get_appeal(appeal_no)
async def _async_get_appeal(self, appeal_no: str) -> AppealDetail:
data = await self._client._async_request(
"GET", self._path(f"/appeal/{appeal_no}")
)
return _parse_appeal_detail(data)
def _sync_get_appeal(self, appeal_no: str) -> AppealDetail:
data = self._client._sync_request(
"GET", self._path(f"/appeal/{appeal_no}")
)
return _parse_appeal_detail(data)
def reply_appeal(self, appeal_no: str, result: int, reply: str):
"""
处理申述(回复)
Args:
appeal_no: 申述单号
result: 处理结果1=同意退款 2=拒绝申述 3=申请仲裁
reply: 回复内容说明
Returns:
None
示例::
# 同意退款
client.supplier.reply_appeal("APL20240310001", result=1, reply="邮箱确有问题,同意退款")
# 拒绝申述
client.supplier.reply_appeal("APL20240310001", result=2, reply="邮箱状态正常,拒绝申述")
"""
body = {"result": result, "reply": reply}
if _is_async_context():
return self._async_reply_appeal(appeal_no, body)
return self._sync_reply_appeal(appeal_no, body)
async def _async_reply_appeal(self, appeal_no: str, body: dict) -> None:
await self._client._async_request(
"POST", self._path(f"/appeal/{appeal_no}/reply"), json_data=body
)
def _sync_reply_appeal(self, appeal_no: str, body: dict) -> None:
self._client._sync_request(
"POST", self._path(f"/appeal/{appeal_no}/reply"), json_data=body
)
def batch_reply_appeals(
self,
appeal_nos: List[str],
result: int,
reply: str,
):
"""
批量处理申述
Args:
appeal_nos: 申述单号列表(最多 100 条)
result: 处理结果1=同意退款 2=拒绝申述 3=申请仲裁
reply: 回复内容说明
Returns:
dict: 包含 success 和 failed 数量
示例::
result = client.supplier.batch_reply_appeals(
appeal_nos=["APL001", "APL002", "APL003"],
result=2,
reply="经验证邮箱正常,拒绝申述"
)
print(f"成功处理: {result['success']}")
"""
body = {
"appeal_nos": appeal_nos,
"result": result,
"reply": reply,
}
if _is_async_context():
return self._async_batch_reply_appeals(body)
return self._sync_batch_reply_appeals(body)
async def _async_batch_reply_appeals(self, body: dict) -> dict:
return await self._client._async_request(
"POST", self._path("/appeals/batch-reply"), json_data=body
)
def _sync_batch_reply_appeals(self, body: dict) -> dict:
return self._client._sync_request(
"POST", self._path("/appeals/batch-reply"), json_data=body
)
# ===== 数据看板 =====
def get_dashboard(self):
"""
获取数据看板总览
Returns:
DashboardSummary: 看板数据,包含邮箱总量、接码统计、佣金数据等
示例::
summary = client.supplier.get_dashboard()
print(f"总邮箱: {summary.total_emails}")
print(f"今日佣金: {summary.today_commission}")
print(f"成功率: {summary.success_rate}%")
"""
if _is_async_context():
return self._async_get_dashboard()
return self._sync_get_dashboard()
async def _async_get_dashboard(self) -> DashboardSummary:
data = await self._client._async_request("GET", self._path("/dashboard/summary"))
return self._build_dashboard(data)
def _sync_get_dashboard(self) -> DashboardSummary:
data = self._client._sync_request("GET", self._path("/dashboard/summary"))
return self._build_dashboard(data)
def _build_dashboard(self, data: dict) -> DashboardSummary:
return DashboardSummary(
total_emails=data.get("total_emails", 0),
active_emails=data.get("active_emails", 0),
total_assigned=data.get("total_assigned", 0),
total_success=data.get("total_success", 0),
success_rate=data.get("success_rate", 0.0),
total_commission=data.get("total_commission", "0.0000"),
available_balance=data.get("available_balance", "0.0000"),
today_assigned=data.get("today_assigned", 0),
today_success=data.get("today_success", 0),
today_commission=data.get("today_commission", "0.0000"),
email_category=data.get("email_category", {}),
)

1360
core/luckmail/user.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,9 @@ export default function Register() {
cfworker_admin_token: cfg.cfworker_admin_token || '',
cfworker_domain: cfg.cfworker_domain || '',
cfworker_fingerprint: cfg.cfworker_fingerprint || '',
luckmail_base_url: cfg.luckmail_base_url || 'https://mails.luckyous.com/',
luckmail_api_key: cfg.luckmail_api_key || '',
luckmail_email_type: cfg.luckmail_email_type || '',
})
})
}, [form])
@@ -84,6 +87,9 @@ export default function Register() {
cfworker_admin_token: values.cfworker_admin_token,
cfworker_domain: values.cfworker_domain,
cfworker_fingerprint: values.cfworker_fingerprint,
luckmail_base_url: values.luckmail_base_url,
luckmail_api_key: values.luckmail_api_key,
luckmail_email_type: values.luckmail_email_type,
yescaptcha_key: values.yescaptcha_key,
solver_url: values.solver_url,
},
@@ -185,6 +191,7 @@ export default function Register() {
{ value: 'freemail', label: 'Freemail' },
{ value: 'laoudo', label: 'Laoudo' },
{ value: 'cfworker', label: 'CF Worker' },
{ value: 'luckmail', label: 'LuckMail' },
]}
/>
</Form.Item>
@@ -217,6 +224,19 @@ export default function Register() {
</Form.Item>
</>
)}
{mailProvider === 'luckmail' && (
<>
<Form.Item name="luckmail_base_url" label="平台地址">
<Input placeholder="https://mails.luckyous.com" />
</Form.Item>
<Form.Item name="luckmail_api_key" label="API Key">
<Input.Password placeholder="ak_..." />
</Form.Item>
<Form.Item name="luckmail_email_type" label="邮箱类型(可选)">
<Input placeholder="ms_graph / ms_imap" />
</Form.Item>
</>
)}
</Card>
{captchaSolver === 'yescaptcha' && (

View File

@@ -21,6 +21,7 @@ const SELECT_FIELDS: Record<string, { label: string; value: string }[]> = {
{ label: 'MoeMail (sall.cc)', value: 'moemail' },
{ label: 'Freemail自建 CF Worker', value: 'freemail' },
{ label: 'CF Worker自建域名', value: 'cfworker' },
{ label: 'LuckMail付费接码平台', value: 'luckmail' },
],
default_executor: [
{ label: 'API 协议(无浏览器)', value: 'protocol' },
@@ -105,6 +106,15 @@ const TAB_ITEMS = [
{ key: 'cfworker_fingerprint', label: 'Fingerprint', placeholder: '6703363b...' },
],
},
{
title: 'LuckMail',
desc: '付费接码平台,支持 Outlook / Gmail 等多种邮箱类型',
fields: [
{ key: 'luckmail_base_url', label: '平台地址', placeholder: 'https://mails.luckyous.com' },
{ key: 'luckmail_api_key', label: 'API Key', secret: true },
{ key: 'luckmail_email_type', label: '邮箱类型(可选)', placeholder: 'ms_graph / ms_imap / self_built' },
],
},
],
},
{
@@ -521,7 +531,12 @@ export default function Settings() {
const [activeTab, setActiveTab] = useState('register')
useEffect(() => {
apiFetch('/config').then(form.setFieldsValue)
apiFetch('/config').then((data) => {
if (!data.luckmail_base_url) {
data.luckmail_base_url = 'https://mails.luckyous.com/'
}
form.setFieldsValue(data)
})
}, [])
const save = async () => {