What happens
Opening the Raw tab on a message with PDF attachments freezes the tab for several seconds; on a slow machine or a bigger message Chrome shows Page unresponsive.
Reproduced with a real message from a Django app that mails a contract PDF + an instructions PDF (1,548,621 bytes on the wire, ~20k lines of base64, longest line 135 chars).
Measured in the running app (localhost:8025, Chrome, M-series):
click "Raw" -> 7267 ms main thread blocked
<pre> text length: 1548621
<pre> scrollHeight: 389174 px
JS heap after: 4 MB
Heap stays tiny, so this is layout/paint, not memory: the whole raw source is dropped into one non-virtualized <pre> and the browser lays out ~20k line boxes in a single frame.
Where
ui/src/components/MessageDetail.tsx:613 — RawView renders the entire string at once:
<pre class="p-4 text-xs ... whitespace-pre-wrap font-mono leading-relaxed">
{raw()}
</pre>
There is no size cap and no virtualization anywhere in ui/src.
The server side is fine: GET /api/v1/messages/{id}/raw (crates/rustmail-api/src/handlers.rs:222) answered the 1.5 MB body in 2.5 ms.
Related, same file
ui/src/components/MessageDetail.tsx:38 — the rawSource resource is created on selectedId, so the full raw body is fetched for every selected message even when the user never opens Raw or Headers. Headers only needs the block before the first blank line.
Possible fixes
- Cap what
RawView renders (first N KB) with an explicit "showing first N KB of X — download .eml" affordance, and link exportUrl(id, "eml").
- Or chunk/virtualize the
<pre> (render slices on scroll) so layout cost is bounded.
- Independently: fetch raw lazily, only when
tab() is raw or headers — or add a ?headers_only=1 variant for the Headers tab.
What happens
Opening the Raw tab on a message with PDF attachments freezes the tab for several seconds; on a slow machine or a bigger message Chrome shows Page unresponsive.
Reproduced with a real message from a Django app that mails a contract PDF + an instructions PDF (1,548,621 bytes on the wire, ~20k lines of base64, longest line 135 chars).
Measured in the running app (
localhost:8025, Chrome, M-series):Heap stays tiny, so this is layout/paint, not memory: the whole raw source is dropped into one non-virtualized
<pre>and the browser lays out ~20k line boxes in a single frame.Where
ui/src/components/MessageDetail.tsx:613—RawViewrenders the entire string at once:There is no size cap and no virtualization anywhere in
ui/src.The server side is fine:
GET /api/v1/messages/{id}/raw(crates/rustmail-api/src/handlers.rs:222) answered the 1.5 MB body in 2.5 ms.Related, same file
ui/src/components/MessageDetail.tsx:38— therawSourceresource is created onselectedId, so the full raw body is fetched for every selected message even when the user never opens Raw or Headers. Headers only needs the block before the first blank line.Possible fixes
RawViewrenders (first N KB) with an explicit "showing first N KB of X — download .eml" affordance, and linkexportUrl(id, "eml").<pre>(render slices on scroll) so layout cost is bounded.tab()israworheaders— or add a?headers_only=1variant for the Headers tab.