Some checks are pending
Docker Image CI / build-and-push-image (push) Waiting to run
154 lines
5.3 KiB
Python
154 lines
5.3 KiB
Python
"""
|
|
Outlook 服务基础定义
|
|
包含枚举类型和数据类
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
|
|
class ProviderType(str, Enum):
|
|
"""Outlook 提供者类型"""
|
|
IMAP_OLD = "imap_old" # 旧版 IMAP (outlook.office365.com)
|
|
IMAP_NEW = "imap_new" # 新版 IMAP (outlook.live.com)
|
|
GRAPH_API = "graph_api" # Microsoft Graph API
|
|
|
|
|
|
class TokenEndpoint(str, Enum):
|
|
"""Token 端点"""
|
|
LIVE = "https://login.live.com/oauth20_token.srf"
|
|
CONSUMERS = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
|
|
COMMON = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
|
|
|
|
|
class IMAPServer(str, Enum):
|
|
"""IMAP 服务器"""
|
|
OLD = "outlook.office365.com"
|
|
NEW = "outlook.live.com"
|
|
|
|
|
|
class ProviderStatus(str, Enum):
|
|
"""提供者状态"""
|
|
HEALTHY = "healthy" # 健康
|
|
DEGRADED = "degraded" # 降级
|
|
DISABLED = "disabled" # 禁用
|
|
|
|
|
|
@dataclass
|
|
class EmailMessage:
|
|
"""邮件消息数据类"""
|
|
id: str # 消息 ID
|
|
subject: str # 主题
|
|
sender: str # 发件人
|
|
recipients: List[str] = field(default_factory=list) # 收件人列表
|
|
body: str = "" # 正文内容
|
|
body_preview: str = "" # 正文预览
|
|
received_at: Optional[datetime] = None # 接收时间
|
|
received_timestamp: int = 0 # 接收时间戳
|
|
is_read: bool = False # 是否已读
|
|
has_attachments: bool = False # 是否有附件
|
|
raw_data: Optional[bytes] = None # 原始数据(用于调试)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""转换为字典"""
|
|
return {
|
|
"id": self.id,
|
|
"subject": self.subject,
|
|
"sender": self.sender,
|
|
"recipients": self.recipients,
|
|
"body": self.body,
|
|
"body_preview": self.body_preview,
|
|
"received_at": self.received_at.isoformat() if self.received_at else None,
|
|
"received_timestamp": self.received_timestamp,
|
|
"is_read": self.is_read,
|
|
"has_attachments": self.has_attachments,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class TokenInfo:
|
|
"""Token 信息数据类"""
|
|
access_token: str
|
|
expires_at: float # 过期时间戳
|
|
token_type: str = "Bearer"
|
|
scope: str = ""
|
|
refresh_token: Optional[str] = None
|
|
|
|
def is_expired(self, buffer_seconds: int = 120) -> bool:
|
|
"""检查 Token 是否已过期"""
|
|
import time
|
|
return time.time() >= (self.expires_at - buffer_seconds)
|
|
|
|
@classmethod
|
|
def from_response(cls, data: Dict[str, Any], scope: str = "") -> "TokenInfo":
|
|
"""从 API 响应创建"""
|
|
import time
|
|
return cls(
|
|
access_token=data.get("access_token", ""),
|
|
expires_at=time.time() + data.get("expires_in", 3600),
|
|
token_type=data.get("token_type", "Bearer"),
|
|
scope=scope or data.get("scope", ""),
|
|
refresh_token=data.get("refresh_token"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ProviderHealth:
|
|
"""提供者健康状态"""
|
|
provider_type: ProviderType
|
|
status: ProviderStatus = ProviderStatus.HEALTHY
|
|
failure_count: int = 0 # 连续失败次数
|
|
last_success: Optional[datetime] = None # 最后成功时间
|
|
last_failure: Optional[datetime] = None # 最后失败时间
|
|
last_error: str = "" # 最后错误信息
|
|
disabled_until: Optional[datetime] = None # 禁用截止时间
|
|
|
|
def record_success(self):
|
|
"""记录成功"""
|
|
self.status = ProviderStatus.HEALTHY
|
|
self.failure_count = 0
|
|
self.last_success = datetime.now()
|
|
self.disabled_until = None
|
|
|
|
def record_failure(self, error: str):
|
|
"""记录失败"""
|
|
self.failure_count += 1
|
|
self.last_failure = datetime.now()
|
|
self.last_error = error
|
|
|
|
def should_disable(self, threshold: int = 3) -> bool:
|
|
"""判断是否应该禁用"""
|
|
return self.failure_count >= threshold
|
|
|
|
def is_disabled(self) -> bool:
|
|
"""检查是否被禁用"""
|
|
if self.disabled_until and datetime.now() < self.disabled_until:
|
|
return True
|
|
return False
|
|
|
|
def disable(self, duration_seconds: int = 300):
|
|
"""禁用提供者"""
|
|
from datetime import timedelta
|
|
self.status = ProviderStatus.DISABLED
|
|
self.disabled_until = datetime.now() + timedelta(seconds=duration_seconds)
|
|
|
|
def enable(self):
|
|
"""启用提供者"""
|
|
self.status = ProviderStatus.HEALTHY
|
|
self.disabled_until = None
|
|
self.failure_count = 0
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""转换为字典"""
|
|
return {
|
|
"provider_type": self.provider_type.value,
|
|
"status": self.status.value,
|
|
"failure_count": self.failure_count,
|
|
"last_success": self.last_success.isoformat() if self.last_success else None,
|
|
"last_failure": self.last_failure.isoformat() if self.last_failure else None,
|
|
"last_error": self.last_error,
|
|
"disabled_until": self.disabled_until.isoformat() if self.disabled_until else None,
|
|
}
|