# Rendering HTML email safely

Connect renders HTML through `mail_render.py`, which strips the markup that could act — script, embedded objects, event handlers, styling that escapes the message frame — and rewrites every remote image to load through a proxy. What you read is the sender's words and layout, not the sender's code, and opening a message does not report back to whoever sent it.

- **Status:** Available
- **Audience:** both
- **Channels:** email
- **In the app:** #/inbox
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/email/html-email/

## The problem with rendering someone else's HTML

An HTML email is a document written by a stranger and displayed inside your own authenticated session. Two separate things can go wrong with that. The first is execution: markup that runs, loads or navigates does work on your behalf inside a page that is already signed in. The second is disclosure: a document that fetches anything from the sender's server at render time tells the sender the exact moment a human looked at it.

Connect treats these as two problems with two answers, because they have different failure modes. Sanitising handles the first. Proxying handles the second, and is covered in full on [Remote images in email](/docs/email/remote-images/).

## What the sanitiser takes out

| Group | Why it goes | What survives |
|---|---|---|
| Anything that executes | Script, event handlers and embedded objects run with the privileges of the page showing them | Nothing from this group; the text they surrounded is kept |
| Remote loads | Every fetch at render time is a signal back to the sender | Images, rewritten to a proxied address |
| Frame-escaping layout | Positioning and sizing that leaves the message region can cover the application's own controls | Colour, weight, alignment and the table layout most senders rely on |
| Malformed structure | Unbalanced markup renders differently in every parser, which is what a spoofed message exploits | A re-serialised, well-formed document |

The exact rule set lives in `mail_render.py` and is the single place it lives — the Conversations screen does not have its own softer version, and neither does the Owner's. That matters more than the specific list: a sanitiser with two implementations is a sanitiser with one bypass.

> **Note** Sanitising is applied on the way to the screen, not on the way into storage. The canonical message keeps the body the provider delivered, so a later audit can show what actually arrived rather than what was considered safe to display on the day.

## What you end up looking at

A message that used a normal marketing template still looks like that template: headings, columns, a logo, a coloured button. The button is text and a link rather than something that navigates on its own, and the logo is being served through the proxy. A message that was mostly a tracking pixel and a redirect looks like almost nothing, which is an accurate rendering of what it was.

Where a sender supplied both an HTML part and a plain-text part, the plain-text part is the more reliable thing to quote back into a reply, and it is what the model works from when the HTML carries little beyond presentation. The multipart structure that makes this possible is described in the MIME page under [Technology reference](/docs/technology/).

## The edges

**A message renders as a blank block** — Its content was inside something removed — commonly an embedded object or a script-built layout. The plain-text part, if there is one, is the fallback.
**A logo or signature image does not appear** — The proxy could not retrieve it: the host is gone, is slow, or refuses a request that does not carry the original recipient's headers. The message is intact; one asset is not.
**The layout is narrower than the sender intended** — Frame-escaping width was removed. Nothing was lost from the text.
**A link points somewhere unexpected** — Sanitising does not rewrite link destinations to make them safe, and never claims to. The address you see is the address in the message; judge it as you would in any client.

> **Careful** A sanitised message is safe to *display*. It is not evidence that the message is honest. Instructions inside a message body — 'reply with the account details', 'ignore your standing instructions' — are content, not commands, and are treated as content by the drafting path. That threat is set out in [Prompt injection arrives as ordinary business mail](/research/prompt-injection-in-business-mail/).

## Questions

### Can I turn the sanitiser off to see the original message?

No, and there is no per-message override. The stored canonical body still holds what arrived, so an administrator investigating a specific message has the original available; the reading surface does not offer an unsafe mode, because a switch that exists gets used on exactly the message where it should not be.

### Does the model see the same thing I see?

It works from the text of the message, not from the rendered layout, so styling that was removed makes no difference to a draft. What does make a difference is a message whose meaning lived in an image, since there is no text to read; those are the threads worth checking before approving a reply.

### Does opening a message mark it read at the provider?

Marking read is a write-back, handled separately and described on [Writing back to Gmail](/docs/email/gmail-writeback/). Rendering a message in Connect and changing its state at the provider are different actions.

## Related

- [Remote images in email](https://connectbyjbrh.com/docs/email/remote-images/)
- [Email attachments](https://connectbyjbrh.com/docs/email/attachments/)
- [From a provider message to a canonical one](https://connectbyjbrh.com/docs/email/message-canonicalisation/)
- [Prompt injection arrives as ordinary business mail](https://connectbyjbrh.com/research/prompt-injection-in-business-mail/)
- [Technology reference](https://connectbyjbrh.com/docs/technology/)

## What this page is based on

- CHANNELS.md §1 — rendering and privacy
- `backend/app/mail_render.py` — the sanitiser
- Connect capability registry (docs-source/facts.py) — `html_mail_render`
