A PDF attachment opens in a reader inside plMail: pages, zoom, page
position, and the download link the chip always had.
Nothing about how attachments are SERVED changes. The viewer fetches the
existing route, and fetch() ignores Content-Disposition — so PDFs stay
`attachment`, and the inline allow-list is not widened to hand
mail-controlled bytes to the browser's own PDF plugin.
pdf.js is vendored under public/pdfjs/<version>/ rather than declared in
the importmap, because it needs stable directory prefixes for cmaps,
standard fonts and its WASM fallbacks — and AssetMapper digests every
filename it compiles. It loads lazily; 4.7 MB has no business being
parsed on a page that merely contains a PDF chip.
It runs under the enforced policy unchanged: a same-origin module worker
(pdf.js only reaches for its blob: wrapper on the cross-origin branch)
and useWasm: false, since script-src carries no 'wasm-unsafe-eval' and
even ATTEMPTING WASM would log a violation on every scanned document.
An E2E test asserts no securitypolicyviolation while the reader is open,
so an upgrade cannot quietly reintroduce either.
Closing the reader terminates its worker. The controller constructs the
worker itself and hands it over as workerPort, because destroy() asks
the worker to stand down over a message channel that no longer answers
once the modal has emptied its frame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>