From cba60a2aa3bdc2c3d004f7adc06233f9a7edd2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E4=BA=91?= <58023926@qq.com> Date: Sat, 28 Mar 2026 22:02:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20LuckMail=20?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=8F=8A=E8=87=AA=E5=8A=A8=E5=88=86=E9=85=8D?= =?UTF-8?q?=E9=82=AE=E7=AE=B1=E6=B8=A0=E9=81=93=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 LuckMail 核心 SDK - 封装 \LuckMailMailbox\ 适配器接入注册工厂 - 支持智能匹配各类项目的子渠道编码 - 完善前端 UI 配置选项 --- api/config.py | 1 + api/tasks.py | 28 +- core/base_mailbox.py | 73 ++ core/luckmail/__init__.py | 63 ++ core/luckmail/client.py | 224 +++++ core/luckmail/exceptions.py | 35 + core/luckmail/http_client.py | 355 ++++++++ core/luckmail/models.py | 251 ++++++ core/luckmail/supplier.py | 463 +++++++++++ core/luckmail/user.py | 1360 +++++++++++++++++++++++++++++++ frontend/src/pages/Register.tsx | 20 + frontend/src/pages/Settings.tsx | 17 +- 12 files changed, 2886 insertions(+), 4 deletions(-) create mode 100644 core/luckmail/__init__.py create mode 100644 core/luckmail/client.py create mode 100644 core/luckmail/exceptions.py create mode 100644 core/luckmail/http_client.py create mode 100644 core/luckmail/models.py create mode 100644 core/luckmail/supplier.py create mode 100644 core/luckmail/user.py diff --git a/api/config.py b/api/config.py index 772fb66..2c922d2 100644 --- a/api/config.py +++ b/api/config.py @@ -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", diff --git a/api/tasks.py b/api/tasks.py index 08273f7..fc9c5f3 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -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", diff --git a/core/base_mailbox.py b/core/base_mailbox.py index 00f3bc8..b087755 100644 --- a/core/base_mailbox.py +++ b/core/base_mailbox.py @@ -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) diff --git a/core/luckmail/__init__.py b/core/luckmail/__init__.py new file mode 100644 index 0000000..0aeb261 --- /dev/null +++ b/core/luckmail/__init__.py @@ -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", +] diff --git a/core/luckmail/client.py b/core/luckmail/client.py new file mode 100644 index 0000000..bd8cd60 --- /dev/null +++ b/core/luckmail/client.py @@ -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]}...)" + ) diff --git a/core/luckmail/exceptions.py b/core/luckmail/exceptions.py new file mode 100644 index 0000000..d3fd387 --- /dev/null +++ b/core/luckmail/exceptions.py @@ -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) diff --git a/core/luckmail/http_client.py b/core/luckmail/http_client.py new file mode 100644 index 0000000..1345c5c --- /dev/null +++ b/core/luckmail/http_client.py @@ -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() diff --git a/core/luckmail/models.py b/core/luckmail/models.py new file mode 100644 index 0000000..17fa7c9 --- /dev/null +++ b/core/luckmail/models.py @@ -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) diff --git a/core/luckmail/supplier.py b/core/luckmail/supplier.py new file mode 100644 index 0000000..abec761 --- /dev/null +++ b/core/luckmail/supplier.py @@ -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", {}), + ) diff --git a/core/luckmail/user.py b/core/luckmail/user.py new file mode 100644 index 0000000..bf06a2d --- /dev/null +++ b/core/luckmail/user.py @@ -0,0 +1,1360 @@ +""" +用户端 API 接口 +Base URL: {base_url}/api/v1/openapi + +所有方法均支持同步/异步双模式,根据调用上下文自动识别: +- 在 async 函数中 await 调用:异步模式 +- 在普通函数中直接调用:同步模式 +""" + +import asyncio +import time +from typing import Any, Dict, List, Optional, Union + +from .http_client import LuckMailHttpClient, _is_async_context +from .models import ( + AppealInfo, + EmailItem, + ImportResult, + OrderCode, + OrderInfo, + PageResult, + ProjectItem, + ProjectPrice, + PurchaseItem, + TagItem, + TokenCode, + TokenAliveResult, + TokenMailDetail, + TokenMailItem, + TokenMailList, + UserInfo, +) + + +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), + ) + + +def _parse_user_info(data: dict) -> UserInfo: + return UserInfo( + id=data.get("id", 0), + username=data.get("username", ""), + email=data.get("email", ""), + balance=data.get("balance", "0.0000"), + status=data.get("status", 1), + api_email_enabled=data.get("api_email_enabled", 0), + api_email_price=data.get("api_email_price", "0.0000"), + ) + + +def _parse_email_item(data: dict) -> EmailItem: + return EmailItem( + 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), + ) + + +def _parse_project_item(data: dict) -> ProjectItem: + prices = [ + ProjectPrice( + email_type=p.get("email_type", ""), + code_price=p.get("code_price", "0.0000"), + buy_price=p.get("buy_price", "0.0000"), + ) + for p in data.get("prices", []) + ] + return ProjectItem( + id=data.get("id", 0), + name=data.get("name", ""), + code=data.get("code", ""), + email_types=data.get("email_types", []), + timeout_seconds=data.get("timeout_seconds", 300), + warranty_hours=data.get("warranty_hours", 0), + daily_limit=data.get("daily_limit", 0), + description=data.get("description", ""), + prices=prices, + ) + + +def _parse_order_info(data: dict) -> OrderInfo: + return OrderInfo( + order_no=data.get("order_no", ""), + email_address=data.get("email_address", ""), + project=data.get("project", ""), + price=data.get("price", "0.0000"), + timeout_seconds=data.get("timeout_seconds", 300), + expired_at=data.get("expired_at", ""), + ) + + +def _parse_order_code(data: dict) -> OrderCode: + return OrderCode( + order_no=data.get("order_no", ""), + status=data.get("status", "pending"), + verification_code=data.get("verification_code"), + mail_from=data.get("mail_from"), + mail_subject=data.get("mail_subject"), + mail_body_html=data.get("mail_body_html"), + ) + + +def _parse_purchase_item(data: dict) -> PurchaseItem: + return PurchaseItem( + id=data.get("id", 0), + email_address=data.get("email_address", ""), + token=data.get("token", ""), + project_name=data.get("project_name", ""), + price=data.get("price", "0.0000"), + status=data.get("status", 1), + tag_id=data.get("tag_id", 0), + tag_name=data.get("tag_name", ""), + user_disabled=data.get("user_disabled", 0), + warranty_hours=data.get("warranty_hours", 0), + warranty_until=data.get("warranty_until"), + created_at=data.get("created_at"), + ) + + +def _parse_tag_item(data: dict) -> TagItem: + return TagItem( + id=data.get("id", 0), + name=data.get("name", ""), + remark=data.get("remark", ""), + limit_type=data.get("limit_type", 0), + purchase_count=data.get("purchase_count", 0), + created_at=data.get("created_at"), + ) + + +def _parse_token_code(data: dict) -> TokenCode: + return TokenCode( + email_address=data.get("email_address", ""), + project=data.get("project", ""), + has_new_mail=data.get("has_new_mail", False), + verification_code=data.get("verification_code"), + mail=data.get("mail"), + ) + + +def _parse_token_alive_result(data: dict) -> TokenAliveResult: + return TokenAliveResult( + email_address=data.get("email_address", ""), + project=data.get("project", ""), + alive=data.get("alive", False), + status=data.get("status", "failed"), + message=data.get("message", ""), + mail_count=data.get("mail_count", 0), + ) + + +def _parse_token_mail_item(data: dict) -> TokenMailItem: + return TokenMailItem( + message_id=data.get("message_id", ""), + from_addr=data.get("from", ""), + subject=data.get("subject", ""), + body=data.get("body", ""), + html_body=data.get("html_body", ""), + received_at=data.get("received_at", ""), + ) + + +def _parse_token_mail_list(data: dict) -> TokenMailList: + mails_raw = data.get("mails", []) + mails = [_parse_token_mail_item(m) for m in mails_raw] if mails_raw else [] + return TokenMailList( + email_address=data.get("email_address", ""), + project=data.get("project", ""), + warranty_until=data.get("warranty_until", ""), + mails=mails, + ) + + +def _parse_token_mail_detail(data: dict) -> TokenMailDetail: + return TokenMailDetail( + message_id=data.get("message_id", ""), + from_addr=data.get("from", ""), + to=data.get("to", ""), + subject=data.get("subject", ""), + body_text=data.get("body_text", ""), + body_html=data.get("body_html", ""), + received_at=data.get("received_at", ""), + verification_code=data.get("verification_code", ""), + ) + + +class UserAPI: + """ + 用户端 API 接口集合 + + 所有方法智能支持同步/异步调用: + - 在 async 函数中:await client.user.get_user_info() + - 在普通函数中:client.user.get_user_info() + + Args: + http_client: LuckMailHttpClient 实例 + """ + + def __init__(self, http_client: LuckMailHttpClient): + self._client = http_client + + # ===== 用户信息 ===== + + def get_user_info(self): + """ + 获取用户信息及余额 + + Returns: + UserInfo: 用户信息对象 + + 同步调用:: + info = client.user.get_user_info() + print(info.username, info.balance) + + 异步调用:: + info = await client.user.get_user_info() + print(info.username, info.balance) + """ + if _is_async_context(): + return self._async_get_user_info() + return self._sync_get_user_info() + + async def _async_get_user_info(self) -> UserInfo: + data = await self._client._async_request("GET", "/api/v1/openapi/user/info") + return _parse_user_info(data) + + def _sync_get_user_info(self) -> UserInfo: + data = self._client._sync_request("GET", "/api/v1/openapi/user/info") + return _parse_user_info(data) + + def get_balance(self): + """ + 查询余额 + + Returns: + str: 余额字符串,如 "150.0000" + + 示例:: + balance = client.user.get_balance() + print(f"余额: {balance}") + """ + if _is_async_context(): + return self._async_get_balance() + return self._sync_get_balance() + + async def _async_get_balance(self) -> str: + data = await self._client._async_request("GET", "/api/v1/openapi/balance") + return data.get("balance", "0.0000") + + def _sync_get_balance(self) -> str: + data = self._client._sync_request("GET", "/api/v1/openapi/balance") + return data.get("balance", "0.0000") + + # ===== 邮箱类型 ===== + + def get_email_types(self): + """ + 获取支持的邮箱类型列表 + + Returns: + List[dict]: 邮箱类型列表,每项含 type、name、description + + 示例:: + types = client.user.get_email_types() + for t in types: + print(t['type'], t['name']) + """ + if _is_async_context(): + return self._async_get_email_types() + return self._sync_get_email_types() + + async def _async_get_email_types(self) -> List[dict]: + return await self._client._async_request("GET", "/api/v1/openapi/email-types") + + def _sync_get_email_types(self) -> List[dict]: + return self._client._sync_request("GET", "/api/v1/openapi/email-types") + + # ===== 我的邮箱管理 ===== + + def get_emails( + self, + page: int = 1, + page_size: int = 20, + keyword: Optional[str] = None, + status: Optional[int] = None, + ): + """ + 获取我的邮箱列表(分页) + + Args: + page: 页码,默认 1 + page_size: 每页数量,默认 20 + keyword: 邮箱地址关键词搜索 + status: 状态过滤:1=正常 2=异常 4=禁用 + + Returns: + PageResult: 分页结果,list 为 EmailItem 列表 + + 示例:: + result = client.user.get_emails(page=1, keyword="outlook") + for email in result.list: + print(email.address, email.status) + """ + params = { + "page": page, + "page_size": page_size, + "keyword": keyword, + "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", "/api/v1/openapi/emails", params=params) + return _parse_page_result(data, _parse_email_item) + + def _sync_get_emails(self, params: dict) -> PageResult: + data = self._client._sync_request("GET", "/api/v1/openapi/emails", params=params) + return _parse_page_result(data, _parse_email_item) + + def import_emails(self, email_type: str, emails: List[dict]): + """ + 导入邮箱到私有邮箱池 + + Args: + email_type: 邮箱类型,如 'ms_graph', 'ms_imap', 'google_variant', 'self_built' + emails: 邮箱列表,每项为 dict,包含 address、password、client_id、refresh_token 等 + + Returns: + ImportResult: 导入结果(success/duplicate/failed 数量) + + 示例:: + result = client.user.import_emails( + email_type='ms_graph', + emails=[ + { + 'address': 'user@outlook.com', + 'password': 'pass123', + 'client_id': 'xxx-xxx-xxx', + 'refresh_token': 'xxxxxxxxxxxxxxxx' + } + ] + ) + print(f"成功: {result.success}, 重复: {result.duplicate}, 失败: {result.failed}") + """ + body = {"type": email_type, "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", "/api/v1/openapi/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", "/api/v1/openapi/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, + status: Optional[int] = None, + ): + """ + 导出邮箱(txt 文件流) + + Args: + keyword: 关键词过滤 + status: 状态过滤:1=正常 2=异常 4=禁用 + + Returns: + bytes: txt 文件内容,每行格式:address----password 或 address----client_id----refresh_token + + 示例:: + content = client.user.export_emails(keyword="outlook") + with open("emails.txt", "wb") as f: + f.write(content) + """ + params = {"keyword": keyword, "status": status} + if _is_async_context(): + return self._client._async_get_stream("/api/v1/openapi/emails/export", params=params) + return self._client._sync_get_stream("/api/v1/openapi/emails/export", params=params) + + # ===== 项目列表 ===== + + def get_projects(self, page: int = 1, page_size: int = 50): + """ + 获取项目列表 + + Args: + page: 页码,默认 1 + page_size: 每页数量,默认 50,最大 500 + + Returns: + PageResult: 分页结果,list 为 ProjectItem 列表 + + 示例:: + result = client.user.get_projects() + for p in result.list: + print(p.name, p.code) + """ + params = {"page": page, "page_size": page_size} + if _is_async_context(): + return self._async_get_projects(params) + return self._sync_get_projects(params) + + async def _async_get_projects(self, params: dict) -> PageResult: + data = await self._client._async_request("GET", "/api/v1/openapi/projects", params=params) + return _parse_page_result(data, _parse_project_item) + + def _sync_get_projects(self, params: dict) -> PageResult: + data = self._client._sync_request("GET", "/api/v1/openapi/projects", params=params) + return _parse_page_result(data, _parse_project_item) + + # ===== 接码订单 ===== + + def create_order( + self, + project_code: str, + email_type: Optional[str] = None, + domain: Optional[str] = None, + specified_email: Optional[str] = None, + variant_mode: Optional[str] = None, + ): + """ + 创建接码订单 + + Args: + project_code: 项目编码,如 'twitter', 'facebook' + email_type: 邮箱类型(可选):ms_graph / ms_imap / self_built / google_variant + domain: 指定域名(可选),如 'outlook.com' + specified_email: 指定邮箱地址(可选) + variant_mode: 谷歌变种模式(可选,仅 email_type=google_variant 时有效): dot=点号变种 / plus=+号变种 / mixed=混合变种 / all=随机选择 + + Returns: + OrderInfo: 订单信息,包含 order_no 和分配的 email_address + + 示例:: + order = client.user.create_order('twitter', email_type='ms_graph') + print(f"订单号: {order.order_no}") + print(f"邮箱: {order.email_address}") + """ + body: Dict[str, Any] = {"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 + + if _is_async_context(): + return self._async_create_order(body) + return self._sync_create_order(body) + + async def _async_create_order(self, body: dict) -> OrderInfo: + data = await self._client._async_request("POST", "/api/v1/openapi/order/create", json_data=body) + return _parse_order_info(data) + + def _sync_create_order(self, body: dict) -> OrderInfo: + data = self._client._sync_request("POST", "/api/v1/openapi/order/create", json_data=body) + return _parse_order_info(data) + + def get_order_code(self, order_no: str): + """ + 查询验证码(单次查询) + + Args: + order_no: 订单编号 + + Returns: + OrderCode: 验证码结果,status 为 'success' 时包含 verification_code + + 示例:: + code = client.user.get_order_code(order.order_no) + if code.status == 'success': + print(f"验证码: {code.verification_code}") + """ + if _is_async_context(): + return self._async_get_order_code(order_no) + return self._sync_get_order_code(order_no) + + async def _async_get_order_code(self, order_no: str) -> OrderCode: + data = await self._client._async_request( + "GET", f"/api/v1/openapi/order/{order_no}/code" + ) + return _parse_order_code(data) + + def _sync_get_order_code(self, order_no: str) -> OrderCode: + data = self._client._sync_request( + "GET", f"/api/v1/openapi/order/{order_no}/code" + ) + return _parse_order_code(data) + + def cancel_order(self, order_no: str): + """ + 取消订单 + + Args: + order_no: 订单编号 + + Returns: + None + + 示例:: + client.user.cancel_order(order.order_no) + """ + if _is_async_context(): + return self._async_cancel_order(order_no) + return self._sync_cancel_order(order_no) + + async def _async_cancel_order(self, order_no: str) -> None: + await self._client._async_request( + "POST", f"/api/v1/openapi/order/{order_no}/cancel" + ) + + def _sync_cancel_order(self, order_no: str) -> None: + self._client._sync_request( + "POST", f"/api/v1/openapi/order/{order_no}/cancel" + ) + + def get_orders( + self, + page: int = 1, + page_size: int = 20, + status: Optional[int] = None, + project_id: Optional[int] = None, + ): + """ + 获取订单列表(分页) + + Args: + page: 页码 + page_size: 每页数量 + status: 状态过滤:1=待接码 2=已完成 3=已超时 4=已取消 5=已退款 + project_id: 按项目 ID 筛选 + + Returns: + PageResult: 分页结果,list 为订单 dict 列表 + + 示例:: + result = client.user.get_orders(status=2) + print(f"共 {result.total} 条已完成订单") + """ + params = { + "page": page, + "page_size": page_size, + "status": status, + "project_id": project_id, + } + if _is_async_context(): + return self._async_get_orders(params) + return self._sync_get_orders(params) + + async def _async_get_orders(self, params: dict) -> PageResult: + data = await self._client._async_request("GET", "/api/v1/openapi/orders", params=params) + return _parse_page_result(data) + + def _sync_get_orders(self, params: dict) -> PageResult: + data = self._client._sync_request("GET", "/api/v1/openapi/orders", params=params) + return _parse_page_result(data) + + # ===== 接码轮询(高级方法)===== + + def wait_for_code( + self, + order_no: str, + timeout: int = 300, + interval: float = 3.0, + on_poll: Optional[callable] = None, + ): + """ + 等待接码(带自动轮询),智能识别同步/异步上下文 + + 会自动每隔 interval 秒查询一次,直到收到验证码或超时。 + + Args: + order_no: 订单编号 + timeout: 最大等待时间(秒),默认 300 + interval: 轮询间隔(秒),默认 3.0 + on_poll: 每次轮询时的回调函数,接收 OrderCode 参数(可选) + + Returns: + OrderCode: 最终结果,status 为 'success' 或 'timeout'/'cancelled' + + 同步调用示例:: + order = client.user.create_order('twitter') + result = client.user.wait_for_code(order.order_no, timeout=300) + if result.status == 'success': + print(f"✅ 验证码: {result.verification_code}") + else: + print(f"❌ 接码失败: {result.status}") + + 异步调用示例:: + order = await client.user.create_order('twitter') + result = await client.user.wait_for_code(order.order_no, timeout=300) + if result.status == 'success': + print(f"✅ 验证码: {result.verification_code}") + """ + if _is_async_context(): + return self._async_wait_for_code(order_no, timeout, interval, on_poll) + return self._sync_wait_for_code(order_no, timeout, interval, on_poll) + + async def _async_wait_for_code( + self, + order_no: str, + timeout: int, + interval: float, + on_poll: Optional[callable], + ) -> OrderCode: + """异步轮询等待验证码""" + start = time.time() + while True: + result = await self._async_get_order_code(order_no) + + if on_poll: + if asyncio.iscoroutinefunction(on_poll): + await on_poll(result) + else: + on_poll(result) + + if result.status in ("success", "timeout", "cancelled"): + return result + + elapsed = time.time() - start + if elapsed >= timeout: + return result + + await asyncio.sleep(interval) + + def _sync_wait_for_code( + self, + order_no: str, + timeout: int, + interval: float, + on_poll: Optional[callable], + ) -> OrderCode: + """同步轮询等待验证码""" + start = time.time() + while True: + result = self._sync_get_order_code(order_no) + + if on_poll: + on_poll(result) + + if result.status in ("success", "timeout", "cancelled"): + return result + + elapsed = time.time() - start + if elapsed >= timeout: + return result + + time.sleep(interval) + + # ===== 购买邮箱 ===== + + def purchase_emails( + self, + project_code: str, + quantity: int, + email_type: Optional[str] = None, + domain: Optional[str] = None, + variant_mode: Optional[str] = None, + ): + """ + 购买邮箱 + + Args: + project_code: 项目编码 + quantity: 购买数量(1-10000) + email_type: 邮箱类型(可选) + domain: 指定域名(可选) + variant_mode: 谷歌变种模式(可选,仅 email_type=google_variant 时有效): dot=点号变种 / plus=+号变种 / mixed=混合变种 / all=随机选择 + + Returns: + dict: 购买结果,包含 purchases 列表、total_cost、balance_after + + 示例:: + result = client.user.purchase_emails('twitter', quantity=5, email_type='ms_graph') + for item in result['purchases']: + print(item['email_address'], item['token']) + """ + body: Dict[str, Any] = { + "project_code": project_code, + "quantity": quantity, + } + if email_type: + body["email_type"] = email_type + if domain: + body["domain"] = domain + if variant_mode: + body["variant_mode"] = variant_mode + + if _is_async_context(): + return self._async_purchase_emails(body) + return self._sync_purchase_emails(body) + + async def _async_purchase_emails(self, body: dict) -> dict: + return await self._client._async_request("POST", "/api/v1/openapi/email/purchase", json_data=body) + + def _sync_purchase_emails(self, body: dict) -> dict: + return self._client._sync_request("POST", "/api/v1/openapi/email/purchase", json_data=body) + + def get_purchases( + self, + page: int = 1, + page_size: int = 20, + project_id: Optional[int] = None, + tag_id: Optional[int] = None, + keyword: Optional[str] = None, + user_disabled: Optional[int] = None, + ): + """ + 获取已购邮箱列表 + + Args: + page: 页码 + page_size: 每页数量 + project_id: 按项目 ID 筛选 + tag_id: 按标签 ID 筛选 + keyword: 邮箱地址关键词搜索 + user_disabled: 禁用状态:0=正常 1=已禁用 + + Returns: + PageResult: 分页结果,list 为 PurchaseItem 列表 + + 示例:: + result = client.user.get_purchases(tag_id=1, keyword="outlook") + for item in result.list: + print(item.email_address, item.token, item.tag_name) + """ + params = { + "page": page, + "page_size": page_size, + "project_id": project_id, + "tag_id": tag_id, + "keyword": keyword, + "user_disabled": user_disabled, + } + if _is_async_context(): + return self._async_get_purchases(params) + return self._sync_get_purchases(params) + + async def _async_get_purchases(self, params: dict) -> PageResult: + data = await self._client._async_request("GET", "/api/v1/openapi/email/purchases", params=params) + return _parse_page_result(data, _parse_purchase_item) + + def _sync_get_purchases(self, params: dict) -> PageResult: + data = self._client._sync_request("GET", "/api/v1/openapi/email/purchases", params=params) + return _parse_page_result(data, _parse_purchase_item) + + def get_token_code(self, token: str): + """ + 通过 Token 获取最新验证码(已购邮箱) + + Args: + token: 已购邮箱的 token + + Returns: + TokenCode: 验证码结果 + + 示例:: + result = client.user.get_token_code("tok_abc123def456") + if result.has_new_mail: + print(f"验证码: {result.verification_code}") + """ + if _is_async_context(): + return self._async_get_token_code(token) + return self._sync_get_token_code(token) + + async def _async_get_token_code(self, token: str) -> TokenCode: + data = await self._client._async_request( + "GET", f"/api/v1/openapi/email/token/{token}/code" + ) + return _parse_token_code(data) + + def _sync_get_token_code(self, token: str) -> TokenCode: + data = self._client._sync_request( + "GET", f"/api/v1/openapi/email/token/{token}/code" + ) + return _parse_token_code(data) + + def check_token_alive(self, token: str): + """ + 通过 Token 测试已购邮箱是否可以正常获取邮件列表 + + Args: + token: 已购邮箱 token + + Returns: + TokenAliveResult: 测活结果 + + 示例:: + result = client.user.check_token_alive("tok_abc123def456") + print(result.alive, result.message) + """ + if _is_async_context(): + return self._async_check_token_alive(token) + return self._sync_check_token_alive(token) + + async def _async_check_token_alive(self, token: str) -> TokenAliveResult: + data = await self._client._async_request( + "GET", f"/api/v1/openapi/email/token/{token}/alive" + ) + return _parse_token_alive_result(data) + + def _sync_check_token_alive(self, token: str) -> TokenAliveResult: + data = self._client._sync_request( + "GET", f"/api/v1/openapi/email/token/{token}/alive" + ) + return _parse_token_alive_result(data) + + def wait_for_token_code( + self, + token: str, + timeout: int = 300, + interval: float = 3.0, + on_poll: Optional[callable] = None, + ): + """ + 等待 Token 邮箱的验证码(带自动轮询),智能识别同步/异步上下文 + + Args: + token: 已购邮箱 token + timeout: 最大等待时间(秒) + interval: 轮询间隔(秒) + on_poll: 每次轮询的回调 + + Returns: + TokenCode: 最终结果 + + 示例:: + result = client.user.wait_for_token_code("tok_abc123", timeout=120) + if result.has_new_mail: + print(f"✅ 验证码: {result.verification_code}") + """ + if _is_async_context(): + return self._async_wait_for_token_code(token, timeout, interval, on_poll) + return self._sync_wait_for_token_code(token, timeout, interval, on_poll) + + async def _async_wait_for_token_code( + self, token: str, timeout: int, interval: float, on_poll + ) -> TokenCode: + start = time.time() + while True: + result = await self._async_get_token_code(token) + + if on_poll: + if asyncio.iscoroutinefunction(on_poll): + await on_poll(result) + else: + on_poll(result) + + if result.has_new_mail: + return result + + if time.time() - start >= timeout: + return result + + await asyncio.sleep(interval) + + def _sync_wait_for_token_code( + self, token: str, timeout: int, interval: float, on_poll + ) -> TokenCode: + start = time.time() + while True: + result = self._sync_get_token_code(token) + + if on_poll: + on_poll(result) + + if result.has_new_mail: + return result + + if time.time() - start >= timeout: + return result + + time.sleep(interval) + + # ===== 已购邮箱邮件列表和详情 ===== + + def get_token_mails(self, token: str): + """ + 通过 Token 获取已购邮箱的邮件列表 + + Args: + token: 已购邮箱的 token + + Returns: + TokenMailList: 邮件列表结果,包含 email_address、project、warranty_until、mails + + 示例:: + result = client.user.get_token_mails("tok_abc123def456") + print(f"邮箱: {result.email_address}, 项目: {result.project}") + for mail in result.mails: + print(f" [{mail.received_at}] {mail.from_addr}: {mail.subject}") + """ + if _is_async_context(): + return self._async_get_token_mails(token) + return self._sync_get_token_mails(token) + + async def _async_get_token_mails(self, token: str) -> TokenMailList: + data = await self._client._async_request( + "GET", f"/api/v1/openapi/email/token/{token}/mails" + ) + return _parse_token_mail_list(data) + + def _sync_get_token_mails(self, token: str) -> TokenMailList: + data = self._client._sync_request( + "GET", f"/api/v1/openapi/email/token/{token}/mails" + ) + return _parse_token_mail_list(data) + + def get_token_mail_detail(self, token: str, message_id: str): + """ + 通过 Token 获取已购邮箱的邮件详情 + + Args: + token: 已购邮箱的 token + message_id: 邮件 ID(从 get_token_mails 返回的列表中获取) + + Returns: + TokenMailDetail: 邮件详情,包含 message_id、from_addr、to、subject、body_text、body_html、verification_code + + 示例:: + detail = client.user.get_token_mail_detail("tok_abc123", "AAMkAGI2...") + print(f"主题: {detail.subject}") + print(f"正文: {detail.body_text}") + if detail.verification_code: + print(f"验证码: {detail.verification_code}") + """ + if _is_async_context(): + return self._async_get_token_mail_detail(token, message_id) + return self._sync_get_token_mail_detail(token, message_id) + + async def _async_get_token_mail_detail(self, token: str, message_id: str) -> TokenMailDetail: + data = await self._client._async_request( + "GET", f"/api/v1/openapi/email/token/{token}/mails/{message_id}" + ) + return _parse_token_mail_detail(data) + + def _sync_get_token_mail_detail(self, token: str, message_id: str) -> TokenMailDetail: + data = self._client._sync_request( + "GET", f"/api/v1/openapi/email/token/{token}/mails/{message_id}" + ) + return _parse_token_mail_detail(data) + + # ===== 申述 ===== + + def create_appeal( + self, + appeal_type: int, + reason: str, + description: str, + order_id: Optional[int] = None, + purchase_id: Optional[int] = None, + evidence_urls: Optional[List[str]] = None, + ): + """ + 提交申述 + + Args: + appeal_type: 申述类型:1=接码订单 2=购买邮箱 + reason: 申述原因,如 'no_code', 'wrong_code', 'email_invalid' + description: 详细描述 + order_id: 接码订单 ID(appeal_type=1 时必填) + purchase_id: 购买记录 ID(appeal_type=2 时必填) + evidence_urls: 证据截图 URL 列表(可选) + + Returns: + dict: 包含 appeal_no 的字典 + + 示例:: + result = client.user.create_appeal( + appeal_type=1, + order_id=123, + reason='no_code', + description='等待 5 分钟未收到验证码' + ) + print(f"申述单号: {result['appeal_no']}") + """ + body: Dict[str, Any] = { + "appeal_type": appeal_type, + "reason": reason, + "description": description, + } + if order_id is not None: + body["order_id"] = order_id + if purchase_id is not None: + body["purchase_id"] = purchase_id + if evidence_urls: + body["evidence_urls"] = evidence_urls + + if _is_async_context(): + return self._async_create_appeal(body) + return self._sync_create_appeal(body) + + async def _async_create_appeal(self, body: dict) -> dict: + return await self._client._async_request( + "POST", "/api/v1/openapi/appeal/create", json_data=body + ) + + def _sync_create_appeal(self, body: dict) -> dict: + return self._client._sync_request( + "POST", "/api/v1/openapi/appeal/create", json_data=body + ) + + # ===== 已购邮箱禁用管理 ===== + + def set_purchase_disabled(self, purchase_id: int, disabled: int): + """ + 设置已购邮箱禁用状态 + + Args: + purchase_id: 已购邮箱 ID + disabled: 禁用状态:0=启用 1=禁用 + + Returns: + None + + 示例:: + client.user.set_purchase_disabled(1, 1) # 禁用 + client.user.set_purchase_disabled(1, 0) # 启用 + """ + body = {"disabled": disabled} + if _is_async_context(): + return self._async_set_purchase_disabled(purchase_id, body) + return self._sync_set_purchase_disabled(purchase_id, body) + + async def _async_set_purchase_disabled(self, purchase_id: int, body: dict) -> None: + await self._client._async_request( + "PUT", f"/api/v1/openapi/email/purchases/{purchase_id}/disabled", json_data=body + ) + + def _sync_set_purchase_disabled(self, purchase_id: int, body: dict) -> None: + self._client._sync_request( + "PUT", f"/api/v1/openapi/email/purchases/{purchase_id}/disabled", json_data=body + ) + + def batch_set_purchase_disabled(self, ids: List[int], disabled: int): + """ + 批量设置已购邮箱禁用状态 + + Args: + ids: 已购邮箱 ID 列表 + disabled: 禁用状态:0=启用 1=禁用 + + Returns: + None + + 示例:: + client.user.batch_set_purchase_disabled([1, 2, 3], 1) # 批量禁用 + """ + body = {"ids": ids, "disabled": disabled} + if _is_async_context(): + return self._async_batch_set_purchase_disabled(body) + return self._sync_batch_set_purchase_disabled(body) + + async def _async_batch_set_purchase_disabled(self, body: dict) -> None: + await self._client._async_request( + "POST", "/api/v1/openapi/email/purchases/batch-disabled", json_data=body + ) + + def _sync_batch_set_purchase_disabled(self, body: dict) -> None: + self._client._sync_request( + "POST", "/api/v1/openapi/email/purchases/batch-disabled", json_data=body + ) + + # ===== 已购邮箱标签管理 ===== + + def set_purchase_tag( + self, + purchase_id: int, + tag_id: Optional[int] = None, + tag_name: Optional[str] = None, + ): + """ + 设置已购邮箱标签 + + Args: + purchase_id: 已购邮箱 ID + tag_id: 标签 ID(与 tag_name 二选一,传 0 表示移除标签) + tag_name: 标签名称(与 tag_id 二选一) + + Returns: + None + + 示例:: + client.user.set_purchase_tag(1, tag_id=1) + client.user.set_purchase_tag(1, tag_name="主力号") + client.user.set_purchase_tag(1, tag_id=0) # 移除标签 + """ + body: Dict[str, Any] = {} + if tag_id is not None: + body["tag_id"] = tag_id + if tag_name is not None: + body["tag_name"] = tag_name + if _is_async_context(): + return self._async_set_purchase_tag(purchase_id, body) + return self._sync_set_purchase_tag(purchase_id, body) + + async def _async_set_purchase_tag(self, purchase_id: int, body: dict) -> None: + await self._client._async_request( + "PUT", f"/api/v1/openapi/email/purchases/{purchase_id}/tag", json_data=body + ) + + def _sync_set_purchase_tag(self, purchase_id: int, body: dict) -> None: + self._client._sync_request( + "PUT", f"/api/v1/openapi/email/purchases/{purchase_id}/tag", json_data=body + ) + + def batch_set_purchase_tag( + self, + ids: List[int], + tag_id: Optional[int] = None, + tag_name: Optional[str] = None, + ): + """ + 批量设置已购邮箱标签 + + Args: + ids: 已购邮箱 ID 列表 + tag_id: 标签 ID(与 tag_name 二选一,传 0 表示移除标签) + tag_name: 标签名称(与 tag_id 二选一) + + Returns: + None + + 示例:: + client.user.batch_set_purchase_tag([1, 2, 3], tag_name="主力号") + """ + body: Dict[str, Any] = {"ids": ids} + if tag_id is not None: + body["tag_id"] = tag_id + if tag_name is not None: + body["tag_name"] = tag_name + if _is_async_context(): + return self._async_batch_set_purchase_tag(body) + return self._sync_batch_set_purchase_tag(body) + + async def _async_batch_set_purchase_tag(self, body: dict) -> None: + await self._client._async_request( + "POST", "/api/v1/openapi/email/purchases/batch-tag", json_data=body + ) + + def _sync_batch_set_purchase_tag(self, body: dict) -> None: + self._client._sync_request( + "POST", "/api/v1/openapi/email/purchases/batch-tag", json_data=body + ) + + def api_get_purchases( + self, + count: int, + tag_id: Optional[int] = None, + tag_name: Optional[str] = None, + mark_tag_id: Optional[int] = None, + mark_tag_name: Optional[str] = None, + ): + """ + 按标签获取已购邮箱(API 下发) + + 仅返回未禁用且标签 limit_type=1(可下发)的邮箱。 + 可选择将获取到的邮箱标记为另一个标签。 + + Args: + count: 获取数量(1-100) + tag_id: 按标签 ID 筛选(与 tag_name 二选一) + tag_name: 按标签名称筛选(与 tag_id 二选一) + mark_tag_id: 获取后将邮箱标记为此标签 ID(与 mark_tag_name 二选一) + mark_tag_name: 获取后将邮箱标记为此标签名称(与 mark_tag_id 二选一) + + Returns: + List[PurchaseItem]: 已购邮箱列表 + + 示例:: + items = client.user.api_get_purchases(5, tag_name="主力号", mark_tag_name="已使用") + for item in items: + print(item.email_address, item.token) + """ + body: Dict[str, Any] = {"count": count} + if tag_id is not None: + body["tag_id"] = tag_id + if tag_name is not None: + body["tag_name"] = tag_name + if mark_tag_id is not None: + body["mark_tag_id"] = mark_tag_id + if mark_tag_name is not None: + body["mark_tag_name"] = mark_tag_name + if _is_async_context(): + return self._async_api_get_purchases(body) + return self._sync_api_get_purchases(body) + + async def _async_api_get_purchases(self, body: dict) -> List[PurchaseItem]: + data = await self._client._async_request( + "POST", "/api/v1/openapi/email/purchases/api-get", json_data=body + ) + return [_parse_purchase_item(i) for i in data] + + def _sync_api_get_purchases(self, body: dict) -> List[PurchaseItem]: + data = self._client._sync_request( + "POST", "/api/v1/openapi/email/purchases/api-get", json_data=body + ) + return [_parse_purchase_item(i) for i in data] + + # ===== 标签管理 ===== + + def create_tag(self, name: str, limit_type: int, remark: Optional[str] = None): + """ + 创建邮箱标签 + + Args: + name: 标签名称(用户下唯一) + limit_type: 限制类型:0=不下发 1=可下发 + remark: 备注说明(可选) + + Returns: + TagItem: 创建的标签信息 + + 示例:: + tag = client.user.create_tag("主力号", limit_type=1, remark="主力邮箱池") + print(f"标签 ID: {tag.id}, 名称: {tag.name}") + """ + body: Dict[str, Any] = {"name": name, "limit_type": limit_type} + if remark is not None: + body["remark"] = remark + if _is_async_context(): + return self._async_create_tag(body) + return self._sync_create_tag(body) + + async def _async_create_tag(self, body: dict) -> "TagItem": + data = await self._client._async_request( + "POST", "/api/v1/openapi/email/tags", json_data=body + ) + return _parse_tag_item(data) + + def _sync_create_tag(self, body: dict) -> "TagItem": + data = self._client._sync_request( + "POST", "/api/v1/openapi/email/tags", json_data=body + ) + return _parse_tag_item(data) + + def get_tags(self): + """ + 获取所有标签列表 + + Returns: + List[TagItem]: 标签列表 + + 示例:: + tags = client.user.get_tags() + for tag in tags: + print(tag.id, tag.name, tag.limit_type, tag.purchase_count) + """ + if _is_async_context(): + return self._async_get_tags() + return self._sync_get_tags() + + async def _async_get_tags(self) -> List["TagItem"]: + data = await self._client._async_request("GET", "/api/v1/openapi/email/tags") + return [_parse_tag_item(i) for i in data] + + def _sync_get_tags(self) -> List["TagItem"]: + data = self._client._sync_request("GET", "/api/v1/openapi/email/tags") + return [_parse_tag_item(i) for i in data] + + def update_tag( + self, + tag_id_or_name: Union[int, str], + limit_type: int, + name: Optional[str] = None, + remark: Optional[str] = None, + ): + """ + 更新标签 + + Args: + tag_id_or_name: 标签 ID(数字)或标签名称(字符串) + limit_type: 限制类型:0=不下发 1=可下发 + name: 新的标签名称(可选) + remark: 备注说明(可选) + + Returns: + None + + 示例:: + client.user.update_tag(1, limit_type=1, name="备用号") + client.user.update_tag("主力号", limit_type=0) + """ + body: Dict[str, Any] = {"limit_type": limit_type} + if name is not None: + body["name"] = name + if remark is not None: + body["remark"] = remark + if _is_async_context(): + return self._async_update_tag(tag_id_or_name, body) + return self._sync_update_tag(tag_id_or_name, body) + + async def _async_update_tag(self, tag_id_or_name: Union[int, str], body: dict) -> None: + await self._client._async_request( + "PUT", f"/api/v1/openapi/email/tags/{tag_id_or_name}", json_data=body + ) + + def _sync_update_tag(self, tag_id_or_name: Union[int, str], body: dict) -> None: + self._client._sync_request( + "PUT", f"/api/v1/openapi/email/tags/{tag_id_or_name}", json_data=body + ) + + def delete_tag(self, tag_id_or_name: Union[int, str]): + """ + 删除标签 + + 删除后,该标签下的已购邮箱将变为无标签状态。 + + Args: + tag_id_or_name: 标签 ID(数字)或标签名称(字符串) + + Returns: + None + + 示例:: + client.user.delete_tag(1) + client.user.delete_tag("已使用") + """ + if _is_async_context(): + return self._async_delete_tag(tag_id_or_name) + return self._sync_delete_tag(tag_id_or_name) + + async def _async_delete_tag(self, tag_id_or_name: Union[int, str]) -> None: + await self._client._async_request( + "DELETE", f"/api/v1/openapi/email/tags/{tag_id_or_name}" + ) + + def _sync_delete_tag(self, tag_id_or_name: Union[int, str]) -> None: + self._client._sync_request( + "DELETE", f"/api/v1/openapi/email/tags/{tag_id_or_name}" + ) diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index ec97cd9..769061f 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -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' }, ]} /> @@ -217,6 +224,19 @@ export default function Register() { > )} + {mailProvider === 'luckmail' && ( + <> +