mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 08:15:41 +00:00
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.
67 lines
2.7 KiB
TypeScript
67 lines
2.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useDevicePolicy, useStrictestDevices } from '@/hooks/use-device-policy';
|
|
import { DOWNLOAD_BLOCKED_EVENT, type DownloadKind } from '@/lib/download-guard';
|
|
// Installs the download guard (lib/download-guard.ts) for every page.
|
|
import '@/lib/device-policy';
|
|
import { useMailTransferStore } from '@/stores/mail-transfer-store';
|
|
import { toast } from '@/stores/toast-store';
|
|
|
|
/** Set on <html> while printing is off; app/print.css then prints only the note. */
|
|
export const NO_PRINT_ATTRIBUTE = 'data-no-print';
|
|
|
|
/**
|
|
* The page-wide parts of the device policies (Organisation > Devices):
|
|
* - says why nothing happened when a download or export was refused;
|
|
* - Ctrl+P and the browser's Print menu print a page that only says
|
|
* printing is turned off (they can't be stopped, only blanked);
|
|
* - an export that runs or waits for its download is stopped and its file
|
|
* deleted when "No export" is switched on while it runs.
|
|
*/
|
|
export function DevicePolicyHost() {
|
|
const t = useTranslations('device_policy');
|
|
const strictest = useStrictestDevices();
|
|
const activeNoExport = useDevicePolicy().noExport;
|
|
|
|
useEffect(() => {
|
|
const onBlocked = (event: Event) => {
|
|
const kind = (event as CustomEvent<{ kind?: DownloadKind }>).detail?.kind;
|
|
toast.info(kind === 'export' ? t('export_blocked') : t('download_blocked'));
|
|
};
|
|
window.addEventListener(DOWNLOAD_BLOCKED_EVENT, onBlocked);
|
|
return () => window.removeEventListener(DOWNLOAD_BLOCKED_EVENT, onBlocked);
|
|
}, [t]);
|
|
|
|
useEffect(() => {
|
|
const root = document.documentElement;
|
|
if (strictest.noPrint) root.setAttribute(NO_PRINT_ATTRIBUTE, '');
|
|
else root.removeAttribute(NO_PRINT_ATTRIBUTE);
|
|
return () => root.removeAttribute(NO_PRINT_ATTRIBUTE);
|
|
}, [strictest.noPrint]);
|
|
|
|
useEffect(() => {
|
|
if (!activeNoExport) return;
|
|
const store = useMailTransferStore.getState();
|
|
if (store.job?.kind !== 'export') return;
|
|
const job = store.job;
|
|
store.cancelJob('policy');
|
|
// The private file goes at once, once the job has stopped: nothing may download it any more.
|
|
const discard = () => {
|
|
if (useMailTransferStore.getState().job?.controller === job.controller) useMailTransferStore.getState().close({ now: true });
|
|
};
|
|
if (job.finished) void job.finished.then(discard, discard);
|
|
else discard();
|
|
}, [activeNoExport]);
|
|
|
|
if (!strictest.noPrint || typeof document === 'undefined') return null;
|
|
return createPortal(
|
|
<div data-print-blocked="" aria-hidden="true">
|
|
<p>{t('print_blocked')}</p>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|