feat(attachments): render inline cid images in place, not as attachments

Inline images (referenced by src="cid:…") are now classified at ingest and
kept out of the downloadable attachment lists, RSS/Atom enclosures, and the
API — while still stored in R2 and cleaned up with the email. Fixes the admin
email preview, which injected raw HTML into the data: iframe so cid refs never
resolved; it now rewrites them to absolute /files URLs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-24 14:39:59 +02:00
parent be45e70571
commit 5137637181
14 changed files with 277 additions and 31 deletions
+9 -2
View File
@@ -4,9 +4,16 @@ import { getAttachmentBucket } from "../infrastructure/attachments";
import { FeedRepository } from "../infrastructure/feed-repository";
import { FeedId } from "../domain/value-objects/feed-id";
// All R2 object ids an email owns — both downloadable attachments and inline
// images. Inline images are hidden from the user-facing lists but must still be
// purged from the bucket when the email is deleted.
export function attachmentIdsForCleanup(e: EmailMetadata): string[] {
return [...(e.attachmentIds ?? []), ...(e.inlineAttachmentIds ?? [])];
}
// Delete the R2 attachments belonging to the given email keys. Call before the
// emails are removed from feed metadata, while `emails` still carries their
// attachmentIds.
// attachment ids.
export async function deleteAttachmentsForEmails(
env: Env,
emails: readonly EmailMetadata[],
@@ -15,7 +22,7 @@ export async function deleteAttachmentsForEmails(
const keySet = new Set(keys);
const attachmentIds = emails
.filter((e) => keySet.has(e.key))
.flatMap((e) => e.attachmentIds ?? []);
.flatMap((e) => attachmentIdsForCleanup(e));
if (attachmentIds.length === 0) return;
const bucket = getAttachmentBucket(env);