package app import ( "context" "errors" "net/http" "sort" "strings" "time" "github.com/go-chi/chi/v5" ) const smtpTestTemplateKey = "smtp_test" type MailTemplate struct { Key string `json:"key"` Name string `json:"name"` Subject string `json:"subject"` BodyText string `json:"bodyText"` BodyHTML string `json:"bodyHtml"` UpdatedAt time.Time `json:"updatedAt"` } type mailTemplateUpdate struct { Subject string `json:"subject"` BodyText string `json:"bodyText"` BodyHTML string `json:"bodyHtml"` } type templateRenderData struct { To string From string Subject string PublicHostname string PublicBaseURL string Time time.Time } func defaultMailTemplates() []MailTemplate { now := time.Unix(0, 0).UTC() return []MailTemplate{ { Key: "welcome", Name: "欢迎邮件", Subject: "欢迎使用 NewSzxcn 邮箱", BodyText: "你的自建邮箱 Webmail 已经初始化完成。\n\n请尽快修改默认管理员密码,并配置 MX/SPF/DKIM/DMARC。", BodyHTML: "
你的自建邮箱 Webmail 已经初始化完成。
请尽快修改默认管理员密码,并配置 MX/SPF/DKIM/DMARC。
", UpdatedAt: now, }, { Key: smtpTestTemplateKey, Name: "SMTP 测试", Subject: "NewSzxcn 邮箱 SMTP 测试", BodyText: "这是一封 SMTP 测试邮件。\n\n发件人:{{from}}\n收件人:{{to}}\n时间:{{time}}\n主机:{{publicHostname}}", BodyHTML: "这是一封 SMTP 测试邮件。
发件人:{{from}}
收件人:{{to}}
时间:{{time}}
主机:{{publicHostname}}
" + htmlEscape(bodyText) + "
" } bodyHTML = a.policy.Sanitize(bodyHTML) now := a.now().UTC().Format(time.RFC3339Nano) res, err := a.db.ExecContext(r.Context(), `UPDATE mail_templates SET subject=?,body_text=?,body_html=?,updated_at=? WHERE key=?`, subject, bodyText, bodyHTML, now, key) if err != nil { respondError(w, http.StatusInternalServerError, "failed to update template") return } affected, _ := res.RowsAffected() if affected == 0 { respondError(w, http.StatusNotFound, "template not found") return } tpl, err := a.mailTemplate(r.Context(), key) if err != nil { respondError(w, http.StatusInternalServerError, "failed to load template") return } respondJSON(w, http.StatusOK, tpl) } func (a *App) handleResetMailTemplate(w http.ResponseWriter, r *http.Request) { key := strings.TrimSpace(chi.URLParam(r, "key")) var defaults = defaultMailTemplates() for _, tpl := range defaults { if tpl.Key != key { continue } now := a.now().UTC().Format(time.RFC3339Nano) res, err := a.db.ExecContext(r.Context(), `UPDATE mail_templates SET name=?,subject=?,body_text=?,body_html=?,updated_at=? WHERE key=?`, tpl.Name, tpl.Subject, tpl.BodyText, tpl.BodyHTML, now, key) if err != nil { respondError(w, http.StatusInternalServerError, "failed to reset template") return } if affected, _ := res.RowsAffected(); affected == 0 { respondError(w, http.StatusNotFound, "template not found") return } updated, err := a.mailTemplate(r.Context(), key) if err != nil { respondError(w, http.StatusInternalServerError, "failed to load template") return } respondJSON(w, http.StatusOK, updated) return } respondError(w, http.StatusNotFound, "template not found") } func (a *App) mailTemplate(ctx context.Context, key string) (MailTemplate, error) { row := a.db.QueryRowContext(ctx, `SELECT key,name,subject,body_text,body_html,updated_at FROM mail_templates WHERE key=?`, key) var tpl MailTemplate var updated string if err := row.Scan(&tpl.Key, &tpl.Name, &tpl.Subject, &tpl.BodyText, &tpl.BodyHTML, &updated); err != nil { return MailTemplate{}, err } tpl.UpdatedAt = parseTime(updated) return tpl, nil } func renderMailTemplate(tpl MailTemplate, data templateRenderData) MIMEMessage { values := map[string]string{ "to": data.To, "from": data.From, "subject": data.Subject, "publicHostname": data.PublicHostname, "publicBaseUrl": data.PublicBaseURL, "time": data.Time.Format("2006-01-02 15:04:05 MST"), } keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } sort.Slice(keys, func(i, j int) bool { return len(keys[i]) > len(keys[j]) }) apply := func(input string) string { out := input for _, key := range keys { out = strings.ReplaceAll(out, "{{"+key+"}}", values[key]) } return out } return MIMEMessage{ Subject: apply(tpl.Subject), Text: apply(tpl.BodyText), HTML: apply(tpl.BodyHTML), } }