fix: stop URL-encoding drag-out filenames and preserve Unicode letters

This commit is contained in:
Linus Rath
2026-05-22 14:49:17 +02:00
parent ca0d6805cf
commit 0dca019fe5
3 changed files with 18 additions and 12 deletions
+5 -3
View File
@@ -92,9 +92,11 @@ export function useAttachmentDrag(
return; return;
} }
// `DownloadURL` format: <mime>:<filename>:<url>. Chromium reads this on // `DownloadURL` format: <mime>:<filename>:<url>. The filename must be
// drop and writes a real file at the destination. // raw - URL-encoding it lands literally on disk (`%20` instead of a
e.dataTransfer.setData("DownloadURL", `${type}:${encodeURIComponent(name)}:${url}`); // space). Callers are expected to sanitise reserved chars (`:` etc.)
// beforehand.
e.dataTransfer.setData("DownloadURL", `${type}:${name}:${url}`);
e.dataTransfer.effectAllowed = "copyMove"; e.dataTransfer.effectAllowed = "copyMove";
}, },
[source.name, source.type, prefetch], [source.name, source.type, prefetch],
+7 -4
View File
@@ -206,11 +206,14 @@ export function useEmailDrag({ email, sourceMailboxId, threadEmails }: UseEmailD
const url = singleBlobUrlRef.current; const url = singleBlobUrlRef.current;
if (url) { if (url) {
const name = emailExportFilename(emailsToDrag[0], emailTemplate); const name = emailExportFilename(emailsToDrag[0], emailTemplate);
// `DownloadURL` format: <mime>:<filename>:<url>. Chromium reads this // `DownloadURL` format: <mime>:<filename>:<url>. Chromium expects
// on drop and writes a real file; Firefox/Safari ignore it. // the filename raw - URL-encoding it ends up literally on disk
// (e.g. `%20` instead of a space). The sanitiser already removed
// `:` and other reserved chars, so embedding the name as-is is
// safe. Firefox/Safari ignore this entry entirely.
e.dataTransfer.setData( e.dataTransfer.setData(
"DownloadURL", "DownloadURL",
`message/rfc822:${encodeURIComponent(name)}:${url}`, `message/rfc822:${name}:${url}`,
); );
} else { } else {
// Not warmed up yet — kick off so the next attempt works. Don't // Not warmed up yet — kick off so the next attempt works. Don't
@@ -222,7 +225,7 @@ export function useEmailDrag({ email, sourceMailboxId, threadEmails }: UseEmailD
if (ready) { if (ready) {
e.dataTransfer.setData( e.dataTransfer.setData(
"DownloadURL", "DownloadURL",
`application/zip:${encodeURIComponent(ready.name)}:${ready.url}`, `application/zip:${ready.name}:${ready.url}`,
); );
} else { } else {
// Kick off the bundle build for the next attempt. // Kick off the bundle build for the next attempt.
+6 -5
View File
@@ -1,10 +1,11 @@
import type { Email } from "@/lib/jmap/types"; import type { Email } from "@/lib/jmap/types";
// Restrict filenames to ASCII letters/digits and a small set of safe // Allow any Unicode letter or digit (so umlauts, accents, CJK survive) plus a
// punctuation. Everything else collapses to `_`. Keeps names predictable // small set of safe punctuation. Everything else - emojis, RTL/zero-width
// across Windows/macOS/Linux file systems and avoids emoji/RTL/zero-width // marks, control chars, and the filesystem-reserved `<>:"/\|?*` - collapses
// surprises in subject lines. // to `_`. Keeps filenames usable across Windows/macOS/Linux without flattening
const SAFE_CHARS = /[^A-Za-z0-9 _\-().,!@#&+=[\]{}']/g; // non-ASCII scripts.
const SAFE_CHARS = /[^\p{L}\p{N} _\-().,!@#&+=[\]{}']/gu;
export const DEFAULT_EMAIL_TEMPLATE = "{date} ({from}-{to}) {subject}"; export const DEFAULT_EMAIL_TEMPLATE = "{date} ({from}-{to}) {subject}";
export const DEFAULT_ATTACHMENT_TEMPLATE = "{filename}"; export const DEFAULT_ATTACHMENT_TEMPLATE = "{filename}";