fix: cap filename tokens at the full 200-char limit, not 80
renderRaw (and the attachment-template renderer) sanitised each {token}
with sanitizePart's default 80-char cap, so a single long token such as
{subject} was truncated to 80 — well before the documented 200-char
filename limit, which was therefore unreachable per token. Introduce a
FILENAME_MAX_LEN (200) constant and use it for the per-token cap so the
overall limit governs. Adds tests.
This commit is contained in:
committed by
Linus Rath
parent
ddb596affc
commit
5306f7c548
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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, string>): 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`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user