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>
pdf.js 6.2.108, vendored
Mozilla's PDF renderer, committed rather than installed. plMail renders PDF attachments in the
browser because the server has no PDF capability and is not gaining any — see
App\Service\Mail\AttachmentThumbnailer, whose docblock says as much, and the Dockerfile, which
must build on arm64 as well as amd64.
Why this is not in importmap.php
pdf.js needs cMapUrl, standardFontDataUrl and wasmUrl as directory prefixes — it
concatenates bare filenames onto them at runtime (${wasmUrl}${filename}). AssetMapper digests
every filename it compiles, so a stable directory under /assets/ cannot exist, and extraFiles
in assets/vendor/installed.php is populated only from url() matches inside CSS. There is no
mechanism to declare arbitrary companion files for a JS package.
public/ is Caddy's root, so this is served as static files with correct MIME types and no
application code, exactly like public/icons and public/sw.js. The version is in the path, so an
upgrade is: add the new directory, bump App\Twig\PdfJsExtension::VERSION, delete the old one.
@cantoo/pdf-lib — the write side, used for signing — does go through the importmap, because it
has no worker and no data directory. Two mechanisms, each where it fits.
What is deliberately absent
The .wasm binaries. pdf.js 6 uses WebAssembly for JBIG2, JPEG 2000 and ICC colour, and the
enforced Content Security Policy carries no 'wasm-unsafe-eval', so WebAssembly.instantiate
throws. The package ships pure-JS fallbacks and the loader uses them when useWasm: false — which
the viewer passes. Shipping the .wasm files anyway would be dead weight that the browser is
forbidden to run, and attempting them logs a CSP violation on every scanned PDF, which would fail
tests/e2e/csp.spec.ts.
The cost is that JPEG 2000 and JBIG2 scans decode in JavaScript — slower on exactly the scanned
documents people want to sign. If that ever proves too slow, the one change is adding
'wasm-unsafe-eval' to script-src in ContentSecurityPolicyListener::FULL; it permits compiling
WebAssembly but does not re-enable string-to-code the way 'unsafe-eval' does. Measure first.
Also absent: iccs/ (needs the WASM path), legacy/, image_decoders/, types/, web/ (the
reference viewer — plMail has its own), and the unminified builds and source maps.
How this directory was produced
npm pack pdfjs-dist@6.2.108
tar xzf pdfjs-dist-6.2.108.tgz
V=6.2.108; DEST="public/pdfjs/$V"
mkdir -p "$DEST/build" "$DEST/wasm"
cp package/build/pdf.min.mjs package/build/pdf.worker.min.mjs "$DEST/build/"
cp -r package/cmaps package/standard_fonts "$DEST/"
cp package/wasm/jbig2_nowasm_fallback.js package/wasm/openjpeg_nowasm_fallback.js "$DEST/wasm/"
cp package/wasm/LICENSE_* "$DEST/wasm/"
cp package/LICENSE "$DEST/"
Licences: LICENSE (Apache-2.0, pdf.js) and wasm/LICENSE_* for the bundled decoders.