""" 邮件模板 提供验证码邮件的 HTML 和纯文本模板,支持从数据库加载自定义模板 """ from __future__ import annotations import html import re from html.parser import HTMLParser from typing import Any from sqlalchemy.orm import Session from src.services.system.config import SystemConfigService class HTMLToTextParser(HTMLParser): """HTML 转纯文本解析器""" def __init__(self) -> None: super().__init__() self.text_parts = [] self.skip_data = False def handle_starttag(self, tag: Any, attrs: Any) -> None: # noqa: ARG002 if tag in ("script", "style", "head"): self.skip_data = True elif tag == "br": self.text_parts.append("\n") elif tag in ("p", "div", "tr", "h1", "h2", "h3", "h4", "h5", "h6"): self.text_parts.append("\n") def handle_endtag(self, tag: Any) -> None: if tag in ("script", "style", "head"): self.skip_data = False elif tag in ("p", "div", "tr", "h1", "h2", "h3", "h4", "h5", "h6", "td"): self.text_parts.append("\n") def handle_data(self, data: Any) -> None: if not self.skip_data: text = data.strip() if text: self.text_parts.append(text) class EmailTemplate: """邮件模板类""" # 模板类型定义 TEMPLATE_VERIFICATION = "verification" TEMPLATE_PASSWORD_RESET = "password_reset" # 支持的模板类型及其变量 TEMPLATE_TYPES = { TEMPLATE_VERIFICATION: { "name": "注册验证码", "variables": ["app_name", "code", "expire_minutes", "email"], "default_subject": "验证码", }, TEMPLATE_PASSWORD_RESET: { "name": "找回密码", "variables": ["app_name", "reset_link", "expire_minutes", "email"], "default_subject": "密码重置", }, } # Literary Tech 主题色 - 与网页保持一致 PRIMARY_COLOR = "#c96442" # book-cloth PRIMARY_LIGHT = "#e4b2a0" # kraft BG_WARM = "#faf9f5" # ivory-light BG_MEDIUM = "#e9e6dc" # ivory-medium / cloud-medium TEXT_DARK = "#3d3929" # slate-dark TEXT_MUTED = "#6c695c" # slate-medium BORDER_COLOR = "rgba(61, 57, 41, 0.12)" @staticmethod def get_default_verification_html() -> str: """获取默认的验证码邮件 HTML 模板 - Literary Tech 风格""" return """ 验证码
{{app_name}}

验证码

您正在注册账户,请使用以下验证码完成验证。

{{code}}

验证码将在 {{expire_minutes}} 分钟后失效

如果这不是您的操作,请忽略此邮件。

此邮件由系统自动发送,请勿回复

""" @staticmethod def get_default_password_reset_html() -> str: """获取默认的密码重置邮件 HTML 模板 - Literary Tech 风格""" return """ 密码重置
{{app_name}}

重置密码

您正在重置账户密码,请点击下方按钮完成操作。

链接将在 {{expire_minutes}} 分钟后失效

如果您没有请求重置密码,请忽略此邮件。

此邮件由系统自动发送,请勿回复

