Wire inline images through the whole send pipeline.
Compose UI: an image-picker (image/*, persistable URI grant, mirroring the
attachment picker) behind a new toolbar button appended at the END of the
toolbar — after the block/link buttons and the font/size/align controls — so it
never shifts the bullet button the compose E2E taps without scrolling. Picking
an image adds an inline OutgoingAttachment and hands the editor a
PendingInlineImage, which RichTextEditing.insertImage drops as a [image: name]
token + RichImage(contentId) at the caret. Deleting the token drops the image:
onBodyChange reconciles inline attachments against the body's surviving cid:
references. Inline images are tracked in ComposeUiState alongside regular
attachments but kept out of the attachment-chip row.
Domain/persistence: OutgoingAttachment gains contentId/isInline; the shared
draft/outbox attachment JSON carries them (drafts need no migration — an older
draft reads back as a plain attachment). The outbox stages files by index as
before but now also stores per-file {contentId,isInline} metadata in a new
OutboxEntity.attachments column (Room 17 -> 18, MIGRATION_17_18, DEFAULT '' per
the bccAddresses precedent so fresh-install == migrated; 18.json committed). The
send worker pairs each staged file with its metadata by index (with a positional
fallback for messages queued before the column existed).
SMTP (SmtpSender): inline images wrap the body in a multipart/related, each with
a Content-ID matching the HTML's cid: and inline disposition; regular
attachments keep today's multipart/mixed shape.
Graph (GraphSender): inline fileAttachments carry isInline + contentId.
Tests: GreenMail asserts multipart/related with a Content-ID matching the cid;
GraphSenderTest asserts the inline payload; a mapper test proves an inline
image's cid<->file pairing round-trips a draft save/reopen; RichTextEditing
tests cover insertImage (token/RichImage placement + offset shift + html
round-trip); MigrationTest gains migrate17To18. Reader-side cid: rendering stays
out of scope (follow-up).
Closes#77
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>