feat(email): extract notification templates

This commit is contained in:
2026-05-05 12:18:18 +08:00
parent 8d3773a2eb
commit a780c1bf52
13 changed files with 878 additions and 633 deletions
+108
View File
@@ -0,0 +1,108 @@
from __future__ import annotations
from datetime import datetime
from enum import StrEnum
from html import escape
from pathlib import Path
from string import Template
from typing import Any
class EmailTemplateRenderError(RuntimeError):
pass
class SafeHtml(str):
pass
class EmailTemplate(StrEnum):
NEW_USER_REGISTRATION = "new-user-registration"
USER_APPROVED = "user-approved"
USER_REJECTED = "user-rejected"
TOKEN_EXPIRING = "token-expiring"
TOKEN_EXPIRED = "token-expired"
CHECK_IN_RESULT = "check-in-result"
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
TEMPLATE_META = {
EmailTemplate.NEW_USER_REGISTRATION: {
"title": "新用户注册通知",
"subtitle": "请及时审批新注册账户",
"header_background": "#4f46e5",
},
EmailTemplate.USER_APPROVED: {
"title": "账户审批通过",
"subtitle": "您现在可以使用接龙自动打卡系统",
"header_background": "#15803d",
},
EmailTemplate.USER_REJECTED: {
"title": "账户审批结果通知",
"subtitle": "您的注册申请未通过审批",
"header_background": "#b91c1c",
},
EmailTemplate.TOKEN_EXPIRING: {
"title": "登录凭证即将过期",
"subtitle": "请尽快刷新 QQ 登录凭证",
"header_background": "#c2410c",
},
EmailTemplate.TOKEN_EXPIRED: {
"title": "登录凭证已过期",
"subtitle": "自动打卡任务已无法继续执行",
"header_background": "#b91c1c",
},
EmailTemplate.CHECK_IN_RESULT: {
"title": "打卡结果通知",
"subtitle": "自动打卡任务执行结果",
"header_background": "#2563eb",
},
}
def _load_template(template_name: str) -> Template:
template_path = TEMPLATE_DIR / f"{template_name}.html"
if not template_path.exists():
raise EmailTemplateRenderError(f"email template file missing: {template_name}")
return Template(template_path.read_text(encoding="utf-8"))
def _escape_context(context: dict[str, Any]) -> dict[str, str]:
escaped: dict[str, str] = {}
for key, value in context.items():
if isinstance(value, SafeHtml):
escaped[key] = str(value)
elif value is None:
escaped[key] = ""
else:
escaped[key] = escape(str(value), quote=True)
return escaped
def _substitute(template_obj: Template, context: dict[str, Any]) -> str:
try:
return template_obj.substitute(context)
except KeyError as exc:
missing_key = exc.args[0]
raise EmailTemplateRenderError(
f"missing required email template context: {missing_key}"
) from exc
def render_email_template(template: EmailTemplate, context: dict[str, Any]) -> str:
escaped_context = _escape_context(context)
body = _substitute(_load_template(template.value), escaped_context)
meta = TEMPLATE_META[template]
footer_year = context.get("year") or datetime.now().year
return _substitute(
_load_template("base"),
{
"title": meta["title"],
"subtitle": meta["subtitle"],
"header_background": meta["header_background"],
"body": body,
"year": str(footer_year),
},
)