fix: send the quoted inline images along with replies and forwards

This commit is contained in:
Linus Rath
2026-10-29 01:53:59 +01:00
parent d2bb479ac6
commit 913b37661f
2 changed files with 41 additions and 6 deletions
+22
View File
@@ -27,3 +27,25 @@ describe('replyComposeParams', () => {
expect(replyComposeParams('reply', source, undefined, ' ')?.prefillBody).toBeUndefined();
});
});
describe('replyComposeParams: quoted inline images', () => {
const quotedImage = { blobId: 'b-img', type: 'image/png', name: 'logo.png', disposition: 'inline', cid: '<logo@x>' };
const unusedInline = { blobId: 'b-sig', type: 'image/png', name: 'sig.png', disposition: 'inline', cid: 'sig@x' };
const file = { blobId: 'b-pdf', type: 'application/pdf', name: 'report.pdf', disposition: 'attachment' };
const withImage = {
...source,
htmlBody: [{ partId: '2', type: 'text/html' }],
textBody: [{ partId: '1', type: 'text/plain' }],
bodyValues: { 1: { value: 'Hi' }, 2: { value: '<p>Hi <img src="cid:logo@x"></p>' } },
attachments: [quotedImage, unusedInline, file],
} as unknown as Email;
it('hands a reply the image its quote embeds and nothing else', () => {
expect(replyComposeParams('reply', withImage)?.replyTo?.attachments).toEqual([quotedImage]);
expect(replyComposeParams('reply', source)?.replyTo?.attachments).toBeUndefined();
});
it('hands a forward the quoted image and the files, not unused inline parts', () => {
expect(replyComposeParams('forward', withImage)?.replyTo?.attachments).toEqual([quotedImage, file]);
});
});
+19 -6
View File
@@ -1,6 +1,23 @@
import type { Email } from '../api/types';
import type { Attachment, Email } from '../api/types';
import type { RootStackParamList } from '../navigation/types';
import { pickEmailBody, plainTextBody } from './email-body';
import { extractCidRefs } from './email-html';
import { stripMessageIdBrackets } from './email-threading';
/**
* The parts a reply or forward hands the composer. The inline images the
* quoted HTML embeds go along in both modes: the composer fetches them into
* the quote and attaches them again on send (it never shows them as chips).
* Without them a quoted `cid:` image went out pointing at a part the new
* message did not have. A forward also carries the real attachments.
*/
function carriedParts(mode: 'reply' | 'replyAll' | 'forward', source: Email, quoteHtml?: string): Attachment[] | undefined {
const quoted = new Set(quoteHtml ? extractCidRefs(quoteHtml) : []);
const isQuoted = (a: Attachment) => !!a.cid && quoted.has(stripMessageIdBrackets(a.cid));
const parts = (source.attachments ?? []).filter((a) =>
isQuoted(a) || (mode === 'forward' && !(a.disposition === 'inline' && a.cid)));
return parts.length > 0 || mode === 'forward' ? parts : undefined;
}
/**
* Composer params for a reply / reply-all / forward of `source`. Shared by
@@ -42,11 +59,7 @@ export function replyComposeParams(
// Threading needs the RFC Message-ID, never the JMAP object id (#234).
messageId: source.messageId ?? undefined,
references: source.references ?? undefined,
// Forward carries the original attachments as blob refs; cid-embedded
// inline images are already part of the quoted HTML.
attachments: mode === 'forward'
? (source.attachments ?? []).filter((a) => !(a.disposition === 'inline' && a.cid))
: undefined,
attachments: carriedParts(mode, source, quoteHtml),
originalEmailId: source.id,
jmapAccountId: ownerAccountId,
},