""" Token 管理器 支持多个 Microsoft Token 端点,自动选择合适的端点 """ import json import logging import threading import time from typing import Dict, Optional, Any from curl_cffi import requests as _requests from .base import ProviderType, TokenEndpoint, TokenInfo from .account import OutlookAccount logger = logging.getLogger(__name__) # 各提供者的 Scope 配置 PROVIDER_SCOPES = { ProviderType.IMAP_OLD: "", # 旧版 IMAP 不需要特定 scope ProviderType.IMAP_NEW: "https://outlook.office.com/IMAP.AccessAsUser.All offline_access", ProviderType.GRAPH_API: "https://graph.microsoft.com/.default", } # 各提供者的 Token 端点 PROVIDER_TOKEN_URLS = { ProviderType.IMAP_OLD: TokenEndpoint.LIVE.value, ProviderType.IMAP_NEW: TokenEndpoint.CONSUMERS.value, ProviderType.GRAPH_API: TokenEndpoint.COMMON.value, } class TokenManager: """ Token 管理器 支持多端点 Token 获取和缓存 """ # Token 缓存: key = (email, provider_type) -> TokenInfo _token_cache: Dict[tuple, TokenInfo] = {} _cache_lock = threading.Lock() # 默认超时时间 DEFAULT_TIMEOUT = 30 # Token 刷新提前时间(秒) REFRESH_BUFFER = 120 def __init__( self, account: OutlookAccount, provider_type: ProviderType, proxy_url: Optional[str] = None, timeout: int = DEFAULT_TIMEOUT, ): """ 初始化 Token 管理器 Args: account: Outlook 账户 provider_type: 提供者类型 proxy_url: 代理 URL(可选) timeout: 请求超时时间 """ self.account = account self.provider_type = provider_type self.proxy_url = proxy_url self.timeout = timeout # 获取端点和 Scope self.token_url = PROVIDER_TOKEN_URLS.get(provider_type, TokenEndpoint.LIVE.value) self.scope = PROVIDER_SCOPES.get(provider_type, "") def get_cached_token(self) -> Optional[TokenInfo]: """获取缓存的 Token""" cache_key = (self.account.email.lower(), self.provider_type) with self._cache_lock: token = self._token_cache.get(cache_key) if token and not token.is_expired(self.REFRESH_BUFFER): return token return None def set_cached_token(self, token: TokenInfo): """缓存 Token""" cache_key = (self.account.email.lower(), self.provider_type) with self._cache_lock: self._token_cache[cache_key] = token def clear_cache(self): """清除缓存""" cache_key = (self.account.email.lower(), self.provider_type) with self._cache_lock: self._token_cache.pop(cache_key, None) def get_access_token(self, force_refresh: bool = False) -> Optional[str]: """ 获取 Access Token Args: force_refresh: 是否强制刷新 Returns: Access Token 字符串,失败返回 None """ # 检查缓存 if not force_refresh: cached = self.get_cached_token() if cached: logger.debug(f"[{self.account.email}] 使用缓存的 Token ({self.provider_type.value})") return cached.access_token # 刷新 Token try: token = self._refresh_token() if token: self.set_cached_token(token) return token.access_token except Exception as e: logger.error(f"[{self.account.email}] 获取 Token 失败 ({self.provider_type.value}): {e}") return None def _refresh_token(self) -> Optional[TokenInfo]: """ 刷新 Token Returns: TokenInfo 对象,失败返回 None """ if not self.account.client_id or not self.account.refresh_token: raise ValueError("缺少 client_id 或 refresh_token") logger.debug(f"[{self.account.email}] 正在刷新 Token ({self.provider_type.value})...") logger.debug(f"[{self.account.email}] Token URL: {self.token_url}") # 构建请求体 data = { "client_id": self.account.client_id, "refresh_token": self.account.refresh_token, "grant_type": "refresh_token", } # 添加 Scope(如果需要) if self.scope: data["scope"] = self.scope headers = { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", } proxies = None if self.proxy_url: proxies = {"http": self.proxy_url, "https": self.proxy_url} try: resp = _requests.post( self.token_url, data=data, headers=headers, proxies=proxies, timeout=self.timeout, impersonate="chrome110", ) if resp.status_code != 200: error_body = resp.text logger.error(f"[{self.account.email}] Token 刷新失败: HTTP {resp.status_code}") logger.debug(f"[{self.account.email}] 错误响应: {error_body[:500]}") if "service abuse" in error_body.lower(): logger.warning(f"[{self.account.email}] 账号可能被封禁") elif "invalid_grant" in error_body.lower(): logger.warning(f"[{self.account.email}] Refresh Token 已失效") return None response_data = resp.json() # 解析响应 token = TokenInfo.from_response(response_data, self.scope) logger.info( f"[{self.account.email}] Token 刷新成功 ({self.provider_type.value}), " f"有效期 {int(token.expires_at - time.time())} 秒" ) return token except json.JSONDecodeError as e: logger.error(f"[{self.account.email}] JSON 解析错误: {e}") return None except Exception as e: logger.error(f"[{self.account.email}] 未知错误: {e}") return None @classmethod def clear_all_cache(cls): """清除所有 Token 缓存""" with cls._cache_lock: cls._token_cache.clear() logger.info("已清除所有 Token 缓存") @classmethod def get_cache_stats(cls) -> Dict[str, Any]: """获取缓存统计""" with cls._cache_lock: return { "cache_size": len(cls._token_cache), "entries": [ { "email": key[0], "provider": key[1].value, } for key in cls._token_cache.keys() ], } def create_token_manager( account: OutlookAccount, provider_type: ProviderType, proxy_url: Optional[str] = None, timeout: int = TokenManager.DEFAULT_TIMEOUT, ) -> TokenManager: """ 创建 Token 管理器的工厂函数 Args: account: Outlook 账户 provider_type: 提供者类型 proxy_url: 代理 URL timeout: 超时时间 Returns: TokenManager 实例 """ return TokenManager(account, provider_type, proxy_url, timeout)