package mail import ( "context" "github.com/ultisuite/ulti-backend/internal/mail/sanitize" ) const resanitizeBatchSize = 200 type ResanitizeBodiesResult struct { Scanned int `json:"scanned"` Updated int `json:"updated"` } // ResanitizeAccountBodies re-applies email HTML sanitization to stored messages. func (s *Service) ResanitizeAccountBodies(ctx context.Context, externalID, accountID string) (ResanitizeBodiesResult, error) { if err := s.verifyAccountOwnership(ctx, externalID, accountID); err != nil { return ResanitizeBodiesResult{}, err } return s.ResanitizeAccountBodiesByID(ctx, accountID) } // ResanitizeAccountBodiesByID re-sanitizes messages without an ownership check (CLI/admin). func (s *Service) ResanitizeAccountBodiesByID(ctx context.Context, accountID string) (ResanitizeBodiesResult, error) { var result ResanitizeBodiesResult var lastID string for { rows, err := s.db.Query(ctx, ` SELECT id, body_html FROM messages WHERE account_id = $1 AND body_html <> '' AND ($2 = '' OR id > $2::uuid) ORDER BY id LIMIT $3 `, accountID, lastID, resanitizeBatchSize) if err != nil { return result, err } batchCount := 0 for rows.Next() { var id, bodyHTML string if err := rows.Scan(&id, &bodyHTML); err != nil { rows.Close() return result, err } batchCount++ result.Scanned++ lastID = id sanitized := sanitize.SanitizeHTML(bodyHTML) if sanitized == bodyHTML { continue } if _, err := s.db.Exec(ctx, ` UPDATE messages SET body_html = $2, updated_at = NOW() WHERE id = $1 `, id, sanitized); err != nil { rows.Close() return result, err } result.Updated++ } if err := rows.Err(); err != nil { return result, err } rows.Close() if batchCount < resanitizeBatchSize { break } } return result, nil }