Files
webmail/lib/watermark.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

37 lines
2.0 KiB
TypeScript

/**
* The watermark of the device policy `watermark` (lib/device-policy.ts): the
* user's address and the date, repeated across the reader and printouts.
* Real text elements, not a background image, so printers show it even
* when "print backgrounds" is off. A fixed layer repeats on every printed
* page.
*
* It can be removed with the browser's tools; the docs say so.
*/
import { escapeHtml } from '@/lib/email-sanitization';
/** How many copies of the text the layer holds (rows x columns). */
export const WATERMARK_ROWS = 8;
export const WATERMARK_COLUMNS = 3;
/** Styles for the layer and its copies, shared by the React overlay and the print documents. */
export const WATERMARK_CSS = `
.bulwark-watermark { position: absolute; inset: 0; overflow: hidden; pointer-events: none; user-select: none; z-index: 6;
display: grid; grid-template-columns: repeat(${WATERMARK_COLUMNS}, 1fr); align-content: space-around; justify-items: center; }
.bulwark-watermark[data-fixed] { position: fixed; z-index: 2147483000; }
.bulwark-watermark span { transform: rotate(-30deg); white-space: nowrap; font: 600 14px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: rgba(120, 120, 120, 0.22); padding: 40px 8px; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
@media print { .bulwark-watermark span { color: rgba(90, 90, 90, 0.28); } }
`;
/** The layer as HTML for a print document (escaped). `fixed` repeats it on every page. */
export function watermarkHtml(text: string, options: { fixed?: boolean } = {}): string {
const copy = `<span>${escapeHtml(text)}</span>`;
return `<div class="bulwark-watermark" aria-hidden="true" data-testid="watermark"${options.fixed ? ' data-fixed=""' : ''}>${copy.repeat(WATERMARK_ROWS * WATERMARK_COLUMNS)}</div>`;
}
/** The styles and layer for the head and body of a print document. */
export function watermarkForPrint(text: string): { style: string; html: string } {
return { style: `<style>${WATERMARK_CSS}</style>`, html: watermarkHtml(text, { fixed: true }) };
}