fix(app): 重建仅含 HTML 邮件摘要
- 在迁移流程中新增仅含 HTML 消息摘要重建,修正旧数据中摘要保留样式内容的问题。 - 扩展 `snippetFrom` 的清理逻辑,过滤 `style`、`script`、`head`、`title`、`noscript` 等非内容标签。 - 补充相关测试,覆盖样式内容被剔除后的摘要重建行为。
This commit is contained in:
@@ -427,6 +427,9 @@ func (a *App) migrate(ctx context.Context) error {
|
|||||||
if err := a.migrateMessageAuthentication(ctx); err != nil {
|
if err := a.migrateMessageAuthentication(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := a.rebuildHTMLOnlyMessageSnippets(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := a.migrateUsersForTwoFactor(ctx); err != nil {
|
if err := a.migrateUsersForTwoFactor(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -492,6 +495,47 @@ func (a *App) migrateMessageAuthentication(ctx context.Context) error {
|
|||||||
return nil
|
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 {
|
func (a *App) migrateSendQueueMessageID(ctx context.Context) error {
|
||||||
rows, err := a.db.QueryContext(ctx, `PRAGMA table_info(send_queue)`)
|
rows, err := a.db.QueryContext(ctx, `PRAGMA table_info(send_queue)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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) {
|
func TestMailRulesConditionGroupsAndActions(t *testing.T) {
|
||||||
a := newTestApp(t)
|
a := newTestApp(t)
|
||||||
ts := httptest.NewServer(a.Router())
|
ts := httptest.NewServer(a.Router())
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func (p *HTMLPolicy) Sanitize(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var emailStyleTagRe = regexp.MustCompile(`(?is)<style\b([^>]*)>(.*?)</style>`)
|
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) {
|
func extractSafeEmailStyles(value string) ([]string, string) {
|
||||||
styles := []string{}
|
styles := []string{}
|
||||||
@@ -241,6 +242,7 @@ func snippetFrom(text, html string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stripTags(s string) string {
|
func stripTags(s string) string {
|
||||||
|
s = htmlNonContentTagRe.ReplaceAllString(s, " ")
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
inTag := false
|
inTag := false
|
||||||
for _, r := range s {
|
for _, r := range s {
|
||||||
|
|||||||
Reference in New Issue
Block a user