完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 00:17:00 +08:00
parent acde7ee288
commit 6f054a903c
98 changed files with 8239 additions and 2073 deletions

View File

@@ -0,0 +1,265 @@
package service
import (
"bytes"
"mime/quotedprintable"
"regexp"
"strings"
"golang.org/x/net/html"
)
// 手机 QQ / 163 等会丢弃 <style>;发送与预览前把简单 class 规则内联,并做成完整 HTML 文档。
var (
mailCSSCommentRe = regexp.MustCompile(`(?s)/\*.*?\*/`)
mailCSSRuleRe = regexp.MustCompile(`([^{}@]+)\{([^{}]+)\}`)
)
func prepareMailHTML(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return raw
}
doc, err := html.Parse(strings.NewReader(raw))
if err != nil {
return wrapMailDocument(raw)
}
css := map[string]string{}
collectAndRemoveMailStyles(doc, css)
if len(css) > 0 {
applyMailClassStyles(doc, css)
}
ensureMailDocumentHead(doc)
var buf bytes.Buffer
if err := html.Render(&buf, doc); err != nil {
return wrapMailDocument(raw)
}
return buf.String()
}
func wrapMailDocument(body string) string {
lower := strings.ToLower(body)
if strings.Contains(lower, "<html") {
return body
}
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body style="margin:0;padding:0;">` + body + `</body></html>`
}
func collectAndRemoveMailStyles(n *html.Node, css map[string]string) {
if n.Type == html.ElementNode && n.Data == "style" {
var b strings.Builder
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.TextNode {
b.WriteString(c.Data)
}
}
parseMailCSSRules(b.String(), css)
// 删除 style 节点
parent := n.Parent
if parent != nil {
next := n.NextSibling
parent.RemoveChild(n)
if next != nil {
collectAndRemoveMailStyles(next, css)
}
return
}
}
for c := n.FirstChild; c != nil; {
next := c.NextSibling
collectAndRemoveMailStyles(c, css)
c = next
}
}
func parseMailCSSRules(src string, css map[string]string) {
src = mailCSSCommentRe.ReplaceAllString(src, "")
src = stripMailAtRules(src)
for _, m := range mailCSSRuleRe.FindAllStringSubmatch(src, -1) {
decls := normalizeMailDecls(m[2])
if decls == "" {
continue
}
for _, sel := range strings.Split(m[1], ",") {
sel = strings.TrimSpace(sel)
// 仅支持单一 class:.email-card
if !strings.HasPrefix(sel, ".") || strings.ContainsAny(sel, " \t>+~[:#") {
continue
}
name := strings.TrimPrefix(sel, ".")
if name == "" || strings.Contains(name, ".") {
continue
}
css[name] = mergeMailDecls(css[name], decls)
}
}
}
// 去掉 @media / @supports 等块,避免大括号干扰简单解析。
func stripMailAtRules(src string) string {
var b strings.Builder
for i := 0; i < len(src); {
if src[i] == '@' {
j := i
for j < len(src) && src[j] != '{' {
j++
}
if j >= len(src) {
break
}
depth := 0
for j < len(src) {
if src[j] == '{' {
depth++
} else if src[j] == '}' {
depth--
if depth == 0 {
j++
break
}
}
j++
}
i = j
continue
}
b.WriteByte(src[i])
i++
}
return b.String()
}
func normalizeMailDecls(s string) string {
parts := strings.Split(s, ";")
out := make([]string, 0, len(parts))
seen := map[string]int{}
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
key, _, ok := strings.Cut(p, ":")
if !ok {
continue
}
key = strings.ToLower(strings.TrimSpace(key))
if key == "" {
continue
}
// 后写覆盖先写
if idx, ok := seen[key]; ok {
out[idx] = p
continue
}
seen[key] = len(out)
out = append(out, p)
}
return strings.Join(out, ";")
}
func mergeMailDecls(base, extra string) string {
if base == "" {
return normalizeMailDecls(extra)
}
if extra == "" {
return normalizeMailDecls(base)
}
return normalizeMailDecls(base + ";" + extra)
}
func applyMailClassStyles(n *html.Node, css map[string]string) {
if n.Type == html.ElementNode {
class := ""
styleIdx := -1
styleVal := ""
for i, a := range n.Attr {
switch strings.ToLower(a.Key) {
case "class":
class = a.Val
case "style":
styleIdx = i
styleVal = a.Val
}
}
if class != "" {
var fromClass string
for _, c := range strings.Fields(class) {
if d, ok := css[c]; ok {
fromClass = mergeMailDecls(fromClass, d)
}
}
if fromClass != "" {
// 元素已有 style 优先(等价于内联覆盖 class)
merged := mergeMailDecls(fromClass, styleVal)
if styleIdx >= 0 {
n.Attr[styleIdx].Val = merged
} else {
n.Attr = append(n.Attr, html.Attribute{Key: "style", Val: merged})
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
applyMailClassStyles(c, css)
}
}
func ensureMailDocumentHead(doc *html.Node) {
var htmlNode *html.Node
for c := doc.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == "html" {
htmlNode = c
break
}
}
if htmlNode == nil {
return
}
hasLang := false
for _, a := range htmlNode.Attr {
if strings.EqualFold(a.Key, "lang") {
hasLang = true
break
}
}
if !hasLang {
htmlNode.Attr = append(htmlNode.Attr, html.Attribute{Key: "lang", Val: "zh-CN"})
}
var head *html.Node
for c := htmlNode.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == "head" {
head = c
break
}
}
if head == nil {
head = &html.Node{Type: html.ElementNode, Data: "head"}
htmlNode.InsertBefore(head, htmlNode.FirstChild)
}
hasCharset := false
for c := head.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == "meta" {
for _, a := range c.Attr {
if strings.EqualFold(a.Key, "charset") {
hasCharset = true
}
}
}
}
if !hasCharset {
meta := &html.Node{
Type: html.ElementNode,
Data: "meta",
Attr: []html.Attribute{{Key: "charset", Val: "UTF-8"}},
}
head.InsertBefore(meta, head.FirstChild)
}
}
func writeQuotedPrintableHTML(w interface{ Write([]byte) (int, error) }, body string) error {
qp := quotedprintable.NewWriter(w)
if _, err := qp.Write([]byte(body)); err != nil {
return err
}
return qp.Close()
}