From edd11ac27b6d074afcbbf943c23560a15445ab8f Mon Sep 17 00:00:00 2001 From: Aaron Guise Date: Wed, 29 Jul 2026 11:41:34 +1200 Subject: [PATCH 1/3] fix: strip from/to names from forward-as-attachment filenames buildForwardAsAttachmentPayload named the synthetic .eml attachment using the user's own emailDownloadTemplate, which by default embeds sender/recipient display names. Since this attachment can go to an external recipient (e.g. an upstream spam gateway, or anyone else), the filename now always renders as "{date}-{subject}.eml" regardless of the user's configured template, while still honoring their space/case/diacritics preferences. --- lib/__tests__/forward-as-attachment.test.ts | 24 +++++++++++++++++++-- lib/forward-as-attachment.ts | 16 ++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/lib/__tests__/forward-as-attachment.test.ts b/lib/__tests__/forward-as-attachment.test.ts index 4dc92cf4..9f98ca28 100644 --- a/lib/__tests__/forward-as-attachment.test.ts +++ b/lib/__tests__/forward-as-attachment.test.ts @@ -1,3 +1,6 @@ +// Pin TZ so the local-time date rendering in the filename test is deterministic. +process.env.TZ = 'UTC'; + import { describe, it, expect } from 'vitest'; import { buildForwardAsAttachmentPayload } from '@/lib/forward-as-attachment'; import type { Email } from '@/lib/jmap/types'; @@ -52,13 +55,30 @@ describe('buildForwardAsAttachmentPayload', () => { expect(payload?.subject).toBe(''); }); - it('honors a custom filename template, matching "Export as .eml" naming instead of always using the default', () => { + it('applies user space/case transforms but ignores a custom filename template, unlike "Export as .eml"', () => { const email = makeEmail({ subject: 'Missed spam example' }); const payload = buildForwardAsAttachmentPayload(email, 'Fwd:', { template: 'custom-{subject}', lowercase: true, spaceReplacement: 'dash', }); - expect(payload?.attachment.name).toBe('custom-missed-spam-example.eml'); + expect(payload?.attachment.name).toBe('2026-07-26-22.25.22-missed-spam-example.eml'); + }); + + it('uses a dash between date and subject by default', () => { + const email = makeEmail({ subject: 'Missed spam example' }); + const payload = buildForwardAsAttachmentPayload(email, 'Fwd:'); + expect(payload?.attachment.name).toBe('2026-07-26 22.25.22-Missed spam example.eml'); + }); + + it('never includes from/to in the filename, even with the default template, to avoid leaking names to the recipient', () => { + const email = makeEmail({ + subject: 'Missed spam example', + from: [{ name: 'Alice Sender', email: 'alice@example.com' }], + to: [{ name: "'Bobby'", email: 'bob@example.com' }], + }); + const payload = buildForwardAsAttachmentPayload(email, 'Fwd:'); + expect(payload?.attachment.name).not.toContain('Alice'); + expect(payload?.attachment.name).not.toContain('Bobby'); }); }); diff --git a/lib/forward-as-attachment.ts b/lib/forward-as-attachment.ts index 42c4bde8..d0feb48d 100644 --- a/lib/forward-as-attachment.ts +++ b/lib/forward-as-attachment.ts @@ -24,12 +24,14 @@ export interface ForwardAsAttachmentPayload { * not per-email, so the same blobId a message already has can be attached * to a brand new outgoing email directly. * - * `filenameOptions`, when passed, should be the same options the caller - * uses for "Export as .eml" / drag-out (the user's configured filename - * template, space/case/diacritics transforms - see - * useSettingsStore's emailDownloadTemplate and friends), so the two - * actions produce consistent filenames for the same message. Falls back - * to emailExportFilename's own default template when omitted. + * `filenameOptions`, when passed, carries the user's configured space/case/ + * diacritics transforms (see useSettingsStore's filenameSpaceReplacement + * and friends) for consistency with "Export as .eml" / drag-out. Its + * `template`, if any, is ignored: this attachment goes out to a possibly + * external recipient (spam gateway, another person), so the filename is + * always just "{date}-{subject}.eml" - never the user's own from/to naming + * template, which could otherwise leak sender/recipient names into an + * attachment filename visible to that recipient. * * Returns null when the email has no blobId (nothing to reference). */ @@ -48,7 +50,7 @@ export function buildForwardAsAttachmentPayload( subject: email.subject ? buildForwardSubject(email.subject, forwardPrefix) : "", attachment: { blobId: email.blobId, - name: emailExportFilename(email, filenameOptions), + name: emailExportFilename(email, { ...filenameOptions, template: "{date}-{subject}" }), type: "message/rfc822", size: email.size, }, From 71565e93283dc1d7ee35ef4b7a97e1e1320314f5 Mon Sep 17 00:00:00 2001 From: Aaron Guise Date: Wed, 29 Jul 2026 11:47:42 +1200 Subject: [PATCH 2/3] test: restore TZ after pinning it in forward-as-attachment tests Setting process.env.TZ at module scope without restoring it could leak into other test files sharing the same Vitest worker. Match the beforeAll/afterAll restore pattern already used in lib/__tests__/calendar-utils.test.ts. --- lib/__tests__/forward-as-attachment.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/__tests__/forward-as-attachment.test.ts b/lib/__tests__/forward-as-attachment.test.ts index 9f98ca28..40d3d5d7 100644 --- a/lib/__tests__/forward-as-attachment.test.ts +++ b/lib/__tests__/forward-as-attachment.test.ts @@ -1,10 +1,18 @@ -// Pin TZ so the local-time date rendering in the filename test is deterministic. -process.env.TZ = 'UTC'; - -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { buildForwardAsAttachmentPayload } from '@/lib/forward-as-attachment'; import type { Email } from '@/lib/jmap/types'; +// Pin TZ so the local-time date rendering in the filename test is deterministic, +// restoring it after so this doesn't leak into other test files in the same worker. +let originalTZ: string | undefined; +beforeAll(() => { + originalTZ = process.env.TZ; + process.env.TZ = 'UTC'; +}); +afterAll(() => { + process.env.TZ = originalTZ; +}); + function makeEmail(overrides: Partial = {}): Email { return { id: 'e1', From 3108b2f3362bb9a0f40273f68fe8e99f27b1d66f Mon Sep 17 00:00:00 2001 From: Aaron Guise Date: Wed, 29 Jul 2026 13:31:56 +1200 Subject: [PATCH 3/3] fix: avoid leaving TZ="undefined" when restoring an unset timezone process.env coerces assigned values to strings, so process.env.TZ = originalTZ left the literal string "undefined" behind (instead of clearing TZ) when it was unset before the test ran. Delete the var in that case instead of assigning undefined. --- lib/__tests__/forward-as-attachment.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/__tests__/forward-as-attachment.test.ts b/lib/__tests__/forward-as-attachment.test.ts index 40d3d5d7..ceec2b7f 100644 --- a/lib/__tests__/forward-as-attachment.test.ts +++ b/lib/__tests__/forward-as-attachment.test.ts @@ -10,7 +10,10 @@ beforeAll(() => { process.env.TZ = 'UTC'; }); afterAll(() => { - process.env.TZ = originalTZ; + // process.env coerces to strings, so `= undefined` would leave the literal + // string "undefined" behind when TZ was originally unset - delete instead. + if (originalTZ === undefined) delete process.env.TZ; + else process.env.TZ = originalTZ; }); function makeEmail(overrides: Partial = {}): Email {