fix: leave bounce and receipt report parts out of the list's attachment chips

This commit is contained in:
Linus Rath
2026-10-31 04:00:21 +01:00
parent 352973793f
commit 7772da8463
3 changed files with 47 additions and 5 deletions
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { isReportPart } from '../attachment-display';
import { realAttachments } from '../list-attachments';
import type { Attachment } from '../../api/types';
// A bounce's delivery status and a read receipt's disposition showed as
// attachment chips in the list (the reader already hid them), and the RFC
// 6533 `global-` forms counted as real files everywhere (webmail 8a09ba573).
const part = (type: string, name: string): Attachment => ({ blobId: `b-${name}`, type, name, size: 1 } as Attachment);
describe('report parts in list chips', () => {
it('counts the global- forms as report parts', () => {
expect(isReportPart('message/global-delivery-status')).toBe(true);
expect(isReportPart('message/global-disposition-notification')).toBe(true);
expect(isReportPart('Message/Delivery-Status; charset=utf-8')).toBe(true);
expect(isReportPart('message/rfc822')).toBe(false);
});
it('leaves report parts out of the list chips', () => {
const parts = [
part('message/delivery-status', 'details.txt'),
part('message/global-disposition-notification', 'receipt'),
part('application/pdf', 'invoice.pdf'),
];
expect(realAttachments(parts).map((a) => a.name)).toEqual(['invoice.pdf']);
});
});
+15 -3
View File
@@ -56,10 +56,22 @@ export function isCalendarMimeType(type?: string | null): boolean {
return CAL_MIME.has(type.split(';')[0].trim().toLowerCase());
}
/** Machine-readable report parts (MDN receipts, DSN bounces) - never real attachments. */
/**
* Machine-readable report parts: the delivery status of a bounce (RFC 3464),
* the disposition of a read receipt (RFC 8098) and their RFC 6533 UTF-8
* forms. A multipart/report needs them, but they are not files anyone
* attached, so no view lists them and a forward leaves them out (webmail
* lib/attachment-visibility.ts).
*/
const REPORT_PART_TYPES: ReadonlySet<string> = new Set([
'message/delivery-status',
'message/disposition-notification',
'message/global-delivery-status',
'message/global-disposition-notification',
]);
export function isReportPart(type?: string | null): boolean {
const base = (type || '').split(';')[0].trim().toLowerCase();
return base === 'message/disposition-notification' || base === 'message/delivery-status';
return REPORT_PART_TYPES.has((type || '').split(';')[0].trim().toLowerCase());
}
export function isRfc822Attachment(att: Pick<Attachment, 'name' | 'type'>): boolean {
+4 -2
View File
@@ -1,4 +1,5 @@
import type { Attachment, Email } from '../api/types';
import { isReportPart } from './attachment-display';
/**
* Attachment chips for message-list rows (webmail 0e1c34cd, #1089).
@@ -14,11 +15,12 @@ import type { Attachment, Email } from '../api/types';
/**
* The parts the sender actually attached. Anything marked inline or
* referenced by a Content-ID (signature logos, spacers, tracking pixels) is
* dropped, as are parts without a filename, which can't be labelled or saved.
* dropped, as are parts without a filename, which can't be labelled or saved,
* and the report parts of a bounce or a read receipt.
*/
export function realAttachments(attachments?: readonly Attachment[] | null): Attachment[] {
if (!attachments?.length) return [];
return attachments.filter((a) => a.disposition !== 'inline' && !a.cid && !!a.name);
return attachments.filter((a) => a.disposition !== 'inline' && !a.cid && !!a.name && !isReportPart(a.type));
}
/**