From 947e3ad248d7dac53aa78fae975fa476ba4a315e Mon Sep 17 00:00:00 2001 From: LanQin_ Date: Wed, 24 Jun 2026 17:34:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(app):=20=E9=87=8D=E5=BB=BA=E4=BB=85?= =?UTF-8?q?=E5=90=AB=20HTML=20=E9=82=AE=E4=BB=B6=E6=91=98=E8=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在迁移流程中新增仅含 HTML 消息摘要重建,修正旧数据中摘要保留样式内容的问题。 - 扩展 `snippetFrom` 的清理逻辑,过滤 `style`、`script`、`head`、`title`、`noscript` 等非内容标签。 - 补充相关测试,覆盖样式内容被剔除后的摘要重建行为。 --- apps/api/internal/app/app.go | 44 +++++++++++++++++++++++++++++++ apps/api/internal/app/app_test.go | 40 ++++++++++++++++++++++++++++ apps/api/internal/app/util.go | 2 ++ 3 files changed, 86 insertions(+) diff --git a/apps/api/internal/app/app.go b/apps/api/internal/app/app.go index c8255de..88d4010 100644 --- a/apps/api/internal/app/app.go +++ b/apps/api/internal/app/app.go @@ -427,6 +427,9 @@ func (a *App) migrate(ctx context.Context) error { if err := a.migrateMessageAuthentication(ctx); err != nil { return err } + if err := a.rebuildHTMLOnlyMessageSnippets(ctx); err != nil { + return err + } if err := a.migrateUsersForTwoFactor(ctx); err != nil { return err } @@ -492,6 +495,47 @@ func (a *App) migrateMessageAuthentication(ctx context.Context) error { return nil } +func (a *App) rebuildHTMLOnlyMessageSnippets(ctx context.Context) error { + rows, err := a.db.QueryContext(ctx, `SELECT id,body_html,snippet FROM messages WHERE trim(body_text)='' AND body_html<>''`) + if err != nil { + return err + } + defer rows.Close() + type update struct { + id string + snippet string + } + updates := []update{} + for rows.Next() { + var id, bodyHTML, current string + if err := rows.Scan(&id, &bodyHTML, ¤t); err != nil { + return err + } + next := snippetFrom("", bodyHTML) + if next != current { + updates = append(updates, update{id: id, snippet: next}) + } + } + if err := rows.Err(); err != nil { + return err + } + if len(updates) == 0 { + return nil + } + now := a.now().UTC().Format(time.RFC3339Nano) + tx, err := a.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + for _, item := range updates { + if _, err := tx.ExecContext(ctx, `UPDATE messages SET snippet=?,updated_at=? WHERE id=?`, item.snippet, now, item.id); err != nil { + return err + } + } + return tx.Commit() +} + func (a *App) migrateSendQueueMessageID(ctx context.Context) error { rows, err := a.db.QueryContext(ctx, `PRAGMA table_info(send_queue)`) if err != nil { diff --git a/apps/api/internal/app/app_test.go b/apps/api/internal/app/app_test.go index 5762065..21d4968 100644 --- a/apps/api/internal/app/app_test.go +++ b/apps/api/internal/app/app_test.go @@ -442,6 +442,46 @@ func TestParseMailAuthenticationResults(t *testing.T) { } } +func TestSnippetFromHTMLIgnoresStyleContent(t *testing.T) { + html := `Hidden title

蓝钦AI 余额充值成功

` + got := snippetFrom("", html) + if strings.Contains(got, "body {") || strings.Contains(got, "margin:") || strings.Contains(got, "Hidden title") { + t.Fatalf("snippet kept non-content html text: %q", got) + } + if !strings.Contains(got, "蓝钦AI 余额充值成功") { + t.Fatalf("snippet missing body text: %q", got) + } +} + +func TestRebuildHTMLOnlyMessageSnippetsDropsStyleContent(t *testing.T) { + a := newTestApp(t) + ctx := context.Background() + _, mb := defaultAdminUserAndMailbox(t, a) + folderID, err := a.ensureFolder(ctx, mb.ID, "Sent") + if err != nil { + t.Fatal(err) + } + now := a.now().UTC().Format(time.RFC3339Nano) + bodyHTML := `

hello readable body

` + if _, err := a.db.ExecContext(ctx, `INSERT INTO messages(id,mailbox_id,folder_id,recipient_addr,message_uid,message_id,subject,from_addr,from_name,to_addrs,cc_addrs,bcc_addrs,sent_at,received_at,snippet,body_text,body_html,is_read,is_starred,has_attachments,size_bytes,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, "msg_css_snippet", mb.ID, folderID, "", newID("uid"), "", "css", mb.Address, "", "[]", "[]", "[]", now, now, "body { margin: 0; padding: 24px; }", "", bodyHTML, 1, 0, 0, int64(len(bodyHTML)), now, now); err != nil { + t.Fatal(err) + } + if err := a.rebuildHTMLOnlyMessageSnippets(ctx); err != nil { + t.Fatal(err) + } + var snippet string + if err := a.db.QueryRowContext(ctx, `SELECT snippet FROM messages WHERE id='msg_css_snippet'`).Scan(&snippet); err != nil { + t.Fatal(err) + } + if strings.Contains(snippet, "body {") || strings.Contains(snippet, "margin:") { + t.Fatalf("snippet was not rebuilt: %q", snippet) + } + if snippet != "hello readable body" { + t.Fatalf("snippet=%q, want body text", snippet) + } +} + func TestMailRulesConditionGroupsAndActions(t *testing.T) { a := newTestApp(t) ts := httptest.NewServer(a.Router()) diff --git a/apps/api/internal/app/util.go b/apps/api/internal/app/util.go index b956bec..9f19971 100644 --- a/apps/api/internal/app/util.go +++ b/apps/api/internal/app/util.go @@ -53,6 +53,7 @@ func (p *HTMLPolicy) Sanitize(s string) string { } var emailStyleTagRe = regexp.MustCompile(`(?is)]*)>(.*?)`) +var htmlNonContentTagRe = regexp.MustCompile(`(?is)<(style|script|head|title|noscript)\b[^>]*>.*?`) func extractSafeEmailStyles(value string) ([]string, string) { styles := []string{} @@ -241,6 +242,7 @@ func snippetFrom(text, html string) string { } func stripTags(s string) string { + s = htmlNonContentTagRe.ReplaceAllString(s, " ") var b strings.Builder inTag := false for _, r := range s {