# Forwarding an HTML message with its formatting Item 171. Design, 2026-08-27. **Read this before the backlog row**, which records only the cause. ## The defect `ComposeContextBuilder::quoteBody()` reads `ParsedMessage::plainBody` and nothing else, so the forward path drops the original's `htmlBody` entirely. Two consequences, and they are not the same severity: - An original with both parts forwards its text/plain alternative. The words survive, the sender's formatting does not. This is what the user reported. - **An original with an HTML part ONLY has an empty `plainBody`**, so the forward carries an attribution line and an empty quote. The content is gone and nothing says so. Measured on the developer's own inbox 2026-08-27: **30 of 342 sampled messages** (~9%) declare `text/html` with no `text/plain` part. The silent half is not an edge case. ## What was decided, and by whom The user chose, 2026-08-27, from three routes put to them: 1. **Inline, carrying the original's markup** — CHOSEN. The forward carries the original's own HTML rather than a flattened quote. Highest inline fidelity, and the most work, because it is the only route that puts a stranger's markup into an outgoing message. **Amended 2026-08-27, after the first build**: a forward sends ONE part, not a `multipart/alternative`. The Send-as-HTML toggle chooses which — the original's markup when on, the text quote when off. The user's reasoning is that a forward's shape is something they have already decided by flipping that toggle, and sending both halves hands the choice to the recipient's client instead. An ordinary (non-forward) message still sends the alternative as before; only the forward path is single-part. 2. Attach the original as `message/rfc822` (item 130's mechanism). Rejected here, though item 130 may still build it for its own sake: the user wants the content inline, not as an attachment. 3. Text fallback only. Rejected: it fixes the silent-loss half and does not answer the note at all, since formatting is still lost on every forward. **Remote content is STRIPPED BY DEFAULT, with a per-forward opt-out**, also the user's choice against "always strip" and "keep everything". A control in the composer, checked by default, reading roughly "Strip remote content from the forwarded message". ## Why this is the security-critical item in the backlog Every other HTML path in this application renders a stranger's markup *to the user*, behind protections that live in `MessageView`: an off-the-record profile, JavaScript disabled, and `RequestInterceptor` blocking every request by default and failing closed. **None of those protections apply here.** The markup leaves this process and is rendered by somebody else's mail client, under their policy, on their machine. The interceptor cannot help: it intercepts requests *we* would make. So the sanitising has to happen to the bytes, before they are handed to `MessageSender`, and there is no second line of defence behind it. The concrete harm, in the user's own words when the decision was put to them: forwarding a tracking pixel forwards the tracking. The original sender learns that the forwarded copy was opened, by whom, and how many times, and the user's recipient never consented to that. ## The allow-list rule, which is not negotiable `HtmlBuilder::namespaceCids()` is the closest prior art and it is a **block-list**: it names the attributes that can carry a `cid:` and rewrites those. It documents scoping `srcset=` out, on the reasoning that its quoting grammar differs and `cid:` in `srcset` is not seen in the wild. **That trade is correct for rewriting and WRONG for stripping**, and the asymmetry is the whole design: | | a missed reference means | |---|---| | `namespaceCids` (rewrite) | one broken image | | this sanitiser (strip) | a tracking beacon reaching the recipient | So the sanitiser must **allow-list what may remain**, not block-list what must go. Anything not recognised is removed. A new HTML attribute, a quoting form not anticipated, a `srcset`, a CSS `image-set()`, an `@import` — each is handled by the default, which is removal, rather than by having been enumerated in advance. Stated as the invariant to test against: **after sanitising, no attribute value and no CSS construct in the output may contain a URL whose scheme is anything other than `cid:`, and no element that fetches may remain without one.** ## What is kept and what goes Kept: - `cid:` references. They travel inside the message, fetch nothing, and are what makes an inline logo survive. Item 129 will need the same machinery. - Structural and presentational markup: tables, lists, headings, spans, `style=""` attributes with their remote constructs removed. Removed: - Any `src`, `href`, `background`, `poster`, `srcset`, `data-*` or other attribute value carrying a non-`cid:` URL scheme. `http:`, `https:`, `//host/path`, `data:` (which can carry markup), `file:` above all. - ``, `