diff --git a/src/lib/__tests__/reply-compose.test.ts b/src/lib/__tests__/reply-compose.test.ts index 16d65bda..e1269f7c 100644 --- a/src/lib/__tests__/reply-compose.test.ts +++ b/src/lib/__tests__/reply-compose.test.ts @@ -68,3 +68,18 @@ describe('replyComposeParams: quoted inline images', () => { expect(replyComposeParams('forward', withImage)?.replyTo?.attachments).toEqual([quotedImage, file]); }); }); + +describe('replyComposeParams: report parts', () => { + const file = { blobId: 'b-pdf', type: 'application/pdf', name: 'report.pdf', disposition: 'attachment' }; + const reports = [ + { blobId: 'r1', type: 'message/delivery-status', name: null, disposition: 'attachment' }, + { blobId: 'r2', type: 'message/disposition-notification', name: null, disposition: 'attachment' }, + { blobId: 'r3', type: 'message/global-delivery-status', name: null, disposition: 'attachment' }, + { blobId: 'r4', type: 'Message/Global-Disposition-Notification; charset=utf-8', name: null, disposition: 'attachment' }, + ]; + const bounce = { ...source, attachments: [...reports, file] } as unknown as Email; + + it('leaves bounce and receipt reports, the global forms too, out of a forward', () => { + expect(replyComposeParams('forward', bounce)?.replyTo?.attachments).toEqual([file]); + }); +}); diff --git a/src/lib/reply-compose.ts b/src/lib/reply-compose.ts index f46c6d97..a066a220 100644 --- a/src/lib/reply-compose.ts +++ b/src/lib/reply-compose.ts @@ -1,9 +1,25 @@ import type { Attachment, Email } from '../api/types'; import type { RootStackParamList } from '../navigation/types'; +import { isReportPart } from './attachment-display'; import { pickEmailBody, plainTextBody } from './email-body'; import { extractCidRefs } from './email-html'; import { stripMessageIdBrackets } from './email-threading'; +// Bounce and read-receipt reports (RFC 3464, RFC 8098) and their +// internationalised forms (RFC 6533). They describe the message they came +// with and mean nothing attached to another (webmail +// lib/attachment-visibility.ts, 8a09ba573). +const REPORT_TYPES = new Set([ + 'message/delivery-status', + 'message/disposition-notification', + 'message/global-delivery-status', + 'message/global-disposition-notification', +]); + +function isReport(type?: string | null): boolean { + return isReportPart(type) || REPORT_TYPES.has((type || '').split(';')[0].trim().toLowerCase()); +} + /** * 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 @@ -15,7 +31,7 @@ function carriedParts(mode: 'reply' | 'replyAll' | 'forward', source: Email, quo 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))); + !isReport(a.type) && (isQuoted(a) || (mode === 'forward' && !(a.disposition === 'inline' && a.cid)))); return parts.length > 0 || mode === 'forward' ? parts : undefined; }