diff --git a/lib/__tests__/download-filename.test.ts b/lib/__tests__/download-filename.test.ts index 455483c9..7302713f 100644 --- a/lib/__tests__/download-filename.test.ts +++ b/lib/__tests__/download-filename.test.ts @@ -41,11 +41,11 @@ describe('emailExportFilename', () => { expect(emailExportFilename(makeEmail({}), '{unknown_token}')).toBe('email.eml'); }); - it('CHARACTERISATION: per-token sanitise caps each value at 80 chars', () => { - // sanitizePart defaults to maxLen=80, applied per {token} during render — - // so a single long {subject} is truncated to 80 well before the 200 cap. + it('lets a single long token reach the 200-char filename cap', () => { + // Each {token} is now capped at FILENAME_MAX_LEN (200), so the overall + // filename limit governs instead of an earlier 80-char per-token cap. const out = emailExportFilename(makeEmail({ subject: 'a'.repeat(300) }), '{subject}'); - expect(out).toBe('a'.repeat(80) + '.eml'); + expect(out).toBe('a'.repeat(200) + '.eml'); }); }); diff --git a/lib/download-filename.ts b/lib/download-filename.ts index cbf4cad1..11dcc00e 100644 --- a/lib/download-filename.ts +++ b/lib/download-filename.ts @@ -64,6 +64,11 @@ export const BUNDLE_TOKENS: { token: string; description: string }[] = [ { token: "day", description: "2-digit day" }, ]; +// Overall cap for a generated filename stem. Also used as the per-token cap so +// a single long token (e.g. {subject}) isn't truncated earlier than the final +// filename would be. +const FILENAME_MAX_LEN = 200; + function sanitizePart(input: string, maxLen = 80): string { const cleaned = input .replace(SAFE_CHARS, "_") @@ -173,7 +178,7 @@ function renderRaw(template: string, vars: Record): string { return template.replace(/\{(\w+)\}/g, (_, key: string) => { const value = vars[key]; if (value === undefined) return ""; - return sanitizePart(value); + return sanitizePart(value, FILENAME_MAX_LEN); }); } @@ -184,9 +189,9 @@ export function emailExportFilename( const opts = typeof options === "string" ? { template: options } : options; const template = opts.template ?? DEFAULT_EMAIL_TEMPLATE; const rendered = renderRaw(template, emailVars(email)); - const cleaned = sanitizePart(rendered, 200); + const cleaned = sanitizePart(rendered, FILENAME_MAX_LEN); const transformed = applyTransforms(cleaned, opts); - const stem = transformed.slice(0, 200) || "email"; + const stem = transformed.slice(0, FILENAME_MAX_LEN) || "email"; return `${stem}.eml`; } @@ -199,7 +204,7 @@ export function attachmentDownloadFilename( const template = opts.template ?? DEFAULT_ATTACHMENT_TEMPLATE; if (!email) { const filename = (attachment.name || "attachment").trim(); - const cleaned = sanitizePart(filename, 200) || "attachment"; + const cleaned = sanitizePart(filename, FILENAME_MAX_LEN) || "attachment"; return applyTransforms(cleaned, opts) || cleaned; } const vars = attachmentVars(email, attachment); @@ -208,10 +213,10 @@ export function attachmentDownloadFilename( if (value === undefined) return ""; // Preserve dots in {filename} so the original extension survives the // sanitiser (it strips trailing dots otherwise). - return key === "filename" ? value.replace(SAFE_CHARS, "_") : sanitizePart(value); + return key === "filename" ? value.replace(SAFE_CHARS, "_") : sanitizePart(value, FILENAME_MAX_LEN); }); const templateMentionsExt = /\{(ext|filename)\}/.test(template); - const cleaned = sanitizePart(rendered, 200) || "attachment"; + const cleaned = sanitizePart(rendered, FILENAME_MAX_LEN) || "attachment"; if (templateMentionsExt) { return applyTransforms(cleaned, opts) || cleaned; } @@ -235,9 +240,9 @@ export function bundleExportFilename( const opts = typeof options === "string" ? { template: options } : options; const template = opts.template ?? DEFAULT_BUNDLE_TEMPLATE; const rendered = renderRaw(template, bundleVars(count, iso)); - const cleaned = sanitizePart(rendered, 200); + const cleaned = sanitizePart(rendered, FILENAME_MAX_LEN); const transformed = applyTransforms(cleaned, opts); - const stem = transformed.slice(0, 200) || "emails"; + const stem = transformed.slice(0, FILENAME_MAX_LEN) || "emails"; return `${stem}.zip`; }