fix(app): 重建仅含 HTML 邮件摘要

- 在迁移流程中新增仅含 HTML 消息摘要重建,修正旧数据中摘要保留样式内容的问题。
- 扩展 `snippetFrom` 的清理逻辑,过滤 `style`、`script`、`head`、`title`、`noscript` 等非内容标签。
- 补充相关测试,覆盖样式内容被剔除后的摘要重建行为。
This commit is contained in:
LanQin_
2026-06-24 17:34:41 +08:00
parent 2a572bf13a
commit 947e3ad248
3 changed files with 86 additions and 0 deletions
+44
View File
@@ -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, &current); 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 {
+40
View File
@@ -442,6 +442,46 @@ func TestParseMailAuthenticationResults(t *testing.T) {
}
}
func TestSnippetFromHTMLIgnoresStyleContent(t *testing.T) {
html := `<html><head><style>body { margin: 0; padding: 24px; background: #f4f4f5; }</style><title>Hidden title</title></head><body><p>蓝钦AI 余额充值成功</p></body></html>`
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 := `<html><head><style>body { margin: 0; padding: 24px; background: #f4f4f5; }</style></head><body><p>hello readable body</p></body></html>`
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-snippet@example.test>", "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())
+2
View File
@@ -53,6 +53,7 @@ func (p *HTMLPolicy) Sanitize(s string) string {
}
var emailStyleTagRe = regexp.MustCompile(`(?is)<style\b([^>]*)>(.*?)</style>`)
var htmlNonContentTagRe = regexp.MustCompile(`(?is)<(style|script|head|title|noscript)\b[^>]*>.*?</\s*(style|script|head|title|noscript)\s*>`)
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 {