Skip to content

feat: expose HTML email content and inline MIME parts via API#301

Open
sarcasticbird wants to merge 2 commits intowesm:mainfrom
sarcasticbird:feature/html-email-content-api
Open

feat: expose HTML email content and inline MIME parts via API#301
sarcasticbird wants to merge 2 commits intowesm:mainfrom
sarcasticbird:feature/html-email-content-api

Conversation

@sarcasticbird
Copy link
Copy Markdown

Summary

  • Add GetMessageRaw to the query engine interface for raw MIME retrieval with zlib decompression (SQLite, DuckDB, remote stub, mock)
  • When the engine is available, GET /messages/{id} returns body_html alongside body for rich email rendering
  • Add GET /messages/{id}/inline/{cid} endpoint for serving CID-referenced inline images embedded in HTML emails
  • Inline endpoint blocks SVG/XHTML content types, sets Cache-Control: private, and filters on IsInline flag

Motivation

query.MessageDetail already has separate BodyText and BodyHTML fields, but the API collapses them into a single body via the store layer. Clients that want to render rich HTML emails need the HTML body and access to inline MIME parts (images referenced via cid: URLs). This adds both without touching the store layer — the engine path is preferred when available, with a clean fallback to the existing store path.

Changed files

Layer Files Change
Engine interface engine.go, shared.go, sqlite.go, duckdb.go, remote/engine.go, mock_engine.go GetMessageRaw method + shared zlib decompression
API handlers handlers.go, server.go Engine-path body_html in message detail, inline MIME endpoint
Tests sqlite_crud_test.go, handlers_test.go GetMessageRaw CRUD tests, inline endpoint tests (PNG, SVG rejection, XHTML rejection, CID not found, no engine, message not found), engine-path body_html test

Add GetMessageRaw to the query engine interface for raw MIME retrieval
with zlib decompression. When the engine is available, handleGetMessage
returns body_html alongside body for rich email rendering. Add inline
MIME parts endpoint (GET /messages/{id}/inline/{cid}) for serving
CID-referenced images embedded in HTML emails.
- Block SVG and non-image content types in inline MIME endpoint
- Use normalized Content-Type in response header (strip MIME params)
- Filter on IsInline to prevent serving regular attachments
- Set Cache-Control: private (user-specific content) and Content-Disposition: inline
- Wrap bare errors in getMessageRawShared with message ID context
- Add tests: engine-path body_html, inline PNG, SVG/XHTML rejection,
  CID not found, no engine, message not found
@roborev-ci
Copy link
Copy Markdown

roborev-ci Bot commented Apr 27, 2026

roborev: Combined Review (04866a1)

Medium issue found: inline MIME content can bypass the deleted-message visibility filter.

Medium

  • internal/api/handlers.go:1627
    The inline endpoint fetches message_raw directly by message ID, so it can serve inline content for messages hidden elsewhere after deleted_from_source_at is set.
    Fix: Before loading raw MIME, verify the message is visible through the same message-detail path, or make GetMessageRaw join messages and apply the same non-deleted filter.

Synthesized from 3 reviews (agents: codex, gemini | types: default, security)

@sarcasticbird
Copy link
Copy Markdown
Author

The inline endpoint is consistent with the existing `GetMessage` detail endpoint — neither filters on `deleted_from_source_at`. The `getMessageByQueryShared` function (used by `GetMessage(id)`) has no deleted-message filter; it uses a bare `WHERE m.id = ?`.

The `deleted_from_source_at` filtering is applied at the list/search/aggregate query level (`MessageFilter.HideDeletedFromSource`), not on single-message lookups. This is intentional — if a client can view a message's detail, it should also be able to view its inline parts. Adding a deleted filter to the inline endpoint but not to `GetMessage` would be inconsistent and wouldn't actually prevent access to the message content.

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.

1 participant