""" @staticmethod def get_default_template(template_type: str) -> dict[str, str]: """ 获取默认模板 Args: template_type: 模板类型 Returns: 包含 subject 和 html 的字典 """ if template_type == EmailTemplate.TEMPLATE_VERIFICATION: return { "subject": "验证码", "html": EmailTemplate.get_default_verification_html(), } elif template_type == EmailTemplate.TEMPLATE_PASSWORD_RESET: return { "subject": "密码重置", "html": EmailTemplate.get_default_password_reset_html(), } else: return {"subject": "通知", "html": ""} @staticmethod def get_template(db: Session, template_type: str) -> dict[str, str]: """ 从数据库获取模板,如果不存在则返回默认模板 Args: db: 数据库会话 template_type: 模板类型 Returns: 包含 subject 和 html 的字典 """ default = EmailTemplate.get_default_template(template_type) # 从数据库获取自定义模板 subject_key = f"email_template_{template_type}_subject" html_key = f"email_template_{template_type}_html" custom_subject = SystemConfigService.get_config(db, subject_key, default=None) custom_html = SystemConfigService.get_config(db, html_key, default=None) return { "subject": custom_subject if custom_subject else default["subject"], "html": custom_html if custom_html else default["html"], } @staticmethod def render_template(template_html: str, variables: dict[str, Any]) -> str: """ 渲染模板,替换 {{variable}} 格式的变量 Args: template_html: HTML 模板 variables: 变量字典 Returns: 渲染后的 HTML """ result = template_html for key, value in variables.items(): # HTML 转义变量值,防止 XSS escaped_value = html.escape(str(value)) # 替换 {{key}} 格式的变量 pattern = r"\{\{\s*" + re.escape(key) + r"\s*\}\}" result = re.sub(pattern, escaped_value, result) return result @staticmethod def html_to_text(html: str) -> str: """ 从 HTML 提取纯文本 Args: html: HTML 内容 Returns: 纯文本内容 """ parser = HTMLToTextParser() parser.feed(html) text = " ".join(parser.text_parts) # 清理多余空白 text = re.sub(r"\n\s*\n", "\n\n", text) text = re.sub(r" +", " ", text) return text.strip() @staticmethod def get_verification_code_html( code: str, expire_minutes: int = 5, db: Session | None = None, **kwargs: Any ) -> str: """ 获取验证码邮件 HTML Args: code: 验证码 expire_minutes: 过期时间(分钟) db: 数据库会话(用于获取自定义模板) **kwargs: 其他模板变量 Returns: 渲染后的 HTML """ app_name = kwargs.get("app_name", "Aether") email = kwargs.get("email", "") # 获取模板 if db: template = EmailTemplate.get_template(db, EmailTemplate.TEMPLATE_VERIFICATION) else: template = EmailTemplate.get_default_template(EmailTemplate.TEMPLATE_VERIFICATION) # 渲染变量 variables = { "app_name": app_name, "code": code, "expire_minutes": expire_minutes, "email": email, } return EmailTemplate.render_template(template["html"], variables) @staticmethod def get_verification_code_text( code: str, expire_minutes: int = 5, db: Session | None = None, **kwargs: Any ) -> str: """ 获取验证码邮件纯文本(从 HTML 自动生成) Args: code: 验证码 expire_minutes: 过期时间(分钟) db: 数据库会话 **kwargs: 其他模板变量 Returns: 纯文本邮件内容 """ html = EmailTemplate.get_verification_code_html(code, expire_minutes, db, **kwargs) return EmailTemplate.html_to_text(html) @staticmethod def get_password_reset_html( reset_link: str, expire_minutes: int = 30, db: Session | None = None, **kwargs: Any ) -> str: """ 获取密码重置邮件 HTML Args: reset_link: 重置链接 expire_minutes: 过期时间(分钟) db: 数据库会话 **kwargs: 其他模板变量 Returns: 渲染后的 HTML """ app_name = kwargs.get("app_name", "Aether") email = kwargs.get("email", "") # 获取模板 if db: template = EmailTemplate.get_template(db, EmailTemplate.TEMPLATE_PASSWORD_RESET) else: template = EmailTemplate.get_default_template(EmailTemplate.TEMPLATE_PASSWORD_RESET) # 渲染变量 variables = { "app_name": app_name, "reset_link": reset_link, "expire_minutes": expire_minutes, "email": email, } return EmailTemplate.render_template(template["html"], variables) @staticmethod def get_password_reset_text( reset_link: str, expire_minutes: int = 30, db: Session | None = None, **kwargs: Any ) -> str: """ 获取密码重置邮件纯文本(从 HTML 自动生成) Args: reset_link: 重置链接 expire_minutes: 过期时间(分钟) db: 数据库会话 **kwargs: 其他模板变量 Returns: 纯文本邮件内容 """ html = EmailTemplate.get_password_reset_html(reset_link, expire_minutes, db, **kwargs) return EmailTemplate.html_to_text(html) @staticmethod def get_subject(template_type: str = "verification", db: Session | None = None) -> str: """ 获取邮件主题 Args: template_type: 模板类型 db: 数据库会话 Returns: 邮件主题 """ if db: template = EmailTemplate.get_template(db, template_type) return template["subject"] default_subjects = { "verification": "验证码", "welcome": "欢迎加入", "password_reset": "密码重置", } return default_subjects.get(template_type, "通知")