Skip to content

✨(global) normalize thread/message snippet#755

Open
jbpenrath wants to merge 4 commits into
mainfrom
feat/message-preview
Open

✨(global) normalize thread/message snippet#755
jbpenrath wants to merge 4 commits into
mainfrom
feat/message-preview

Conversation

@jbpenrath

@jbpenrath jbpenrath commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Purpose

Currently we compute thread snippet naively. So if html or text body contains html or markdown tags it was not stripped and the snippet was unreadable by a human so we did not use it. Furthermore, we did not compute snippet for message but it could be useful to display it on frontend when a message is folded.

Summary by CodeRabbit

  • New Features
    • Added read-only snippet previews to API message responses, and surfaced them across thread UI (thread list items and folded message headers).
    • Added backfill_thread_snippets to rebuild existing thread snippets (dry-run, batching, limits, date cutoff).
  • Bug Fixes
    • Thread snippets now reliably track the latest visible (non-draft, non-trashed) message and fall back to empty when needed (including unreadable content).
    • Preview/snippet text is sanitized (Markdown/HTML cleaned) before truncation.
  • UI/Style
    • Improved snippet styling and display; message composer now exports textBody as plain text for cleaner previews.

@jbpenrath jbpenrath self-assigned this Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change centralizes cleaned plain-text preview generation, derives persisted thread snippets through Thread.update_stats(), adds API and backfill support, updates outbound text composition, and renders snippets in frontend thread and message views.

Changes

Snippet pipeline

