Inline images in rich HTML emails — embedded via Content-ID + <img src="cid:..."> (e.g. USPS Informed Delivery digests) — were listed under Attachments with a download chip and never rendered in the message body. Two independent bugs combined; both are fixed together.
Fixes
1. Misclassification (ImapClient).isAttachment() treated any part with a filename as a downloadable attachment, sweeping inline images (which carry a filename and a Content-ID under Content-Disposition: inline) into the attachment list. Classification now treats a part as a downloadable attachment only when its disposition is attachment, or it has a filename but noContent-ID. Inline images are collected with their Content-ID and excluded from the displayed list (AttachmentDao.observeForMessage filters contentId IS NULL; getForMessage still returns everything so their bytes remain fetchable). The Content-ID is read via MimePart.getContentID() (from the already-fetched IMAP BODYSTRUCTURE) rather than a raw getHeader("Content-ID"), which Angus leaves unpopulated on IMAP parts without a separate per-part MIME-header fetch.
2. No rendering path (HtmlBody). The WebView's WebViewClient now overrides shouldInterceptRequest to resolve cid:<id> requests to the matching part's bytes with the correct MIME type — backing the CSP's existing img-src ... cid: allowance (inline images render even while remote images are blocked, since they are embedded, not remote). The resolver is factored into pure, unit-testable functions (cidKey / resolveInlineImage).
Content-ID plumbing. Threaded end-to-end: AttachmentPart → Attachment → AttachmentEntity, plus MailRepository.inlineImages(messageId) (downloads/caches inline bytes, reusing the attachment cache) and ReaderViewModel, which surfaces the cid → bytes map to HtmlBody.
Schema migration
Adds attachments.contentId (nullable): v16 → v17, MIGRATION_16_17, registered in DatabaseModule, 17.json exported. Existing rows read null (treated as ordinary attachments).
Tests
MIME-part classification: inline+Content-ID excluded; real attachment, attachment-disposition, and filename-without-cid all kept; Content-ID normalization.
GreenMail multipart/related round-trip through fetchBodyPeek (inline image split from a real PDF attachment).
Repository inlineImages() (resolves cid parts to cached bytes, excludes real attachments).
DAO display filter (inline hidden from observeForMessage, kept in getForMessage).
MIGRATION_16_17 (plus the auto-discovered chain-replay suite).
Reader UI
ReaderScreen needed only pass-through wiring of inlineImages to HtmlBody; the #134 attachment accordion is untouched (it renders whatever list it receives, now correctly excluding inline images).
Companion to #77 (compose-side inline images); this is the reader-side follow-up.
## Problem
Inline images in rich HTML emails — embedded via `Content-ID` + `<img src="cid:...">` (e.g. USPS Informed Delivery digests) — were listed under **Attachments** with a download chip and never rendered in the message body. Two independent bugs combined; both are fixed together.
## Fixes
**1. Misclassification (`ImapClient`).** `isAttachment()` treated any part with a filename as a downloadable attachment, sweeping inline images (which carry a filename **and** a `Content-ID` under `Content-Disposition: inline`) into the attachment list. Classification now treats a part as a downloadable attachment only when its disposition is `attachment`, **or** it has a filename but **no** `Content-ID`. Inline images are collected with their `Content-ID` and excluded from the displayed list (`AttachmentDao.observeForMessage` filters `contentId IS NULL`; `getForMessage` still returns everything so their bytes remain fetchable). The Content-ID is read via `MimePart.getContentID()` (from the already-fetched IMAP BODYSTRUCTURE) rather than a raw `getHeader("Content-ID")`, which Angus leaves unpopulated on IMAP parts without a separate per-part MIME-header fetch.
**2. No rendering path (`HtmlBody`).** The WebView's `WebViewClient` now overrides `shouldInterceptRequest` to resolve `cid:<id>` requests to the matching part's bytes with the correct MIME type — backing the CSP's existing `img-src ... cid:` allowance (inline images render even while remote images are blocked, since they are embedded, not remote). The resolver is factored into pure, unit-testable functions (`cidKey` / `resolveInlineImage`).
**Content-ID plumbing.** Threaded end-to-end: `AttachmentPart` → `Attachment` → `AttachmentEntity`, plus `MailRepository.inlineImages(messageId)` (downloads/caches inline bytes, reusing the attachment cache) and `ReaderViewModel`, which surfaces the `cid → bytes` map to `HtmlBody`.
## Schema migration
Adds `attachments.contentId` (nullable): **v16 → v17**, `MIGRATION_16_17`, registered in `DatabaseModule`, `17.json` exported. Existing rows read `null` (treated as ordinary attachments).
## Tests
- MIME-part classification: inline+Content-ID excluded; real attachment, attachment-disposition, and filename-without-cid all kept; Content-ID normalization.
- GreenMail `multipart/related` round-trip through `fetchBodyPeek` (inline image split from a real PDF attachment).
- `cid → bytes` resolver (cid parsing, remote/unknown → null).
- Repository `inlineImages()` (resolves cid parts to cached bytes, excludes real attachments).
- DAO display filter (inline hidden from `observeForMessage`, kept in `getForMessage`).
- `MIGRATION_16_17` (plus the auto-discovered chain-replay suite).
## Reader UI
`ReaderScreen` needed only pass-through wiring of `inlineImages` to `HtmlBody`; the #134 attachment accordion is untouched (it renders whatever list it receives, now correctly excluding inline images).
Companion to #77 (compose-side inline images); this is the reader-side follow-up.
Closes #133
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Inline images in rich HTML emails — embedded via
Content-ID+<img src="cid:...">(e.g. USPS Informed Delivery digests) — were listed under Attachments with a download chip and never rendered in the message body. Two independent bugs combined; both are fixed together.Fixes
1. Misclassification (
ImapClient).isAttachment()treated any part with a filename as a downloadable attachment, sweeping inline images (which carry a filename and aContent-IDunderContent-Disposition: inline) into the attachment list. Classification now treats a part as a downloadable attachment only when its disposition isattachment, or it has a filename but noContent-ID. Inline images are collected with theirContent-IDand excluded from the displayed list (AttachmentDao.observeForMessagefilterscontentId IS NULL;getForMessagestill returns everything so their bytes remain fetchable). The Content-ID is read viaMimePart.getContentID()(from the already-fetched IMAP BODYSTRUCTURE) rather than a rawgetHeader("Content-ID"), which Angus leaves unpopulated on IMAP parts without a separate per-part MIME-header fetch.2. No rendering path (
HtmlBody). The WebView'sWebViewClientnow overridesshouldInterceptRequestto resolvecid:<id>requests to the matching part's bytes with the correct MIME type — backing the CSP's existingimg-src ... cid:allowance (inline images render even while remote images are blocked, since they are embedded, not remote). The resolver is factored into pure, unit-testable functions (cidKey/resolveInlineImage).Content-ID plumbing. Threaded end-to-end:
AttachmentPart→Attachment→AttachmentEntity, plusMailRepository.inlineImages(messageId)(downloads/caches inline bytes, reusing the attachment cache) andReaderViewModel, which surfaces thecid → bytesmap toHtmlBody.Schema migration
Adds
attachments.contentId(nullable): v16 → v17,MIGRATION_16_17, registered inDatabaseModule,17.jsonexported. Existing rows readnull(treated as ordinary attachments).Tests
multipart/relatedround-trip throughfetchBodyPeek(inline image split from a real PDF attachment).cid → bytesresolver (cid parsing, remote/unknown → null).inlineImages()(resolves cid parts to cached bytes, excludes real attachments).observeForMessage, kept ingetForMessage).MIGRATION_16_17(plus the auto-discovered chain-replay suite).Reader UI
ReaderScreenneeded only pass-through wiring ofinlineImagestoHtmlBody; the #134 attachment accordion is untouched (it renders whatever list it receives, now correctly excluding inline images).Companion to #77 (compose-side inline images); this is the reader-side follow-up.
Closes #133
🤖 Generated with Claude Code