From 9d4d3261da1bf428affcc5ab56031b90b6770794 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sun, 1 Nov 2026 06:50:13 +0100
Subject: [PATCH] fix: clear exported mail from the browser's storage and say
plainly where an export goes
- The private file an export writes in Firefox and Safari is deleted
when the app starts and when someone signs in, not only at the next
export, and at once on sign-out. Close keeps it until its download
had time to finish. Each tab holds its own files under a Web Lock, so
one tab's sweep no longer deletes another tab's running export.
- A browser whose private file system can't write files (Safari before
26) is told the export stays in memory instead of being promised a
download.
- The export's live region announces only the phase and pauses; the
count next to it no longer floods screen readers, and the format
group is named once.
---
.../__tests__/mail-export-dialog.test.tsx | 22 ++++
.../mail-transfer/mail-export-dialog.tsx | 21 ++--
.../mail-transfer/mail-import-dialog.tsx | 6 +-
.../mail-transfer/mail-transfer-host.tsx | 11 +-
lib/mail-export/__tests__/sinks.test.ts | 104 ++++++++++++++++++
lib/mail-export/sinks.ts | 84 ++++++++++++--
lib/sign-out-cleanup.ts | 2 +-
stores/mail-transfer-store.ts | 15 ++-
8 files changed, 234 insertions(+), 31 deletions(-)
create mode 100644 lib/mail-export/__tests__/sinks.test.ts
diff --git a/components/mail-transfer/__tests__/mail-export-dialog.test.tsx b/components/mail-transfer/__tests__/mail-export-dialog.test.tsx
index 53656da6f..bce120993 100644
--- a/components/mail-transfer/__tests__/mail-export-dialog.test.tsx
+++ b/components/mail-transfer/__tests__/mail-export-dialog.test.tsx
@@ -129,4 +129,26 @@ describe('MailExportDialog', () => {
expect(screen.getByTestId('mail-transfer-chip').textContent).toContain('chip');
expect(controller.signal.aborted).toBe(false);
});
+
+ it('keeps the running count out of the live region', () => {
+ render();
+ act(() => {
+ openMailExport({ mailboxId: 'inbox' });
+ useMailTransferStore.getState().startJob({ kind: 'export', controller: new AbortController(), progress: { phase: 'running', exported: 1, failed: 0, failures: [], total: 4, fraction: 0.25 } });
+ });
+ expect(screen.getByTestId('mail-export-status').textContent).toBe('running_title');
+ expect(screen.getByTestId('mail-export-count').textContent).toBe('status_exported');
+ });
+
+ it('says the export stays in memory where the private file system can\'t write files', () => {
+ vi.stubGlobal('navigator', { ...navigator, storage: { getDirectory: async () => ({}) } });
+ vi.stubGlobal('FileSystemFileHandle', class {});
+ try {
+ render();
+ act(() => openMailExport());
+ expect(screen.getByText('save_memory')).toBeTruthy();
+ } finally {
+ vi.unstubAllGlobals();
+ }
+ });
});
diff --git a/components/mail-transfer/mail-export-dialog.tsx b/components/mail-transfer/mail-export-dialog.tsx
index af90c91a9..0f7ecf6c3 100644
--- a/components/mail-transfer/mail-export-dialog.tsx
+++ b/components/mail-transfer/mail-export-dialog.tsx
@@ -9,7 +9,7 @@ import { useAuthStore } from "@/stores/auth-store";
import { isJobActive, useMailTransferStore, type ExportRequest } from "@/stores/mail-transfer-store";
import type { ExportFormat, ExportProgress } from "@/lib/mail-export/engine";
import { safeName } from "@/lib/mail-export/engine";
-import { canPickSaveFile, memorySink, pickSaveFile, privateFileSink, sweepPrivateExports, type ExportSink } from "@/lib/mail-export/sinks";
+import { canPickSaveFile, canWritePrivateFile, memorySink, pickSaveFile, privateFileSink, sweepPrivateExports, type ExportSink } from "@/lib/mail-export/sinks";
import { formatFileSize } from "@/lib/utils";
import { formatClock, TransferBar } from "./transfer-progress";
import { startExport } from "./start-export";
@@ -21,8 +21,7 @@ function today(): string {
/** Where the file will go in this browser, said before the export starts. */
function saveMode(): "picker" | "download" | "memory" {
if (canPickSaveFile()) return "picker";
- const storage = typeof navigator !== "undefined" ? (navigator.storage as { getDirectory?: unknown } | undefined) : undefined;
- return typeof storage?.getDirectory === "function" ? "download" : "memory";
+ return canWritePrivateFile() ? "download" : "memory";
}
export function MailExportDialog({ request }: { request: ExportRequest }) {
@@ -103,7 +102,7 @@ export function MailExportDialog({ request }: { request: ExportRequest }) {
{progress.phase === "done" && progress.file?.download && (
)}
-
+
>
)}
>
@@ -120,14 +119,14 @@ export function MailExportDialog({ request }: { request: ExportRequest }) {
return (