Layer / File(s) Summary
Preview cleaning foundation
src/jmap-email/jmap_email/*, src/jmap-email/tests/*, src/frontend/src/features/blocknote/*
Adds shared HTML/Markdown sanitization, parser integration, plain-text block export, and extensive unit and fuzz coverage.
Backend snippet state and API
src/backend/core/mda/utils.py, src/backend/core/models.py, src/backend/core/api/*, src/backend/core/tests/models/*, src/backend/core/tests/test_utils.py
Replaces thread_snippet with message_snippet, derives thread snippets from the latest visible message, and exposes read-only message snippets through the API.
Thread lifecycle and backfill
src/backend/core/api/viewsets/thread.py, src/backend/core/mda/inbound_create.py, src/backend/core/management/commands/*, src/backend/core/migrations/*, src/backend/core/tests/*
Defers snippet calculation to statistics updates, purges legacy values, adds resumable batch backfill, and updates lifecycle tests.
Plain-text message composition
src/frontend/src/features/blocknote/*, src/frontend/src/features/forms/components/message-composer/*
Generates outbound textBody content from cleaned plain text while retaining HTML export.
Snippet presentation
src/frontend/src/features/layouts/components/thread-panel/components/thread-item/*, src/frontend/src/features/layouts/components/thread-view/components/thread-message/*
Displays thread snippets beside subjects and message snippets in folded headers with dedicated styling and line clamping.
Supporting configuration and fixtures
compose.yaml, src/backend/core/tests/mda/test_inbound.py, src/backend/core/tests/api/test_config.py
Updates development mounts and test fixtures/assertions for the revised pipeline.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Message
  participant JMAPParser
  participant preview_text
  participant Thread
  participant API
  participant UI
  Message->>JMAPParser: parse MIME content
  JMAPParser->>preview_text: clean preview text
  preview_text-->>Thread: return truncated snippet
  Thread->>Thread: persist derived snippet
  Thread->>API: expose snippet fields
  API-->>UI: return snippet data
  UI->>UI: render thread or folded-message snippet
Loading

Possibly related PRs

Suggested reviewers: sylvinus

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: normalizing thread and message snippets.
Docstring Coverage ✅ Passed Docstring coverage is 80.49% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/frontend/src/features/blocknote/markdown-exporter.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/core/management/commands/backfill_thread_snippets.py`:
- Around line 72-85: Update the --before parsing flow in the backfill command so
any datetime returned by parse_datetime is converted to an aware UTC datetime
when it is naive, while preserving existing timezone information for
already-aware values. Ensure the subsequent created_at__lt filter always
receives a timezone-aware cutoff.

In `@src/backend/core/migrations/0033_purge_thread_snippets.py`:
- Around line 14-17: Update purge_snippets to clear non-empty Thread.snippet
values in bounded, keyset-paginated batches rather than one full-table update,
reusing the batching approach and ordering used by backfill_thread_snippets
where available. Disable migration-wide atomicity so each batch commits
independently while preserving the migration’s complete purge behavior.

In `@src/backend/core/models.py`:
- Around line 1036-1038: Update the Thread model’s snippet field definition to
add an index suitable for the exact-match filters used by the backfill command
and purge migration. Keep the existing field behavior and update the
corresponding schema migration so the database index is created.
- Around line 1216-1233: Guard the Message.objects.get refetch in update_stats
when resolving last_visible, catching Message.DoesNotExist from the
snapshot/refetch race. If the message was deleted, avoid propagating the
exception and leave the existing snippet unchanged while still updating
messaged_at.

In `@src/jmap-email/jmap_email/helpers.py`:
- Line 238: Update the _HTML_TAG_RE pattern in helpers.py to match only
plausible HTML tags whose contents begin with a letter, slash, exclamation mark,
or question mark, while preserving markup stripping for valid HTML and avoiding
removal of plain-text comparisons or code expressions.

In `@src/jmap-email/jmap_email/parser.py`:
- Around line 1555-1560: Update the preview source selection in the parser
around text_body and html_body so content values are accepted only when they are
strings; treat None and all other non-string values as empty strings instead of
converting them with str(). Preserve the existing text-before-HTML fallback and
pass the resulting safe source to preview_text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fad56051-c623-4a31-ae29-75b72e511cf4

📥 Commits

Reviewing files that changed from the base of the PR and between f058ee4 and fb56f12.

⛔ Files ignored due to path filters (1)
  • src/frontend/src/features/api/gen/models/message.ts is excluded by !**/gen/**
📒 Files selected for processing (31)
  • src/backend/core/admin.py
  • src/backend/core/api/openapi.json
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/thread.py
  • src/backend/core/factories.py
  • src/backend/core/management/commands/backfill_thread_snippets.py
  • src/backend/core/mda/inbound_create.py
  • src/backend/core/mda/outbound.py
  • src/backend/core/mda/utils.py
  • src/backend/core/migrations/0033_purge_thread_snippets.py
  • src/backend/core/models.py
  • src/backend/core/tests/api/test_messages_create.py
  • src/backend/core/tests/api/test_thread_split.py
  • src/backend/core/tests/commands/test_backfill_thread_snippets.py
  • src/backend/core/tests/mda/test_inbound.py
  • src/backend/core/tests/models/test_thread_snippet.py
  • src/backend/core/tests/test_utils.py
  • src/frontend/src/features/blocknote/hooks/use-base64-composer.tsx
  • src/frontend/src/features/blocknote/markdown-exporter.test.ts
  • src/frontend/src/features/blocknote/markdown-exporter.ts
  • src/frontend/src/features/forms/components/message-composer/index.tsx
  • src/frontend/src/features/layouts/components/thread-panel/components/thread-item/_index.scss
  • src/frontend/src/features/layouts/components/thread-panel/components/thread-item/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-header.tsx
  • src/jmap-email/CHANGELOG.md
  • src/jmap-email/jmap_email/__init__.py
  • src/jmap-email/jmap_email/helpers.py
  • src/jmap-email/jmap_email/parser.py
  • src/jmap-email/tests/test_helpers.py
  • src/jmap-email/tests/test_parser.py
💤 Files with no reviewable changes (2)
  • src/backend/core/mda/outbound.py
  • src/backend/core/factories.py

Comment thread src/backend/core/management/commands/backfill_thread_snippets.py
Comment thread src/backend/core/migrations/0033_purge_thread_snippets.py Outdated
Comment thread src/backend/core/models.py
Comment thread src/backend/core/models.py
Comment thread src/jmap-email/jmap_email/helpers.py Outdated
Comment thread src/jmap-email/jmap_email/parser.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/core/migrations/0033_purge_thread_snippets.py (1)

37-46: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Disable migration-wide atomicity to ensure batches actually commit independently.

Passing atomic=False to RunPython only prevents Django from creating an inner savepoint. Because the Migration class itself does not explicitly declare atomic = False, Django's MigrationExecutor still wraps the entire migration in an outer transaction by default.

Because they share the same active outer transaction, all keyset-paginated batches will still accumulate row locks and WAL, committing simultaneously only when the script finishes. Setting atomic = False on the class itself is required to truly release locks and commit independently per batch.

⚡ Proposed fix
 class Migration(migrations.Migration):
     """Data-only migration: no schema change."""
 
+    atomic = False
+
     dependencies = [
         ("core", "0032_inboundmessage_blob_envelope"),
     ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/migrations/0033_purge_thread_snippets.py` around lines 37 -
46, Set the Migration class’s atomic attribute to False so the migration
executor does not wrap the entire purge in one transaction, allowing
purge_snippets batches to commit independently while preserving the existing
RunPython configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/core/models.py`:
- Around line 1229-1242: Extend the exception handling around snippet derivation
in the thread update logic to catch any Exception raised by
last_message.get_parsed_data() or message_snippet(). Log the failure with
logger.exception() for reporting, then set self.snippet to an empty string so
update_stats() continues without aborting; preserve the existing
Message.DoesNotExist handling.

In `@src/frontend/src/features/blocknote/markdown-exporter.ts`:
- Around line 55-62: Update the HTML_TAG_RE definition used by stripHtmlTags to
include the global g flag, ensuring all matching tags are removed in one
replacement pass. Preserve the existing stripping behavior, and add a narrowly
scoped CodeQL suppression only if the output is confirmed never to reach
dangerouslySetInnerHTML or another HTML sink.
- Around line 78-85: Update the markdown processing around CODE_FENCE_RE and
stripHtmlTags so HTML-like text within fenced code blocks is preserved while
ordinary markdown content still has tags stripped. Preserve fenced code segments
separately or otherwise exclude them from stripHtmlTags, then apply the existing
link, image, autolink, and line-break replacements to the remaining content.

---

Outside diff comments:
In `@src/backend/core/migrations/0033_purge_thread_snippets.py`:
- Around line 37-46: Set the Migration class’s atomic attribute to False so the
migration executor does not wrap the entire purge in one transaction, allowing
purge_snippets batches to commit independently while preserving the existing
RunPython configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81961da1-c0b5-4753-ad24-28b70154f530

📥 Commits

Reviewing files that changed from the base of the PR and between fb56f12 and 7f3c3b7.

📒 Files selected for processing (5)
  • src/backend/core/migrations/0033_purge_thread_snippets.py
  • src/backend/core/models.py
  • src/frontend/src/features/blocknote/markdown-exporter.test.ts
  • src/frontend/src/features/blocknote/markdown-exporter.ts
  • src/jmap-email/jmap_email/parser.py

Comment thread src/backend/core/models.py
Comment thread src/frontend/src/features/blocknote/markdown-exporter.ts
Comment thread src/frontend/src/features/blocknote/markdown-exporter.ts Outdated
@jbpenrath
jbpenrath force-pushed the feat/message-preview branch from 4fa629a to 5a97599 Compare July 21, 2026 09:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/backend/core/models.py (1)

1036-1038: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Snippet field still lacks an index for exact-match filtering.

snippet is filtered by exact match (snippet="") in both backfill_thread_snippets.py and migration 0033_purge_thread_snippets.py, but the field still has no db_index/index. This was raised in a prior review round and wasn't marked as addressed.

As per coding guidelines, src/backend/**/{models.py,migrations/**/*.py} should "Implement database indexing and query optimization (Model Meta indexes, constraints)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/models.py` around lines 1036 - 1038, Update the snippet
field declaration in the model containing snippet to add a database index for
exact-match filtering, using the project’s established indexing convention.
Ensure the generated migration updates the database schema consistently, without
changing the field’s existing text, blank, or update behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/backend/core/models.py`:
- Around line 1036-1038: Update the snippet field declaration in the model
containing snippet to add a database index for exact-match filtering, using the
project’s established indexing convention. Ensure the generated migration
updates the database schema consistently, without changing the field’s existing
text, blank, or update behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 330d1a5c-09e9-43b3-9a0a-6b76061fa3bc

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa629a and 5a97599.

⛔ Files ignored due to path filters (1)
  • src/frontend/src/features/api/gen/models/message.ts is excluded by !**/gen/**
📒 Files selected for processing (35)
  • compose.yaml
  • src/backend/core/admin.py
  • src/backend/core/api/openapi.json
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/thread.py
  • src/backend/core/factories.py
  • src/backend/core/management/commands/backfill_thread_snippets.py
  • src/backend/core/mda/inbound_create.py
  • src/backend/core/mda/outbound.py
  • src/backend/core/mda/utils.py
  • src/backend/core/migrations/0033_purge_thread_snippets.py
  • src/backend/core/models.py
  • src/backend/core/tests/api/test_config.py
  • src/backend/core/tests/api/test_messages_create.py
  • src/backend/core/tests/api/test_thread_split.py
  • src/backend/core/tests/commands/test_backfill_thread_snippets.py
  • src/backend/core/tests/mda/test_inbound.py
  • src/backend/core/tests/models/test_thread_snippet.py
  • src/backend/core/tests/test_utils.py
  • src/frontend/src/features/blocknote/hooks/use-base64-composer.tsx
  • src/frontend/src/features/blocknote/markdown-exporter.test.ts
  • src/frontend/src/features/blocknote/markdown-exporter.ts
  • src/frontend/src/features/forms/components/message-composer/index.tsx
  • src/frontend/src/features/layouts/components/thread-panel/components/thread-item/_index.scss
  • src/frontend/src/features/layouts/components/thread-panel/components/thread-item/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-header.tsx
  • src/jmap-email/CHANGELOG.md
  • src/jmap-email/README.md
  • src/jmap-email/jmap_email/__init__.py
  • src/jmap-email/jmap_email/helpers.py
  • src/jmap-email/jmap_email/parser.py
  • src/jmap-email/tests/test_helpers.py
  • src/jmap-email/tests/test_parser.py
  • src/jmap-email/tests/test_preview_fuzz.py
💤 Files with no reviewable changes (2)
  • src/backend/core/factories.py
  • src/backend/core/mda/outbound.py

Use the new preview_text method of jmap_email to generate
clean snippet for thread (denormalized at update_stats)
and message (at serialization). Now when a mesage is folded, we
display this snippet. In the thread list we also display this snippet above
the subject.
Upgrade to version 0.2.0
@jbpenrath
jbpenrath force-pushed the feat/message-preview branch from 5096075 to 6414310 Compare July 23, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants