Skip to content

Render inline mail images in the body, drop the ones nothing references - #378

Merged
KrisBraun merged 1 commit into
mainfrom
inline-email-images
Aug 7, 2026
Merged

Render inline mail images in the body, drop the ones nothing references#378
KrisBraun merged 1 commit into
mainfrom
inline-email-images

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A mail client embeds an image it attached to a message by pointing at its MIME part — <img src="cid:...">. Mail connectors treated every such part as an ordinary attachment, which produced two problems.

Inline images never rendered inline. An image the body still references was listed as an attachment below the message rather than appearing where the sender placed it, and since connectors report no dimensions for these parts it rendered in a fixed preview box at nothing like its real size. The Outlook connector filtered inline parts out altogether, so a screenshot pasted into a message never appeared at all.

Trimmed quotes left orphans behind. Connectors cut quoted history out of a reply's body, but classified attachments against the raw message. A sender's signature image is re-embedded by every reply in a chain, so each message carried a part whose only reference sat in the quote that had just been removed. Those orphans became an attachment on every message in a long thread — frequently a blank placeholder the reader had never seen, because the sender's own mail gateway had already stripped the image.

What changed

Parts are classified against the body that survives trimming:

PartResult
Referenced by the retained bodyfileRef carries the part's contentId; Plot renders it inline where the sender put it
Not referencedDropped — nowhere to render it, and listing it shows a file the reader cannot place
No HTML body to test againstLeft as an attachment rather than silently discarded

That last row matters: a plain-text message carries no cid: references to match against, so absence of a reference is not evidence the part is an orphan.

SDK

  • contentId on ActionType.fileRef — set it only when the note's content still references the part. A non-null value tells Plot to render the image in the body instead of appending an attachment chip.
  • contentId and inline on ImapMessage.attachments. A multipart/related image routinely omits Content-Disposition entirely, so a bare Content-ID is itself the signal that the part belongs to the body.
  • referencedContentIds and normalizeContentId on the signals entry point, so every mail connector shares one rule rather than three approximations of it.

Connectors

  • Gmail reads Content-ID / Content-Disposition per part and classifies against the post-trim body.
  • Outlook requests contentId in the attachments $select and replaces its blanket !isInline filter. This is what makes a pasted inline image appear at all.
  • Apple Mail carries the BODYSTRUCTURE Content-ID (body-fields index 3) and disposition type through to ImapAttachment.

Tests

Each connector gains coverage for the three outcomes — referenced, orphaned, and no-HTML-body — plus parser-level tests for Content-ID extraction and the bare-Content-ID inline case. google 363, outlook 145, apple 456, all green; plot lint clean in all three.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AZjF9kXrBHeX4415ifBXvZ

A mail client embeds an image it attached to a message by pointing at its
MIME part: `<img src="cid:...">`. Mail connectors treated every such part as
an ordinary attachment, which produced two problems.
An image the body still references was listed below the message instead of
appearing where the sender placed it — and because connectors report no
dimensions for it, it rendered in a fixed preview box at nothing like its
real size. The Outlook connector dropped these parts entirely, so a
screenshot pasted into a message never showed up at all.
Worse, connectors trim quoted history out of a reply's body but classified
attachments against the raw message. A sender's signature image is
re-embedded by every reply in a chain, so each message carried a part whose
only reference sat in the quote that had just been removed. Those orphans
became an attachment on every message in a long thread — often a blank
placeholder the reader had never seen, because the sender's own mail gateway
had already stripped the image.
Parts are now classified against the body that survives trimming:
- Referenced by the retained body -> the fileRef action carries the part's
`contentId`, and Plot renders the image inline where the sender put it.
- Not referenced -> dropped. There is nowhere to render it, and listing it
as an attachment shows a file the reader cannot place.
- No HTML body to test against (a plain-text message) -> left as an
attachment rather than silently discarded.
Adds `contentId` to `ActionType.fileRef`, `contentId`/`inline` to
`ImapMessage.attachments`, and `referencedContentIds`/`normalizeContentId`
to the signals entry point so all three mail connectors share one rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZjF9kXrBHeX4415ifBXvZ
@KrisBraun
KrisBraun merged commit 75d415a into mainAug 7, 2026
1 check passed
@KrisBraun
KrisBraun deleted the inline-email-images branch August 7, 2026 04:29
Sign up for freeto 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

@KrisBraun