mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
From 0.16.6 Stalwart refuses FileNode names with `/<>:"\|?*` or a
reserved name such as `CON` ("Name contains a forbidden character."),
and publishes the rules as `forbiddenNameChars` / `forbiddenNodeNames`.
Bulwark did not read them: the error showed up only after an upload had
started, and it aborted the rest of a multi-file or folder upload.
- new folder, new text file and rename now say which characters or
names the server does not allow, before sending anything
- uploads store an accepted variant instead: forbidden characters
become `_` and a reserved name gets `_` appended
MIME types over 30 characters (most Office formats) were always stored
as `application/octet-stream`, because Stalwart before 0.16.6 refuses
them. From 0.16.6 the limit is 255, so the real type is kept there.
46 lines
2.1 KiB
TypeScript
46 lines
2.1 KiB
TypeScript
/**
|
|
* FileNode naming rules a server publishes in its filenode capability
|
|
* (`forbiddenNameChars`, `forbiddenNodeNames`; Stalwart 0.16.6+). A name that
|
|
* breaks them is refused ("Name contains a forbidden character." / "Name is
|
|
* reserved and cannot be used."), which used to surface only after an upload
|
|
* had started and then aborted the rest of the batch.
|
|
*/
|
|
export interface FileNameRules {
|
|
forbiddenChars: string;
|
|
forbiddenNames: string[];
|
|
}
|
|
|
|
export type FileNameProblem = { kind: 'chars'; chars: string } | { kind: 'reserved' };
|
|
|
|
export function fileNameRulesFrom(capability: unknown): FileNameRules | null {
|
|
if (!capability || typeof capability !== 'object') return null;
|
|
const cap = capability as { forbiddenNameChars?: unknown; forbiddenNodeNames?: unknown };
|
|
const forbiddenChars = typeof cap.forbiddenNameChars === 'string' ? cap.forbiddenNameChars : '';
|
|
const forbiddenNames = Array.isArray(cap.forbiddenNodeNames)
|
|
? cap.forbiddenNodeNames.filter((n): n is string => typeof n === 'string')
|
|
: [];
|
|
if (!forbiddenChars && forbiddenNames.length === 0) return null;
|
|
return { forbiddenChars, forbiddenNames };
|
|
}
|
|
|
|
export function fileNameProblem(name: string, rules: FileNameRules | null): FileNameProblem | null {
|
|
if (!rules) return null;
|
|
const bad = [...new Set([...name].filter(c => rules.forbiddenChars.includes(c)))];
|
|
if (bad.length) return { kind: 'chars', chars: bad.join(' ') };
|
|
// Matched against the whole name, ignoring case: "CON" is reserved, "CON.txt" is not.
|
|
if (rules.forbiddenNames.some(n => n.toLowerCase() === name.toLowerCase())) return { kind: 'reserved' };
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A variant of `name` the server accepts: forbidden characters become "_",
|
|
* a reserved name gets "_" appended. Used for uploads, where the name comes
|
|
* from the local file system and cannot be asked for again.
|
|
*/
|
|
export function acceptedFileName(name: string, rules: FileNameRules | null): string {
|
|
if (!rules) return name;
|
|
let out = [...name].map(c => (rules.forbiddenChars.includes(c) ? '_' : c)).join('');
|
|
if (rules.forbiddenNames.some(n => n.toLowerCase() === out.toLowerCase())) out = `${out}_`;
|
|
return out;
|
|
}
|