Files
webmail/components/mail-transfer/start-export.ts
T
Linus Rath e80f5ade81 feat: let admins turn off downloads, export, printing and the install prompt, and add a watermark
Organisation > Devices now sets policy.json `organisation.devices`, each
switch off by default and scoped per domain or group:

- No downloads: attachments and files only preview (reader, conversation
  view, Pro tab, list chips, .eml previews, the Files app). Download,
  Download all, Open in new tab, Save to Files, drag-out, the browser's
  PDF viewer (pdf.js instead) and media download controls go away.
- No export: no .eml export, View source, message drag-out, or folder and
  account export; a running export stops and its file is deleted when
  the switch turns on. Import stays.
- No printing: no Print buttons in mail, calendar and contacts; Ctrl+P
  prints a note; the reader frame prints blank.
- No install prompt: no card, and the browser's own banner is held back.
- Watermark: the user's address and the date over the reader,
  previews and every printout (mail, calendar, contacts, plugins).

A message follows its own account's policy; what isn't about one message
follows the strictest signed-in account. Every user path hides its
button, and the anchor and new-tab helpers refuse as a backstop
(lib/download-guard.ts), never the blob readers. The plugin APIs
`ui.downloadFile` and `ui.print` refuse too, and the office editor frame
loses `allow-downloads`.

Two assertions in components/mail/__tests__/blob-routing.test.tsx now
expect `{ kind: 'export' }` as the fifth argument of downloadBlob for an
.eml export: the export says what it is, so the backstop checks "No
export" rather than "No downloads" for it.
2026-11-01 08:52:34 +01:00

70 lines
2.4 KiB
TypeScript

import { runExport, folderTree, type ExportFolder, type ExportFormat } from "@/lib/mail-export/engine";
import type { ExportSink } from "@/lib/mail-export/sinks";
import type { IJMAPClient } from "@/lib/jmap/client-interface";
import { useAuthStore } from "@/stores/auth-store";
import { useMailTransferStore } from "@/stores/mail-transfer-store";
import { downloadAllowed } from "@/lib/download-guard";
export interface StartExportInput {
format: ExportFormat;
/** JMAP account of the folder (or of the whole export). */
accountId: string;
/** One folder (its id in its account); the whole account when absent. */
mailboxId?: string;
sink: ExportSink;
}
/** Runs an export as the dialog's job. */
export function startExport(input: StartExportInput): void {
const auth = useAuthStore.getState();
if (!auth.client) return;
// Backstop for the device policy "No export" (the dialog is not offered then).
if (!downloadAllowed("export", auth.activeAccountId ?? null)) {
void input.sink.abort().catch(() => {});
return;
}
const client: IJMAPClient = auth.client;
const controller = new AbortController();
let ended: () => void = () => {};
const finished = new Promise<void>((resolve) => { ended = resolve; });
useMailTransferStore.getState().startJob({
kind: "export",
controller,
scope: auth.activeAccountId ?? "default",
finished,
progress: { phase: "running", exported: 0, failed: 0, failures: [], total: 0, fraction: 0 },
});
void (async () => {
try {
await run();
} finally {
ended();
}
})();
async function run() {
let folders: ExportFolder[];
try {
folders = input.mailboxId
? [{ mailboxId: input.mailboxId, path: [] }]
: folderTree(await client.getMailboxes(input.accountId));
} catch (error) {
await input.sink.abort().catch(() => {});
useMailTransferStore.getState().updateProgress({
phase: "failed", exported: 0, failed: 0, failures: [], total: 0, fraction: 0,
error: error instanceof Error ? error.message : String(error),
});
return;
}
const result = await runExport({
client,
accountId: input.accountId,
folders,
format: input.format,
sink: input.sink,
signal: controller.signal,
onProgress: (progress) => useMailTransferStore.getState().updateProgress(progress),
});
useMailTransferStore.getState().updateProgress(result);
}
}