from __future__ import annotations from datetime import datetime from typing import Literal, Optional from pydantic import BaseModel, EmailStr, Field, field_validator SmtpSecurityValue = Literal["NONE", "SSL", "STARTTLS"] class EmailSettingsRead(BaseModel): register_domain: str = "huapont.cn" smtp_host: str = "" smtp_port: int = 465 smtp_security: SmtpSecurityValue = "SSL" smtp_username: str = "" smtp_password_configured: bool = False sender_email: str = "" sender_name: Optional[str] = "CTMS 系统" allowed_register_domain: str = "huapont.cn,qq.com" verification_code_ttl_minutes: int = 10 send_cooldown_seconds: int = 60 max_verify_attempts: int = 5 updated_at: Optional[datetime] = None class EmailSettingsUpdate(BaseModel): smtp_host: str = Field(default="", max_length=255) smtp_port: int = Field(ge=1, le=65535) smtp_security: SmtpSecurityValue = "SSL" smtp_username: str = Field(default="", max_length=255) smtp_password: Optional[str] = Field(default=None, max_length=500) sender_email: str = Field(default="", max_length=255) sender_name: Optional[str] = Field(default="CTMS 系统", max_length=255) allowed_register_domain: str = Field(default="", max_length=255) verification_code_ttl_minutes: int = Field(default=10, ge=1, le=60) send_cooldown_seconds: int = Field(default=60, ge=10, le=3600) max_verify_attempts: int = Field(default=5, ge=1, le=20) @field_validator("allowed_register_domain") @classmethod def normalize_domain(cls, value: str) -> str: domains = [] for item in value.replace(",", ",").split(","): domain = item.strip().lower().lstrip("@") if domain and domain not in domains: domains.append(domain) if not domains: raise ValueError("至少配置一个允许注册邮箱域名") return ",".join(domains) class EmailTestRequest(BaseModel): recipient_email: EmailStr class EmailDomainCreateRequest(BaseModel): register_domain: str = Field(min_length=1, max_length=255) @field_validator("register_domain") @classmethod def normalize_domain(cls, value: str) -> str: return value.strip().lower().lstrip("@") class EmailSettingsListResponse(BaseModel): items: list[EmailSettingsRead] total: int class RegisterEmailCodeSendRequest(BaseModel): email: EmailStr class RegisterEmailCodeVerifyRequest(BaseModel): email: EmailStr code: str = Field(min_length=4, max_length=12) class PasswordResetLinkSendRequest(BaseModel): email: EmailStr class PasswordResetRequest(BaseModel): email: EmailStr code: str = Field(min_length=4, max_length=12) password: str = Field(min_length=8, max_length=72) @field_validator("password") @classmethod def validate_password_strength(cls, value: str) -> str: has_letter = any(ch.isalpha() for ch in value) has_number = any(ch.isdigit() for ch in value) if not has_letter or not has_number: raise ValueError("密码需至少 8 位且包含字母和数字") return value class PasswordResetCodeVerifyRequest(BaseModel): email: EmailStr code: str = Field(min_length=4, max_length=12) class PasswordResetTokenRequest(BaseModel): token: str = Field(min_length=20, max_length=200) password: str = Field(min_length=8, max_length=72) @field_validator("password") @classmethod def validate_password_strength(cls, value: str) -> str: has_letter = any(ch.isalpha() for ch in value) has_number = any(ch.isdigit() for ch in value) if not has_letter or not has_number: raise ValueError("密码需至少 8 位且包含字母和数字") return value class EmailCodeResponse(BaseModel): message: str class EmailCodeVerifyResponse(BaseModel): verified: bool class PasswordResetCodeVerifyResponse(BaseModel): verified: bool reset_token: str