use super::system_config_string; use crate::{AppState, GatewayError}; use serde_json::json; pub(crate) struct AdminEmailTemplateDefinition { pub(crate) template_type: &'static str, pub(crate) name: &'static str, pub(crate) variables: &'static [&'static str], pub(crate) default_subject: &'static str, pub(crate) default_html: &'static str, } const ADMIN_EMAIL_TEMPLATE_DEFINITIONS: &[AdminEmailTemplateDefinition] = &[ AdminEmailTemplateDefinition { template_type: "verification", name: "注册验证码", variables: &["app_name", "code", "expire_minutes", "email"], default_subject: "验证码", default_html: r#" 验证码
{{app_name}}

验证码

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

{{code}}

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

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

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

"#, }, AdminEmailTemplateDefinition { template_type: "password_reset", name: "找回密码", variables: &["app_name", "reset_link", "expire_minutes", "email"], default_subject: "密码重置", default_html: r#" 密码重置
{{app_name}}

重置密码

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

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

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

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

"#, }, ]; pub(crate) fn admin_email_template_definition( template_type: &str, ) -> Option<&'static AdminEmailTemplateDefinition> { let normalized = template_type.trim(); ADMIN_EMAIL_TEMPLATE_DEFINITIONS .iter() .find(|definition| definition.template_type == normalized) } pub(crate) fn admin_email_template_subject_key(template_type: &str) -> String { format!("email_template_{template_type}_subject") } pub(crate) fn admin_email_template_html_key(template_type: &str) -> String { format!("email_template_{template_type}_html") } pub(crate) async fn read_admin_email_template_payload( state: &AppState, template_type: &str, ) -> Result, GatewayError> { let Some(definition) = admin_email_template_definition(template_type) else { return Ok(None); }; let subject = state .read_system_config_json_value(&admin_email_template_subject_key(definition.template_type)) .await?; let html = state .read_system_config_json_value(&admin_email_template_html_key(definition.template_type)) .await?; let subject = system_config_string(subject.as_ref()) .unwrap_or_else(|| definition.default_subject.to_string()); let html = system_config_string(html.as_ref()).unwrap_or_else(|| definition.default_html.to_string()); let is_custom = subject != definition.default_subject || html != definition.default_html; Ok(Some(json!({ "type": definition.template_type, "name": definition.name, "variables": definition.variables, "subject": subject, "html": html, "is_custom": is_custom, "default_subject": definition.default_subject, "default_html": definition.default_html, }))) } pub(crate) fn escape_admin_email_template_html(value: &str) -> String { value .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('\"', """) .replace('\'', "'") } pub(crate) fn render_admin_email_template_html( template_html: &str, variables: &std::collections::BTreeMap, ) -> Result { let mut rendered = template_html.to_string(); for (key, value) in variables { let pattern = regex::Regex::new(&format!(r"\{{\{{\s*{}\s*\}}\}}", regex::escape(key))) .map_err(|err| GatewayError::Internal(err.to_string()))?; rendered = pattern .replace_all(&rendered, escape_admin_email_template_html(value)) .into_owned(); } Ok(rendered) }