Spaces:
Sleeping
Sleeping
| """ | |
| 邮箱服务配置 API 路由 | |
| """ | |
| import logging | |
| from typing import List, Optional, Dict, Any | |
| from fastapi import APIRouter, HTTPException, Query | |
| from pydantic import BaseModel | |
| from ...database import crud | |
| from ...database.session import get_db | |
| from ...database.models import EmailService as EmailServiceModel | |
| from ...services import EmailServiceFactory, EmailServiceType | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter() | |
| # ============== Pydantic Models ============== | |
| class EmailServiceCreate(BaseModel): | |
| """创建邮箱服务请求""" | |
| service_type: str | |
| name: str | |
| config: Dict[str, Any] | |
| enabled: bool = True | |
| priority: int = 0 | |
| class EmailServiceUpdate(BaseModel): | |
| """更新邮箱服务请求""" | |
| name: Optional[str] = None | |
| config: Optional[Dict[str, Any]] = None | |
| enabled: Optional[bool] = None | |
| priority: Optional[int] = None | |
| class EmailServiceResponse(BaseModel): | |
| """邮箱服务响应""" | |
| id: int | |
| service_type: str | |
| name: str | |
| enabled: bool | |
| priority: int | |
| config: Optional[Dict[str, Any]] = None # 过滤敏感信息后的配置 | |
| last_used: Optional[str] = None | |
| created_at: Optional[str] = None | |
| updated_at: Optional[str] = None | |
| class Config: | |
| from_attributes = True | |
| class EmailServiceListResponse(BaseModel): | |
| """邮箱服务列表响应""" | |
| total: int | |
| services: List[EmailServiceResponse] | |
| class ServiceTestResult(BaseModel): | |
| """服务测试结果""" | |
| success: bool | |
| message: str | |
| details: Optional[Dict[str, Any]] = None | |
| class OutlookBatchImportRequest(BaseModel): | |
| """Outlook 批量导入请求""" | |
| data: str # 多行数据,每行格式: 邮箱----密码 或 邮箱----密码----client_id----refresh_token | |
| enabled: bool = True | |
| priority: int = 0 | |
| class OutlookBatchImportResponse(BaseModel): | |
| """Outlook 批量导入响应""" | |
| total: int | |
| success: int | |
| failed: int | |
| accounts: List[Dict[str, Any]] | |
| errors: List[str] | |
| # ============== Helper Functions ============== | |
| # 敏感字段列表,返回响应时需要过滤 | |
| SENSITIVE_FIELDS = {'password', 'api_key', 'refresh_token', 'access_token', 'admin_token'} | |
| def filter_sensitive_config(config: Dict[str, Any]) -> Dict[str, Any]: | |
| """过滤敏感配置信息""" | |
| if not config: | |
| return {} | |
| filtered = {} | |
| for key, value in config.items(): | |
| if key in SENSITIVE_FIELDS: | |
| # 敏感字段不返回,但标记是否存在 | |
| filtered[f"has_{key}"] = bool(value) | |
| else: | |
| filtered[key] = value | |
| # 为 Outlook 计算是否有 OAuth | |
| if config.get('client_id') and config.get('refresh_token'): | |
| filtered['has_oauth'] = True | |
| return filtered | |
| def service_to_response(service: EmailServiceModel) -> EmailServiceResponse: | |
| """转换服务模型为响应""" | |
| return EmailServiceResponse( | |
| id=service.id, | |
| service_type=service.service_type, | |
| name=service.name, | |
| enabled=service.enabled, | |
| priority=service.priority, | |
| config=filter_sensitive_config(service.config), | |
| last_used=service.last_used.isoformat() if service.last_used else None, | |
| created_at=service.created_at.isoformat() if service.created_at else None, | |
| updated_at=service.updated_at.isoformat() if service.updated_at else None, | |
| ) | |
| # ============== API Endpoints ============== | |
| async def get_email_services_stats(): | |
| """获取邮箱服务统计信息""" | |
| with get_db() as db: | |
| from sqlalchemy import func | |
| # 按类型统计 | |
| type_stats = db.query( | |
| EmailServiceModel.service_type, | |
| func.count(EmailServiceModel.id) | |
| ).group_by(EmailServiceModel.service_type).all() | |
| # 启用数量 | |
| enabled_count = db.query(func.count(EmailServiceModel.id)).filter( | |
| EmailServiceModel.enabled == True | |
| ).scalar() | |
| stats = { | |
| 'outlook_count': 0, | |
| 'custom_count': 0, | |
| 'temp_mail_count': 0, | |
| 'duck_mail_count': 0, | |
| 'freemail_count': 0, | |
| 'imap_mail_count': 0, | |
| 'tempmail_available': True, # 临时邮箱始终可用 | |
| 'enabled_count': enabled_count | |
| } | |
| for service_type, count in type_stats: | |
| if service_type == 'outlook': | |
| stats['outlook_count'] = count | |
| elif service_type == 'moe_mail': | |
| stats['custom_count'] = count | |
| elif service_type == 'temp_mail': | |
| stats['temp_mail_count'] = count | |
| elif service_type == 'duck_mail': | |
| stats['duck_mail_count'] = count | |
| elif service_type == 'freemail': | |
| stats['freemail_count'] = count | |
| elif service_type == 'imap_mail': | |
| stats['imap_mail_count'] = count | |
| return stats | |
| async def get_service_types(): | |
| """获取支持的邮箱服务类型""" | |
| return { | |
| "types": [ | |
| { | |
| "value": "tempmail", | |
| "label": "Tempmail.lol", | |
| "description": "临时邮箱服务,无需配置", | |
| "config_fields": [ | |
| {"name": "base_url", "label": "API 地址", "default": "https://api.tempmail.lol/v2", "required": False}, | |
| {"name": "timeout", "label": "超时时间", "default": 30, "required": False}, | |
| ] | |
| }, | |
| { | |
| "value": "outlook", | |
| "label": "Outlook", | |
| "description": "Outlook 邮箱,需要配置账户信息", | |
| "config_fields": [ | |
| {"name": "email", "label": "邮箱地址", "required": True}, | |
| {"name": "password", "label": "密码", "required": True}, | |
| {"name": "client_id", "label": "OAuth Client ID", "required": False}, | |
| {"name": "refresh_token", "label": "OAuth Refresh Token", "required": False}, | |
| ] | |
| }, | |
| { | |
| "value": "moe_mail", | |
| "label": "MoeMail", | |
| "description": "自定义域名邮箱服务", | |
| "config_fields": [ | |
| {"name": "base_url", "label": "API 地址", "required": True}, | |
| {"name": "api_key", "label": "API Key", "required": True}, | |
| {"name": "default_domain", "label": "默认域名", "required": False}, | |
| ] | |
| }, | |
| { | |
| "value": "temp_mail", | |
| "label": "Temp-Mail(自部署)", | |
| "description": "自部署 Cloudflare Worker 临时邮箱,admin 模式管理", | |
| "config_fields": [ | |
| {"name": "base_url", "label": "Worker 地址", "required": True, "placeholder": "https://mail.example.com"}, | |
| {"name": "admin_password", "label": "Admin 密码", "required": True, "secret": True}, | |
| {"name": "domain", "label": "邮箱域名", "required": True, "placeholder": "example.com"}, | |
| {"name": "enable_prefix", "label": "启用前缀", "required": False, "default": True}, | |
| ] | |
| }, | |
| { | |
| "value": "duck_mail", | |
| "label": "DuckMail", | |
| "description": "DuckMail 接口邮箱服务,支持 API Key 私有域名访问", | |
| "config_fields": [ | |
| {"name": "base_url", "label": "API 地址", "required": True, "placeholder": "https://api.duckmail.sbs"}, | |
| {"name": "default_domain", "label": "默认域名", "required": True, "placeholder": "duckmail.sbs"}, | |
| {"name": "api_key", "label": "API Key", "required": False, "secret": True}, | |
| {"name": "password_length", "label": "随机密码长度", "required": False, "default": 12}, | |
| ] | |
| }, | |
| { | |
| "value": "freemail", | |
| "label": "Freemail", | |
| "description": "Freemail 自部署 Cloudflare Worker 临时邮箱服务", | |
| "config_fields": [ | |
| {"name": "base_url", "label": "API 地址", "required": True, "placeholder": "https://freemail.example.com"}, | |
| {"name": "admin_token", "label": "Admin Token", "required": True, "secret": True}, | |
| {"name": "domain", "label": "邮箱域名", "required": False, "placeholder": "example.com"}, | |
| ] | |
| }, | |
| { | |
| "value": "imap_mail", | |
| "label": "IMAP 邮箱", | |
| "description": "标准 IMAP 协议邮箱(Gmail/QQ/163等),仅用于接收验证码,强制直连", | |
| "config_fields": [ | |
| {"name": "host", "label": "IMAP 服务器", "required": True, "placeholder": "imap.gmail.com"}, | |
| {"name": "port", "label": "端口", "required": False, "default": 993}, | |
| {"name": "use_ssl", "label": "使用 SSL", "required": False, "default": True}, | |
| {"name": "email", "label": "邮箱地址", "required": True}, | |
| {"name": "password", "label": "密码/授权码", "required": True, "secret": True}, | |
| ] | |
| } | |
| ] | |
| } | |
| async def list_email_services( | |
| service_type: Optional[str] = Query(None, description="服务类型筛选"), | |
| enabled_only: bool = Query(False, description="只显示启用的服务"), | |
| ): | |
| """获取邮箱服务列表""" | |
| with get_db() as db: | |
| query = db.query(EmailServiceModel) | |
| if service_type: | |
| query = query.filter(EmailServiceModel.service_type == service_type) | |
| if enabled_only: | |
| query = query.filter(EmailServiceModel.enabled == True) | |
| services = query.order_by(EmailServiceModel.priority.asc(), EmailServiceModel.id.asc()).all() | |
| return EmailServiceListResponse( | |
| total=len(services), | |
| services=[service_to_response(s) for s in services] | |
| ) | |
| async def get_email_service(service_id: int): | |
| """获取单个邮箱服务详情""" | |
| with get_db() as db: | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if not service: | |
| raise HTTPException(status_code=404, detail="服务不存在") | |
| return service_to_response(service) | |
| async def get_email_service_full(service_id: int): | |
| """获取单个邮箱服务完整详情(包含敏感字段,用于编辑)""" | |
| with get_db() as db: | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if not service: | |
| raise HTTPException(status_code=404, detail="服务不存在") | |
| return { | |
| "id": service.id, | |
| "service_type": service.service_type, | |
| "name": service.name, | |
| "enabled": service.enabled, | |
| "priority": service.priority, | |
| "config": service.config or {}, # 返回完整配置 | |
| "last_used": service.last_used.isoformat() if service.last_used else None, | |
| "created_at": service.created_at.isoformat() if service.created_at else None, | |
| "updated_at": service.updated_at.isoformat() if service.updated_at else None, | |
| } | |
| async def create_email_service(request: EmailServiceCreate): | |
| """创建邮箱服务配置""" | |
| # 验证服务类型 | |
| try: | |
| EmailServiceType(request.service_type) | |
| except ValueError: | |
| raise HTTPException(status_code=400, detail=f"无效的服务类型: {request.service_type}") | |
| with get_db() as db: | |
| # 检查名称是否重复 | |
| existing = db.query(EmailServiceModel).filter(EmailServiceModel.name == request.name).first() | |
| if existing: | |
| raise HTTPException(status_code=400, detail="服务名称已存在") | |
| service = EmailServiceModel( | |
| service_type=request.service_type, | |
| name=request.name, | |
| config=request.config, | |
| enabled=request.enabled, | |
| priority=request.priority | |
| ) | |
| db.add(service) | |
| db.commit() | |
| db.refresh(service) | |
| return service_to_response(service) | |
| async def update_email_service(service_id: int, request: EmailServiceUpdate): | |
| """更新邮箱服务配置""" | |
| with get_db() as db: | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if not service: | |
| raise HTTPException(status_code=404, detail="服务不存在") | |
| update_data = {} | |
| if request.name is not None: | |
| update_data["name"] = request.name | |
| if request.config is not None: | |
| # 合并配置而不是替换 | |
| current_config = service.config or {} | |
| merged_config = {**current_config, **request.config} | |
| # 移除空值 | |
| merged_config = {k: v for k, v in merged_config.items() if v} | |
| update_data["config"] = merged_config | |
| if request.enabled is not None: | |
| update_data["enabled"] = request.enabled | |
| if request.priority is not None: | |
| update_data["priority"] = request.priority | |
| for key, value in update_data.items(): | |
| setattr(service, key, value) | |
| db.commit() | |
| db.refresh(service) | |
| return service_to_response(service) | |
| async def delete_email_service(service_id: int): | |
| """删除邮箱服务配置""" | |
| with get_db() as db: | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if not service: | |
| raise HTTPException(status_code=404, detail="服务不存在") | |
| db.delete(service) | |
| db.commit() | |
| return {"success": True, "message": f"服务 {service.name} 已删除"} | |
| async def test_email_service(service_id: int): | |
| """测试邮箱服务是否可用""" | |
| with get_db() as db: | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if not service: | |
| raise HTTPException(status_code=404, detail="服务不存在") | |
| try: | |
| service_type = EmailServiceType(service.service_type) | |
| email_service = EmailServiceFactory.create(service_type, service.config, name=service.name) | |
| health = email_service.check_health() | |
| if health: | |
| return ServiceTestResult( | |
| success=True, | |
| message="服务连接正常", | |
| details=email_service.get_service_info() if hasattr(email_service, 'get_service_info') else None | |
| ) | |
| else: | |
| return ServiceTestResult( | |
| success=False, | |
| message="服务连接失败" | |
| ) | |
| except Exception as e: | |
| logger.error(f"测试邮箱服务失败: {e}") | |
| return ServiceTestResult( | |
| success=False, | |
| message=f"测试失败: {str(e)}" | |
| ) | |
| async def enable_email_service(service_id: int): | |
| """启用邮箱服务""" | |
| with get_db() as db: | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if not service: | |
| raise HTTPException(status_code=404, detail="服务不存在") | |
| service.enabled = True | |
| db.commit() | |
| return {"success": True, "message": f"服务 {service.name} 已启用"} | |
| async def disable_email_service(service_id: int): | |
| """禁用邮箱服务""" | |
| with get_db() as db: | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if not service: | |
| raise HTTPException(status_code=404, detail="服务不存在") | |
| service.enabled = False | |
| db.commit() | |
| return {"success": True, "message": f"服务 {service.name} 已禁用"} | |
| async def reorder_services(service_ids: List[int]): | |
| """重新排序邮箱服务优先级""" | |
| with get_db() as db: | |
| for index, service_id in enumerate(service_ids): | |
| service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first() | |
| if service: | |
| service.priority = index | |
| db.commit() | |
| return {"success": True, "message": "优先级已更新"} | |
| async def batch_import_outlook(request: OutlookBatchImportRequest): | |
| """ | |
| 批量导入 Outlook 邮箱账户 | |
| 支持两种格式: | |
| - 格式一(密码认证):邮箱----密码 | |
| - 格式二(XOAUTH2 认证):邮箱----密码----client_id----refresh_token | |
| 每行一个账户,使用四个连字符(----)分隔字段 | |
| """ | |
| lines = request.data.strip().split("\n") | |
| total = len(lines) | |
| success = 0 | |
| failed = 0 | |
| accounts = [] | |
| errors = [] | |
| with get_db() as db: | |
| for i, line in enumerate(lines): | |
| line = line.strip() | |
| # 跳过空行和注释 | |
| if not line or line.startswith("#"): | |
| continue | |
| parts = line.split("----") | |
| # 验证格式 | |
| if len(parts) < 2: | |
| failed += 1 | |
| errors.append(f"行 {i+1}: 格式错误,至少需要邮箱和密码") | |
| continue | |
| email = parts[0].strip() | |
| password = parts[1].strip() | |
| # 验证邮箱格式 | |
| if "@" not in email: | |
| failed += 1 | |
| errors.append(f"行 {i+1}: 无效的邮箱地址: {email}") | |
| continue | |
| # 检查是否已存在 | |
| existing = db.query(EmailServiceModel).filter( | |
| EmailServiceModel.service_type == "outlook", | |
| EmailServiceModel.name == email | |
| ).first() | |
| if existing: | |
| failed += 1 | |
| errors.append(f"行 {i+1}: 邮箱已存在: {email}") | |
| continue | |
| # 构建配置 | |
| config = { | |
| "email": email, | |
| "password": password | |
| } | |
| # 检查是否有 OAuth 信息(格式二) | |
| if len(parts) >= 4: | |
| client_id = parts[2].strip() | |
| refresh_token = parts[3].strip() | |
| if client_id and refresh_token: | |
| config["client_id"] = client_id | |
| config["refresh_token"] = refresh_token | |
| # 创建服务记录 | |
| try: | |
| service = EmailServiceModel( | |
| service_type="outlook", | |
| name=email, | |
| config=config, | |
| enabled=request.enabled, | |
| priority=request.priority | |
| ) | |
| db.add(service) | |
| db.commit() | |
| db.refresh(service) | |
| accounts.append({ | |
| "id": service.id, | |
| "email": email, | |
| "has_oauth": bool(config.get("client_id")), | |
| "name": email | |
| }) | |
| success += 1 | |
| except Exception as e: | |
| failed += 1 | |
| errors.append(f"行 {i+1}: 创建失败: {str(e)}") | |
| db.rollback() | |
| return OutlookBatchImportResponse( | |
| total=total, | |
| success=success, | |
| failed=failed, | |
| accounts=accounts, | |
| errors=errors | |
| ) | |
| async def batch_delete_outlook(service_ids: List[int]): | |
| """批量删除 Outlook 邮箱服务""" | |
| deleted = 0 | |
| with get_db() as db: | |
| for service_id in service_ids: | |
| service = db.query(EmailServiceModel).filter( | |
| EmailServiceModel.id == service_id, | |
| EmailServiceModel.service_type == "outlook" | |
| ).first() | |
| if service: | |
| db.delete(service) | |
| deleted += 1 | |
| db.commit() | |
| return {"success": True, "deleted": deleted, "message": f"已删除 {deleted} 个服务"} | |
| # ============== 临时邮箱测试 ============== | |
| class TempmailTestRequest(BaseModel): | |
| """临时邮箱测试请求""" | |
| api_url: Optional[str] = None | |
| async def test_tempmail_service(request: TempmailTestRequest): | |
| """测试临时邮箱服务是否可用""" | |
| try: | |
| from ...services import EmailServiceFactory, EmailServiceType | |
| from ...config.settings import get_settings | |
| settings = get_settings() | |
| base_url = request.api_url or settings.tempmail_base_url | |
| config = {"base_url": base_url} | |
| tempmail = EmailServiceFactory.create(EmailServiceType.TEMPMAIL, config) | |
| # 检查服务健康状态 | |
| health = tempmail.check_health() | |
| if health: | |
| return {"success": True, "message": "临时邮箱连接正常"} | |
| else: | |
| return {"success": False, "message": "临时邮箱连接失败"} | |
| except Exception as e: | |
| logger.error(f"测试临时邮箱失败: {e}") | |
| return {"success": False, "message": f"测试失败: {str(e)}"} | |